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 @ 2021-01-13 11:59  博客猿马甲哥  阅读(1174)  评论(0编辑  收藏  举报