C# IEnumerator,IEnumerable ,Iterator

IEnumerator 枚举器接口

在C#语言中,大部分以"I"字母开头命名的都是接口,所以情理之中,IEnumerator也是一个接口。

对于面向对象语言来说,接口就是一份"协议",它定义了一组方法、属性和事件的契约,任何类、结构体或枚举只要符合这个契约,就可以被认为实现了该接口,可以被贴上一个标签,标签上写着这个东西是实现了XX功能的

IEnumerator 是所有非泛型枚举器的基接口。其泛型等效项是 System.Collections.Generic.IEnumerator<T> 接口。其继承了IEnumerator,屏蔽(mask)了基类的Current成员,迭代器返回的值就是泛型类型

看起来IEnumerator这个协议的要求并不复杂,仅仅需要实现三件事:Current属性、MoveNext方法、Reset方法,但是事实上这三部分已经让其具备了一个基本功能:迭代。

我们可以通过Current获取到当前的内容,并通过MoveNext移动到下一个内容的位置,然后继续通过Current获取到当前的内容,最后通过Reset重置。这也是迭代器设计模式的常规思路,对外我们可以不暴露这个迭代的具体过程而只是不停地返回迭代的结果。

在MS官方文档中也给出了自行实现枚举器的示例(注意这不是迭代器,因为方法体中没有yield关键字),这样就可以循环访问(通过枚举器)自定义的集合

cs 复制代码
using System;
using System.Collections;

// Simple business object.
public class Person
{
    public Person(string fName, string lName)
    {
        this.firstName = fName;
        this.lastName = lName;
    }

    public string firstName;
    public string lastName;
}

// Collection of Person objects. This class
// implements IEnumerable so that it can be used
// with ForEach syntax.
public class People : IEnumerable
{
    private Person[] _people;
    public People(Person[] pArray)
    {
        _people = new Person[pArray.Length];

        for (int i = 0; i < pArray.Length; i++)
        {
            _people[i] = pArray[i];
        }
    }

// Implementation for the GetEnumerator method.
    IEnumerator IEnumerable.GetEnumerator()
    {
       return (IEnumerator) GetEnumerator();
    }

    public PeopleEnum GetEnumerator()
    {
        return new PeopleEnum(_people);
    }
}

// When you implement IEnumerable, you must also implement IEnumerator.
public class PeopleEnum : IEnumerator
{
    public Person[] _people;

    // Enumerators are positioned before the first element
    // until the first MoveNext() call.
    int position = -1;

    public PeopleEnum(Person[] list)
    {
        _people = list;
    }

    public bool MoveNext()
    {
        position++;
        return (position < _people.Length);
    }

    public void Reset()
    {
        position = -1;
    }

    object IEnumerator.Current
    {
        get
        {
            return Current;
        }
    }

    public Person Current
    {
        get
        {
            try
            {
                return _people[position];
            }
            catch (IndexOutOfRangeException)
            {
                throw new InvalidOperationException();
            }
        }
    }
}

class App
{
    static void Main()
    {
        Person[] peopleArray = new Person[3]
        {
            new Person("John", "Smith"),
            new Person("Jim", "Johnson"),
            new Person("Sue", "Rabon"),
        };

        People peopleList = new People(peopleArray);
        foreach (Person p in peopleList)
            Console.WriteLine(p.firstName + " " + p.lastName);
    }
}

使用枚举器的例子

foreach背后的原理

foreach语句中in右侧的集合的类型一定要实现IEnumarable接口。因为该语句会调用Ienumerable接口的其中唯一的GetIEnumarator函数,获取该类型的迭代器(迭代器并不唯一),利用迭代器movenext,current函数对集合进行遍历。

IEnumerable 可枚举接口

只要实现GetEnumerator方法的类型就是可枚举类型,反过来也是对的,即如果类中有GetEnumerator方法,也可以不实现IEnumerable接口就可以使用IEnumerator。

Iterator 迭代器

诞生原因:

"懒"又成为了动力,为了提供更简单的创建枚举器和可枚举方类型的方式,我们通过迭代器让编译器自动创建他们,这样就不用我们手动编码可枚举类型和枚举器了。这也是你能发现迭代器函数(包含迭代器块的函数)为什么返回一个IEnumerable<>/IEnumerator的原因,通过直接返回的枚举器或通过可枚举类型再手动调用其GetEnumerator方法获取的枚举器,再结合yield背后的状态机,我们能实现迭代的功能

