2021年了,`IEnumerator`、`IEnumerable`还傻傻分不清楚?

  • IEnumerator接口
  • IEnumerable接口
  • 最佳实践
  • foreach 实质、 微软官方信源
  • 20230817补充

IEnumeratorIEnumerable这两个接口单词相近、含义相关,傻傻分不清楚。
入行多年,一直没有系统性梳理这对李逵李鬼。

最近本人在怼着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会爆异常。

posted @   码甲哥不卷  阅读(1190)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示

目录导航