2021年了,`IEnumerator`、`IEnumerable`还傻傻分不清楚?
- IEnumerator接口
- IEnumerable接口
- 最佳实践
- foreach 实质、 微软官方信源
- 20230817补充
IEnumerator
、IEnumerable
这两个接口单词相近、含义相关,傻傻分不清楚。
入行多年,一直没有系统性梳理这对李逵李鬼。
最近本人在怼着why神的《其实吧,LRU也就那么回事》,方案1使用数组实现LUR,手写算法涉及这一对接口,借此机会本次覆盖这一对难缠的冤家。
1. IEnumerator 接口
IEnumerator、IEnumerable接口有相似的名称,这两个接口通常也在一起使用,它们有不同的用途。
IEnumerator接口为类内部的集合提供了迭代姿势, IEnumerator 要求你实现三个方法:
MoveNext
方法: 该方法将集合索引加1,并返回一个bool值,指示是否已到达集合的末尾。Reset
方法: 它将集合索引重置为其初始值-1,这会使枚举数无效。Current
方法: 返回position
位置的当前对象
2. IEnumerable 接口
IEnumerable接口为foreach
迭代提供了支持,IEnumerable申明集合具备可迭代的性质。
要求列表实现GetEnumerator
方法。
public IEnumerator GetEnumerator()
{
return (IEnumerator)this;
}
该用哪一个接口?
仅凭以上辞藻,很难区分两个接口的使用场景。
IEnumerator接口提供了对类中的集合类型对象的迭代,
IEnumerable接口允许使用foreach循环进行枚举, 我可以被迭代,具体的迭代姿势由上面的接口定义
但是,IEnumerable
接口的GetEnumerator
方法会返回一个IEnumerator
接口。要实现IEnumerable
,你还必须实现IEnumerator
。
从英文词根上讲:
IEnumerator接口代表了枚举器,里面定义了枚举姿势;
IEnumerable接口代表该对象具备了可被枚举的性质。
3. 最佳实践
- 在类中嵌套实现IEnumerator枚举器,你甚至可以创建多个枚举器。
- 为IEnumerator枚举器的
Current
方法提供异常处理。
为什么要这么做?
如果集合的内容发生变化,则reset
方法将被调用,紧接着当前枚举数无效,您将收到一个IndexOutOfRangeException
异常(其他情况也可能导致此异常)。
所以执行一个Try…Catch块来捕获这个异常并引发InvalidOperationException
异常, 提示在迭代时不允许修改集合内容。
这也正是我们常见的在foreach 里面尝试修改迭代对象会报
InvalidOperationException
异常的原因。
下面以汽车列表为例实现IEnumerator IEnumerable接口
public class Car
{
public string Brand { get; set; }
public int Age { get; set; }
public Car(string brand, int age)
{
Brand = brand;
Age = age;
}
}
// 可被迭代的集合类
public class Cars : IEnumerable
{
private readonly Car[] Carlist;
public Cars()
{
Carlist = new Car[6]
{
new Car("Ford",1992),
new Car("Fiat",1988),
new Car("Buick",1932),
new Car("Ford",1932),
new Car("Dodge",1999),
new Car("Honda",1977)
};
}
public IEnumerator GetEnumerator()
{
return new MyEnumerator(Carlist);
}
// 内置的MyEnumerator 枚举器
private class MyEnumerator : IEnumerator
{
public Car[] Carlist;
int position = -1;
public MyEnumerator(Car[] list)
{
Carlist = list;
}
public bool MoveNext()
{
position++;
return (position < Carlist.Length);
}
public void Reset()
{
position = -1;
}
public object Current
{
get
{
try
{
return Carlist[position];
}
catch (IndexOutOfRangeException)
{
throw new InvalidOperationException();
}
}
}
}
}
使用姿势:
public static void Main()
{
var cars = new Cars();
foreach (Car item in cars)
{
Console.WriteLine($"{item.Brand}:{item.Age}");
}
}
---
Ford:1992
Fiat:1988
Buick:1932
Ford:1932
Dodge:1999
Honda:1977
4. foreach编译实质
在foreach cars
的时候,被编译器翻译成如下代码:
List<Car>.Enumerator e = cars.GetEnumerator();
try
{
while (e.MoveNext())
{
int v = e.Current;
Console.WriteLine(v); // «embedded_statement» 要重复执行的语句。
}
}
finally
{
IDisposable d = e as IDisposable;
if (d != null) d.Dispose();
}
这里面要注意:
(1) v是在循环体内定义,是妥妥的块内部变量。 https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#1295-the-foreach-statement
2023/08/17 补充
(2) 变量e
已经脱离变量cars
, 故即使此时cars
被重新指向,也不会影响foreach迭代。
但是若此时修改变量cars
, 则会直接引用内部的Carlist数组
产生变动,foreach会爆异常。
本文来自博客园,作者:{有态度的马甲},转载请注明原文链接:https://www.cnblogs.com/JulianHuang/p/14271285.html
欢迎关注我的原创技术、职场公众号, 加好友谈天说地,一起进化