迭代器块

首先了解一个概念:迭代器块(忽略访问器主体和运算符主体)看到方法主体内有至少一个yield关键字,那该方法主体就可以被称为迭代器块 。迭代器块与其他代码块不同,其他块包含的语句是被当作命令式的 ,即按顺序执行并最终离开代码块。迭代器块不需要在同一时间执行,他是声明性的描述了编译器为我们隐式创建枚举器类的行为这也是为什么迭代器块一定方法一定需要返回一个定制的枚举器(也可以返回可枚举类型,通过可枚举类型的GetEnumerator方法获取)

本质

迭代器不是一个类、接口,而是一种设计模式,迭代器允许我们自定义一个枚举器的行为(迭代器中的代码描述了如何枚举元素),编译器得到有关如何枚举项的描述后,使用它来构建包含所有需要的方法和属性实现的枚举器,产生的类被嵌套包含声明迭代器的类中

以下使用迭代器创建枚举器(左),使用迭代器创建可枚举类型(右)

下图为根据我们的描述隐式生成一个实现IEnumerable的类

由此上结论:枚举器 + 状态机 = 迭代器,下图为迭代器的状态机




字面意思理解yield的含义是让出,先让一让但不完全走掉,不像return,直接退出去

yield 语句

yield 语句有以下两种形式:yield return:在迭代中提供下一个值,yield break:显式示迭代结束

cs 复制代码
Console.WriteLine(string.Join(" ", TakeWhilePositive(new int[] {2, 3, 4, 5, -1, 3, 4})));
// Output: 2 3 4 5

Console.WriteLine(string.Join(" ", TakeWhilePositive(new int[] {9, 8, 7})));
// Output: 9 8 7

IEnumerable<int> TakeWhilePositive(IEnumerable<int> numbers)
{
    foreach (int n in numbers)
    {
        if (n > 0)
        {
            yield return n;
        }
        else
        {
            yield break;
        }
    }
}

实例分析


foreach没有立刻执行代码块中的指令,而是跳转到函数语句中,但是这个函数在一开始就执行一遍了,这次不是在执行函数,而是执行函数内隐式声明了的枚举器函数

如前面的示例所示,当开始对迭代器的结果进行迭代时,在隐式获取的枚举器函数的movenext返回值为true后,函数迭代器会一直执行,直到到达第一个 yield return 语句为止。 迭代器的执行会暂停并记录退出的位置,调用方会获得第一个迭代值()并处理该值。 在后续的每次迭代中(即再次调用movenext函数并返回true时)迭代器的执行都会在导致上一次挂起的 yield return 语句之后恢复,并继续执行(打印了Iterator:yield) ,直到到达下一个 yield return 语句为止。 当控件到达迭代器或 yield break 语句的末尾时,迭代完成。

yield的优点


Reference

IEnumerable Interface (System.Collections) | Microsoft Learn

漫画秒懂 Unity 的协程与迭代器(Coroutine 与 Enumerator) - 知乎

yield 语句 - 在迭代器中提供下一个元素 - C# reference | Microsoft Learn

相关推荐
Prejudices3 分钟前
C++如何调用Python脚本
开发语言·c++·python
我狠狠地刷刷刷刷刷16 分钟前
中文分词模拟器
开发语言·python·算法
wyh要好好学习19 分钟前
C# WPF 记录DataGrid的表头顺序,下次打开界面时应用到表格中
开发语言·c#·wpf
AitTech20 分钟前
C#实现:电脑系统信息的全面获取与监控
开发语言·c#
qing_04060322 分钟前
C++——多态
开发语言·c++·多态
孙同学_22 分钟前
【C++】—掌握STL vector 类:“Vector简介:动态数组的高效应用”
开发语言·c++
froginwe1123 分钟前
XML 编辑器:功能、选择与使用技巧
开发语言
Jam-Young29 分钟前
Python的装饰器
开发语言·python
小辛学西嘎嘎1 小时前
C/C++精品项目之图床共享云存储(3):网络缓冲区类和main
c语言·开发语言·c++
吖秧吖1 小时前
three.js 杂记
开发语言·前端·javascript