65.9K
CodeProject 正在变化。 阅读更多。
Home

选择你的枚举器和我。理解Yield和IEnumerable (C#)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.63/5 (15投票s)

2013年4月8日

CPOL

18分钟阅读

viewsIcon

64528

downloadIcon

466

使用多个枚举器并通过Yield或IEnumerator实现IEnumerable。

Screenshot showing an enumerated DataGridView

引言

我正在开发一个未完成的线性代数(矩阵)类,并希望提供横向(按行)或纵向(按列)枚举它的能力。因此,我尝试实现 IEnumerable 接口,这需要实现一个名为 GetEnumerator() 的方法。然后事情就可能变得有点混乱。

  • 创建一个实现 IEnumerator 的第二个类,并从 GetEnumerator() 方法返回它的一个新实例,这并不难。
    • 创建多个(纵向和横向)也并不更困难,使用一个属性在它们之间进行选择也很直接。
  • 但是 yield 关键字呢,它究竟该如何使用?它是否“更好”?我应该在矩阵类中使用它还是在 IEnumerator 类中使用它?
  • 你能使用 yield 的同时,仍然有多种枚举方式吗?

我倾向于发现关于这个主题(以及许多其他主题)的文章开头都足够简单,然后变得比我希望的更复杂/理论化/详细。我更喜欢基本的、实用的建议和指导,加上少量的理论和解释,我可以一点一点地学习,而不是一次性全部学会。我不是谢尔顿(Sheldon)。事实上,直到我写完附带的演示应用,我才完全确定,然后我才(在实践基础上)理解了它。所以,我们将看看如何通过创建 IEnumerator 类或使用 yield 来实现 IEnumerable ——并且我们将比较这两种方法。我还会提到至少另一种使类可枚举的方法(无需显式实现 IEnumerable)。我们还将看到两种允许类的用户从 n 种不同的枚举方式中进行选择的方法(只要你实现了那么多),一种使用 yield,另一种使用 IEnumerator

如果你只是想了解(像我一样)yield 是否是创建 IEnumerator 类的替代方案(而不是例如一种补充),那么答案是:是的;尽管它们之间存在差异,这些差异在许多情况下可能并非微不足道,我们将简要地看一下这一点。

什么是 Yield?

如果你来到这里是想弄清楚 yield 是什么/做什么的,那么这是我的看法。

当你使用带有 yield迭代器(Iterator)方法时,它不会执行到其自然结束。每当它遇到一个 yield,它会像 return 语句一样返回一个值,然后就停在当前位置,方法并没有结束。下次你调用该方法时,它会从上次离开的地方继续执行,也就是说,它不会每次调用都重新开始。所以在 foreach 循环中,这些方法从头到尾只会运行一次,而不是对集合、数组或其他数据分组中的每个值都运行一次。这是一个可以多次返回值而不是一次性返回的方法——很神奇(或许不神奇,只是编译器为我们节省了时间,因为在幕后(^),它显然还是创建了类似 IEnumerator 的代码...)

谁适合使用?

  1. 初学者。
  2. 任何之前没有(通过 IEnumeratoryield)实现过 IEnumerable 并想要一个例子的人。
  3. 任何想要为一个 IEnumerable 类实现多个枚举器的人。
  4. 任何还没能理解外面其他上百万种解释的人;这只是我对那个集合的补充,我希望你觉得它比你目前读过的其他解释更容易消化。如果不是,也没关系,你还有999,999个其他选择!

快速摘要

  1. 我们可以使用 yield 作为创建 IEnumerator 类的替代方案,同时实现 IEnumerable(特别是 GetEnumerator() 方法)。
  2. 在某些情况下,特别是在中等到高复杂度的情况下,使用 yield 编写和理解/阅读代码可能比 IEnumerator 类容易几个数量级。
  3. 一个 IEnumerable 类可以有多个枚举器,并且可以在运行时由用户选择。
  4. 你可以让一个类可枚举而无需声明它为 IEnumerable,只需实现一个返回类型为 IEnumerable 的方法。
    1. 实现多个返回类型为 IEnumerable 的方法与第3点效果相同。
  5. 当你使用 yield 时,编译器会创建一个 IEnumerator 类,所以在某些方面它并没有什么不同,但你写的代码可能会更容易阅读(代码的效率是另一个问题),请参见最后一节。

警告1:如果你喜欢设计模式和原则,那么即使在这么短的代码中,我毫无疑问也破坏了很多。这就是生活!

警告2:如果在语义上缺乏细节,我向你道歉。我是从实践的角度来写的,有时为了保持简单、实用和可读性,我会有意(或无意地)跳过细节。因此,两个人对缺失细节的解读可能会有所不同。如果你需要或想要完全正确地理解这些细节,外面有大量此类信息,我不会试图在这里复制它,那不是我的意图。如果有明显的书面错误,我非常乐意纠正它们。

目录

背景

这篇文章的起源是(现在也是)一个未完成的线性代数(矩阵)项目。我希望能够按列(纵向)或按行(横向)遍历矩阵,这意味着需要实现 IEnumerable。好的,那么我们该怎么做呢?嗯,我们从经典的谷歌搜索开始。经过大量有点令人沮丧的时间后,我有了一个好主意,我可以使用 yield 关键字,或者我可以创建一个实现 IEnumerator 的类,或者也许我必须两者都做,或者可以两者都做,或者两者都不必做,或者必须做些完全不同的事情;谁知道呢?

最终,你可以使用 yield 或创建一个实现 IEnumerator 的类,由你选择。可能每种方法都有其更适合的场景,我还不够了解,无法断言孰优孰劣。至于我的矩阵类,我还可以实现2个枚举器(如果我真的想,可以实现10个),每个都做不同的事情,并且可以在运行时进行切换。

枚举的基础知识

实现 IEnumerable 的“经典”(如果你喜欢新潮的东西,也可以叫“老旧”)方法相当直接,所以我先用文字描述,然后再附上代码。

IEnumerable 接口只要求你实现一个 GetEnumerator() 方法,该方法返回一个实现了 IEnumerator 的对象。然后 IEnumerator 要求你实现几个方法,其中两个关键的是 MoveNext() 方法和 Current 属性。

所以,想象一下你对一个实现了 IEnumerable 的对象使用 foreach 循环,就像这样:

foreach (double element in EnumerableMatrix)
{
    Console.WriteLine(element.ToString());
}

实际上发生的是,EnumerableMatrix 对象的 GetEnumerator() 方法被调用。这会返回一个新对象,我们知道它必须实现了 IEnumerator。然后,这个 IEnumerator 对象可以通过使用 MoveNext() 方法和 IEnumeratorCurrent 属性进行循环。实际上,上面的代码是下面这种写法的简写:

IEnumerator ObjectToEnumerate = EnumerableMatrix.GetEnumerator()
while (while ObjectToEnumerate.MoveNext() == True)
{
   Console.WriteLine(ObjectToEnumerate.Current.ToString());
}

你可以手动这样做,以便比 foreach 提供更多的枚举控制。在 兴趣点 部分,我们快速看了一下IL代码,我们可以直接看到 foreach 被转换成了如上所示的 MoveNext()Current

要理解枚举,我们必须先数到十

实现 IEnumerable

那么,假设我们理解了 foreach 循环实际上做了什么,那么理解“旧”的或经典的方式来使一个对象可枚举就变得更容易了。我们从我们想要枚举的类开始。

public class ByEnumerator : IEnumerable<double>
{
    Public IEnumerator<Double> GetEnumerator()
    {
        Return New MyNewEnumerator()
    }
 
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}  

它真的就这么简单。IEnumerable 仅仅要求实现这两个方法,你只需将那个不太具体的方法指向另一个即可。

在我的例子中,我正在枚举一个二维的 double 数组,并希望能够选择两种不同的 IEnumerator。所以我们需要做一些改变。我们已经做了以下事情:

添加了一个构造函数,它只接受一个现有的二维 double 数组。

public ByEnumerator(double[,] matrix)
{
    this._matrix = matrix;
    this._matrixEnumerator = MatrixEnumerator.Horizontal;
}

一个属性,允许在不同的枚举方法之间进行选择,以及一个表示不同可能枚举器的 enum

public MatrixEnumerator Enumerator
{ 
    get { return this._matrixEnumerator; }
    set { this._matrixEnumerator = value; }
}
 
public enum MatrixEnumerator
{
    Vertical,
    Horizontal
}

最后是一些条件代码,用来创建所需的 IEnumerator 实例。

public IEnumerator<double> GetEnumerator()
{
    switch (this._matrixEnumerator)
    {
        case MatrixEnumerator.Vertical:
            return new VerticalMatrixEnumerator(this._matrix);
        case MatrixEnumerator.Horizontal:
            return new HorizontalMatrixEnumerator(this._matrix);
        default:
            throw new InvalidOperationException();
    }
}
 
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
    return this.GetEnumerator();
}

现在我们只需要实现那两个新类,即 Horizontal 和 Vertical 枚举器。

实现 IEnumerator

你可以看到,我们返回的 IEnumerator 取决于类的属性 Enumerator。现在我们只需要创建上面代码中提到的两个 IEnumerator 类。我们先展示 Horizontal(水平)枚举器,你会发现我们只是为前面提到的四个方法添加了代码。有一些私有字段来跟踪我们在数组中的位置,除此之外,重要的方法是 MoveNext()。你还可以看到,我们把 IEnumerable 类中的二维数组的副本传递给了 IEnumerator 类(我猜这就是为什么在枚举对象时修改它通常会导致混乱的原因)。

首先是私有字段和构造函数。

public class HorizontalMatrixEnumerator : IEnumerator<double>
{
    private double[,] _matrix;
    private int _colIndex;
    private int _rowIndex;
    private double _curItem;
    private int _lastCol;
    private int _lastRow;
 
    public HorizontalMatrixEnumerator(double[,] matrix)
    {
        this._matrix = matrix;
        this._colIndex = -1;
        this._rowIndex = 0;
        this._curItem = double.NaN;
        this._lastCol = matrix.GetUpperBound(1);
        this._lastRow = matrix.GetUpperBound(0);
    }
  • _matrix 字段很明显,它是 IEnumerator 对象发送过来的二维 double 数组的副本(我特意避免讨论浅拷贝/深拷贝和值/引用 - 保持焦点)。
  • _colIndex_rowIndex 字段用于跟踪我们在数组中的位置。我们稍后会解释为什么它们被初始化为 0-1
  • _curItemCurrent 属性背后的字段。
  • _lastCol_lastRow 只是为了方便地访问数组的 UpperBounds

现在是 Current 属性。

    public double Current
    {
        get
        {
            if (double.IsNaN(this._curItem))
            {
                throw new InvalidOperationException();
            }
            return this._curItem;
        }
    }
 
    object System.Collections.IEnumerator.Current
    {
        get { return this.Current(); }
    }

如前所述,它只是 _curItem 字段的一个包装器。如果它是 NaN,它会抛出一个异常——它在构造函数中被初始化为这样——这确保了如果对象的用户(例如 foreach 循环)在调用 MoveNext() 之前尝试使用它,我们会抛出一个异常,这比让他们在代码因为期望一个有用的值却得到了它不知道如何处理的东西而崩溃时才发现要好一些。

现在是有趣的部分,MoveNext()

    public bool MoveNext()
    {
    if (this._colIndex == this._lastCol & this._rowIndex == this._lastRow)
        {
            return false;
        }
        if (this._colIndex == this._lastCol)
        {
            this._colIndex = 0;
            this._rowIndex += 1;
        }
        else
        {
            this._colIndex += 1;
        }
        this._curItem = this._matrix[this._rowIndex, this._colIndex];
        return true;
    }
  • 首先,我们检查是否已经到达数组的末尾,如果是,那么我们已经完成了,并返回 false,从而让之前的 while {true} do ... 循环优雅地退出。
  • 然后我们检查是否在某一行的最后一列,如果是,我们将列索引重置为 0 并移动到下一行。
  • 如果我们不在一行的末尾,那么我们只需移动到该行的下一列。
  • 在后两种情况下,我们返回 true,告诉 while {true} do ... 循环至少还有一个新值可以循环。
  • 现在是我之前承诺的解释,为什么我们将 _rowIndex 初始化为 0_colIndex 初始化为 -1?嗯,MoveNext() 会在 Current 之前被调用,所以如果我们都设置为 0,那么上面的代码会立即将我们移动到第一行的第二列(0,1),我们就永远不会将 Current 的值设置为数组在 (0,0) 的值。

值得注意的是,上面展示的 MoveNext() 方法本质上是一对被不必要地复杂化了的嵌套 for 循环,yield 版本(我们稍后会展示)更容易编写,也更容易理解。下面的图片应该能清楚地说明这一点。

Traversing the array horizontally

你可以看到,每当列索引达到 2 时,我们需要将其重置为 0 并将行索引加 1,或者只是将列索引加 1 直到它达到 2

最后,Reset() 方法只是将相关字段恢复到对象创建时的状态。

    public void Reset()
    {
        this._colIndex = -1;
        this._rowIndex = 0;
        this._curItem = double.NaN;
    }
} // Class

Vertical(垂直)枚举器改变了一些东西,但主要是 MoveNext() 方法,如下所示。我们只是在 if () ... else ... 部分翻转了行和列的变量,这样我们就是向下移动列而不是横向移动行。我们还将 _rowIndex 初始化为 -1(而不是 0),将 _colIndex 初始化为 0(而不是 -1)。

    public bool MoveNext()
    {
        if (this._colIndex == this._lastCol & this._rowIndex == this._lastRow)
        {
            return false;
        }
        if (this._rowIndex == this._lastRow)
        {
            this._rowIndex = 0;
            this._colIndex += 1;
        }
        else
        {
            this._rowIndex += 1;
        }
        this._curItem = this._matrix[this._rowIndex, this._colIndex];
        return true;
    }

虽然这很简单,但这意味着要创建两个额外的类和(相对)更多的代码,对于一件相对简单的事情来说,感觉有点多余。我们能做得更好吗?让我们来试试yield吧...

谁需要 IEnumerator?

向诱惑屈服吧。它可能不会再从你身边经过。

那么我们如何用 yield 做同样的事情呢?大部分代码都不变。首先,GetEnumerator() 方法不再创建新对象,而是调用几个私有的类方法。除此之外,它完全相同,你必须仔细观察才能注意到有什么变化。

public IEnumerator<double> GetEnumerator()
{
    switch (this._matrixEnumerator)
    {
        case MatrixEnumerator.Horizontal:
            return this.HorizontalEnumerator();
        case MatrixEnumerator.Vertical:
            return this.VerticalEnumerator();
        default:
            throw new InvalidOperationException();
    }
}

本质上是相同的,但不是 return new HorizontalMatrixEnumerator(this._matrix),我们看到的是 return this.HorizontalEnumerator(),这是一个方法引用而不是一个对象。然后我们只需要实现那两个新方法。

private IEnumerator<double> VerticalEnumerator()
{
    if (this._matrix != null)
    {
        for (int col = 0; col <= this._matrix.GetUpperBound(1); col++)
        {
            for (int row = 0; row <= this._matrix.GetUpperBound(0); row++)
            {
                yield return this._matrix[row, col];
            }
        }
    } else {
          throw new InvalidOperationException();
    }
}
 
private IEnumerator<double> HorizontalEnumerator()
{
    if (this._matrix != null)
    {
        for (int row = 0; row <= this._matrix.GetUpperBound(0); row++)
        {
            for (int col = 0; col <= this._matrix.GetUpperBound(1); col++)
            {
                yield return this._matrix[row, col];
            }
        }
    } else {
        throw new InvalidOperationException();
    }
}

我花了一段时间才弄清楚到底该如何声明这两个方法,但最终你会发现它们的返回类型需要是 IEnumerator。你可以在代码中看到这一点,除此之外,它只需要用循环遍历数组即可。从技术上讲,这并不容易多少,实际上一点也不容易,但代码量肯定更少,而且没有那些额外的类需要维护。

但非常清楚的是,它比两个 IEnumerator 类的代码少得多。而且也更容易看清发生了什么,它显然是两个嵌套的 for 循环,正如我们上面所说,这正是 MoveNext() 方法实际上在做的事情,只是方式更复杂。

作为解释,这真的不算多,但如果你已经阅读并理解了如何用 IEnumerator 做同样的事情,那么理解 yield 就变得像这样简单:

  1. 认识到代码看起来与 MoveNext() 方法(本质上)非常相似,只是更简单。
  2. 意识到当你使用带有 yield迭代器(Iterator)方法时,它不会执行到其自然结束。每当它遇到一个 yield,它会像 return 语句一样返回一个值,然后就停在当前位置,方法并没有结束。下次你调用该方法时,它会从上次离开的地方继续执行,也就是说,它不会每次调用都重新开始。所以在 foreach 循环中,这个方法从头到尾只会运行一次,而不是对数组中的每个值都运行一次。这是一个可以多次返回值而不是一次性返回的方法——很神奇(或许不神奇,只是编译器为我们节省了时间,因为在幕后(^),它显然还是创建了类似 IEnumerator 的代码...)

两者唯一的区别在于你遍历数组的方式,是先列后行,还是先行后列。

状态机

如果这个解释对你不起作用,外面还有很多用不同方式和更详细地解释它的选项,Google(^)是你的好朋友。

好的,我不会试图非常详细地解释这个(这篇文章是针对初学者的,而且还有很多我还不懂或者至少我想象中不懂的东西),只是(我希望)足够让你理解 yield 实际上做了什么以及如何做的。你几乎可以把迭代器(Iterator)方法看作一个独立的程序,运行 foreach 循环的程序向你的 IEnumerable 对象发送消息,说“给我下一个”——现在这听起来完全像是一个 MoveNext() 后面跟着一个 Current——但巧妙之处在于,另一个程序一旦执行了 yield 语句就会停止执行,然后等待你请求下一个,此时它执行下一条语句并继续,直到到达下一个 yield 语句,执行它,然后再次等待。在上面的例子中,每次它到达“下一个” yield 语句时,实际上是同一个,只是在循环中。你同样可以有一组一个接一个的 yield 语句(参见 MSDN 示例(^))。

关注点

你学到了什么有趣/好玩/烦人的东西吗?

是的,某个蠢驴(在设计 .Net 和 VB / C# 时)决定将数组索引设计为(行,列)而 DataGridViews 设计为(列,行),这真是个“聪明”的决定。

你做了什么特别聪明、狂野或古怪的事情吗?

狂野或古怪?这是 CP 文章模板建议的。真的吗?在编程时?狂野和古怪。也许如果我喝了太多咖啡,做了一些与编程完全无关的疯狂事情,但除此之外,没有。

foreach == while (MoveNext()) + Current

我们可以清楚地展示之前提到的观点,即对一个 IEnumerable 对象运行 foreach 与手动创建 IEnumerator 并启动一个 while (true) do ... 循环是相同的。这恰好是针对使用 IEnumerator 而非 yield 的对象执行 foreach 的代码,尽管对于 yield 版本来说也非常相似。

IL_016d: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<float64> IEnumerableCS.ByEnumerator::GetEnumerator()
IL_0172: stloc.s CS$5$0001
.try
{
    IL_0174: br.s IL_01a1
    // loop start (head: IL_01a1)
        IL_0176: ldloc.s CS$5$0001
        IL_0178: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<float64>::get_Current()
        IL_017d: conv.r8
        IL_017e: stloc.s Val
        IL_0180: nop
        IL_0181: ldloc.s output
        IL_0183: ldloc.s Val
        IL_0185: ldc.i4.2
        IL_0186: call float64 [mscorlib]System.Math::Round(float64, int32)
        IL_018b: stloc.s CS$0$0002
        IL_018d: ldloca.s CS$0$0002
        IL_018f: call instance string [mscorlib]System.Double::ToString()
        IL_0194: ldstr " ¦ "
        IL_0199: call string [mscorlib]System.String::Concat(string, string, string)
        IL_019e: stloc.s output
        IL_01a0: nop

        IL_01a1: ldloc.s CS$5$0001
        IL_01a3: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
        IL_01a8: stloc.s CS$4$0000
        IL_01aa: ldloc.s CS$4$0000
        IL_01ac: brtrue.s IL_0176
    // end loop

    IL_01ae: leave.s IL_01c4
} // end .try
finally
{
    IL_01b0: ldloc.s CS$5$0001
    IL_01b2: ldnull
    IL_01b3: ceq
    IL_01b5: stloc.s CS$4$0000
    IL_01b7: ldloc.s CS$4$0000
    IL_01b9: brtrue.s IL_01c3

    IL_01bb: ldloc.s CS$5$0001
    IL_01bd: callvirt instance void [mscorlib]System.IDisposable::Dispose()
    IL_01c2: nop

    IL_01c3: endfinally
// end handler

你可以清楚地看到,在 IL_0174 处,它(无条件地)跳转到包含调用 MoveNext() 的短代码段,从 IL_01a1 开始。在该代码段的末尾,它根据 true (IL_01b9) 分支到上面稍长的代码段(假设 MoveNext() 返回了 true),从 IL_0176 开始,然后调用 Current。这完全与 while (true) do ... 循环一致,也就是说,如果待枚举的对象是空的,它永远不会尝试调用 Current;同时,对于非空对象,MoveNext() 第一次返回 false 是在它试图移动到最后一个位置之后,此时,基于 true 的分支 (IL_01b9) 不会发生,代码会继续向下执行,跳出循环。

编译器如何实现 Yield?

我记得我读到过,当你使用 yield 时,编译器会通过创建一个 IEnumerator 来响应,所以它实际上只是为了让代码更容易读、写和维护的简写。我想看看这是不是真的——因为我有一个完美的例子来测试它——通过查看编译器产生了什么样的 IL 代码。(可以试试 ILSpy(^) 或者 Windows 7.1 SDK(^) 自带的 ILDisassembler。)这是你将看到的,首先我们来看看生成的类和代码,特别是 ByYield 类。

The ByYield structure by ILSpy

我们的 HorizontalEnumerator 方法被高亮显示了,那么 IL 代码说了什么呢?

.method private hidebysig 
	instance class [mscorlib]System.Collections.Generic.IEnumerator`1<float64> HorizontalEnumerator () cil managed 
{
	// Method begins at RVA 0x22ec
	// Code size 16 (0x10)
	.maxstack 2
	.locals init (
		[0] class IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'
	)

	IL_0000: ldc.i4.0
	IL_0001: newobj instance void IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::.ctor(int32)
	IL_0006: stloc.0
	IL_0007: ldloc.0
	IL_0008: ldarg.0
	IL_0009: stfld class IEnumerableCS.ByYield IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>4__this'
	IL_000e: ldloc.0
	IL_000f: ret
} // end of method ByYield::HorizontalEnumerator

我没完全看懂,但我确实看到了对 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4 的引用。我们可以在上面的图片中看到它——它是 ByYield 描述中的第三项,就在 DerivedTypes 下面。以下是该类成员中包含的内容。

The ByYield structure by ILSpy

这是我们的老朋友,两个方法 MoveNext()Reset(),以及 Current 属性;这些方法并没有在 ByYield 类中编写,是编译器创建了它们。很明显,编译器将 yield 视为“请为我编写一个 IEnumerator”的简写。

我们可以以非常业余的水平比较一下,编译器的 IEnumerator 版本是否比它优化我们上面写的 MoveNext() 的尝试要好得多。左边是编译器将两个嵌套的 for 循环和一个 yield 转换后的结果。右边是它对我写的 MoveNext() 方法的转换结果。

.method private final hidebysig newslot virtual 
	instance bool MoveNext () cil managed 
{
	.override method instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
	// Method begins at RVA 0x225c
	// Code size 248 (0xf8)
	.maxstack 4
	.locals init (
		[0] bool CS$1$0000,
		[1] int32 CS$4$0001,
		[2] bool CS$4$0002
	)

	IL_0000: ldarg.0
	IL_0001: ldfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>1__state'
	IL_0006: stloc.1
	IL_0007: ldloc.1
	IL_0008: switch (IL_0019, IL_0017)

	IL_0015: br.s IL_001b

	IL_0017: br.s IL_007f

	IL_0019: br.s IL_0020

	IL_001b: br IL_00f2

	IL_0020: ldarg.0
	IL_0021: ldc.i4.m1
	IL_0022: stfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>1__state'
	IL_0027: nop
	IL_0028: ldarg.0
	IL_0029: ldfld class IEnumerableCS.ByYield IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>4__this'
	IL_002e: ldfld float64[0..., 0...] IEnumerableCS.ByYield::_matrix
	IL_0033: ldnull
	IL_0034: ceq
	IL_0036: stloc.2
	IL_0037: ldloc.2
	IL_0038: brtrue IL_00ea

	IL_003d: nop
	IL_003e: ldarg.0
	IL_003f: ldc.i4.0
	IL_0040: stfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<row>5__5'
	IL_0045: br.s IL_00c4

	IL_0047: nop
	IL_0048: ldarg.0
	IL_0049: ldc.i4.0
	IL_004a: stfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<col>5__6'
	IL_004f: br.s IL_0095

	IL_0051: nop
	IL_0052: ldarg.0
	IL_0053: ldarg.0
	IL_0054: ldfld class IEnumerableCS.ByYield IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>4__this'
	IL_0059: ldfld float64[0..., 0...] IEnumerableCS.ByYield::_matrix
	IL_005e: ldarg.0
	IL_005f: ldfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<row>5__5'
	IL_0064: ldarg.0
	IL_0065: ldfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<col>5__6'
	IL_006a: call instance float64 float64[0..., 0...]::Get(int32, int32)
	IL_006f: stfld float64 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>2__current'
	IL_0074: ldarg.0
	IL_0075: ldc.i4.1
	IL_0076: stfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>1__state'
	IL_007b: ldc.i4.1
	IL_007c: stloc.0
	IL_007d: br.s IL_00f6

	IL_007f: ldarg.0
	IL_0080: ldc.i4.m1
	IL_0081: stfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>1__state'
	IL_0086: nop
	IL_0087: ldarg.0
	IL_0088: dup
	IL_0089: ldfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<col>5__6'
	IL_008e: ldc.i4.1
	IL_008f: add
	IL_0090: stfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<col>5__6'

	IL_0095: ldarg.0
	IL_0096: ldfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<col>5__6'
	IL_009b: ldarg.0
	IL_009c: ldfld class IEnumerableCS.ByYield IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>4__this'
	IL_00a1: ldfld float64[0..., 0...] IEnumerableCS.ByYield::_matrix
	IL_00a6: ldc.i4.1
	IL_00a7: callvirt instance int32 [mscorlib]System.Array::GetUpperBound(int32)
	IL_00ac: cgt
	IL_00ae: ldc.i4.0
	IL_00af: ceq
	IL_00b1: stloc.2
	IL_00b2: ldloc.2
	IL_00b3: brtrue.s IL_0051

	IL_00b5: nop
	IL_00b6: ldarg.0
	IL_00b7: dup
	IL_00b8: ldfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<row>5__5'
	IL_00bd: ldc.i4.1
	IL_00be: add
	IL_00bf: stfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<row>5__5'

	IL_00c4: ldarg.0
	IL_00c5: ldfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<row>5__5'
	IL_00ca: ldarg.0
	IL_00cb: ldfld class IEnumerableCS.ByYield IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>4__this'
	IL_00d0: ldfld float64[0..., 0...] IEnumerableCS.ByYield::_matrix
	IL_00d5: ldc.i4.0
	IL_00d6: callvirt instance int32 [mscorlib]System.Array::GetUpperBound(int32)
	IL_00db: cgt
	IL_00dd: ldc.i4.0
	IL_00de: ceq
	IL_00e0: stloc.2
	IL_00e1: ldloc.2
	IL_00e2: brtrue IL_0047

	IL_00e7: nop
	IL_00e8: br.s IL_00f1

	IL_00ea: nop
	IL_00eb: newobj instance void [mscorlib]System.InvalidOperationException::.ctor()
	IL_00f0: throw

	IL_00f1: nop

	IL_00f2: ldc.i4.0
	IL_00f3: stloc.0
	IL_00f4: br.s IL_00f6

	IL_00f6: ldloc.0
	IL_00f7: ret
} // end of method '<HorizontalEnumerator>d__4'::MoveNext
.method public final hidebysig newslot virtual 
	instance bool MoveNext () cil managed 
{
	// Method begins at RVA 0x251c
	// Code size 139 (0x8b)
	.maxstack 4
	.locals init (
		[0] bool CS$1$0000,
		[1] bool CS$4$0001
	)

	IL_0000: nop
	IL_0001: ldarg.0
	IL_0002: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_colIndex
	IL_0007: ldarg.0
	IL_0008: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_lastCol
	IL_000d: ceq
	IL_000f: ldarg.0
	IL_0010: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_rowIndex
	IL_0015: ldarg.0
	IL_0016: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_lastRow
	IL_001b: ceq
	IL_001d: and
	IL_001e: ldc.i4.0
	IL_001f: ceq
	IL_0021: stloc.1
	IL_0022: ldloc.1
	IL_0023: brtrue.s IL_002a

	IL_0025: nop
	IL_0026: ldc.i4.0
	IL_0027: stloc.0
	IL_0028: br.s IL_0089

	IL_002a: ldarg.0
	IL_002b: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_colIndex
	IL_0030: ldarg.0
	IL_0031: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_lastCol
	IL_0036: ceq
	IL_0038: ldc.i4.0
	IL_0039: ceq
	IL_003b: stloc.1
	IL_003c: ldloc.1
	IL_003d: brtrue.s IL_0058

	IL_003f: nop
	IL_0040: ldarg.0
	IL_0041: ldc.i4.0
	IL_0042: stfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_colIndex
	IL_0047: ldarg.0
	IL_0048: dup
	IL_0049: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_rowIndex
	IL_004e: ldc.i4.1
	IL_004f: add
	IL_0050: stfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_rowIndex
	IL_0055: nop
	IL_0056: br.s IL_0068

	IL_0058: nop
	IL_0059: ldarg.0
	IL_005a: dup
	IL_005b: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_colIndex
	IL_0060: ldc.i4.1
	IL_0061: add
	IL_0062: stfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_colIndex
	IL_0067: nop

	IL_0068: ldarg.0
	IL_0069: ldarg.0
	IL_006a: ldfld float64[0..., 0...] IEnumerableCS.HorizontalMatrixEnumerator::_matrix
	IL_006f: ldarg.0
	IL_0070: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_rowIndex
	IL_0075: ldarg.0
	IL_0076: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_colIndex
	IL_007b: call instance float64 float64[0..., 0...]::Get(int32, int32)
	IL_0080: stfld float64 IEnumerableCS.HorizontalMatrixEnumerator::_curItem
	IL_0085: ldc.i4.1
	IL_0086: stloc.0
	IL_0087: br.s IL_0089

	IL_0089: ldloc.0
	IL_008a: ret
} // end of method HorizontalMatrixEnumerator::MoveNext

我没有资格做出任何专业的评论,但尽管编译器在使用 yield 时为了将两个嵌套的 for 循环转换成一个 IEnumerator 显然要做比直接处理专用的 IEnumerator 代码更多的工作,但两者之间(看起来)并没有巨大的差异。它们中哪一个更高效我不知道——也许有人有时间测试一下……我猜这不应该令人惊讶,编译器能够做到这一点本身就让我印象深刻,特别是当你想象一些带有 yield 语句的方法可能有多复杂,而它必须能够处理所有这些情况。

如果你有一个可能包含大量数据、需要可枚举且时间至关重要的类,那么思考这个问题可能是值得的。在这种情况下,尝试你自己的 IEnumeratoryield 可能值得花时间,看看哪个执行得更快。

历史

版本 5 (2013年4月21日): 修复了测试 `var == double.NaN` 的问题,应为 `double.IsNaN(var)`;增加了关于 “foreach == while (MoveNext()) + Current” 的部分;修复了更多排版错误并为清晰起见重写了部分内容

版本 4 (2013年4月13日): [演示应用无代码更改] 重写了引言以改善流畅性,修复了一些排版错误

版本 3 (2013年4月12日): [演示应用无代码更改] 将 do while 改为 while do

版本 2 (2013年4月11日): [演示应用无代码更改] 根据读者评论进行澄清和补充信息,并增加了新章节 “编译器如何实现 Yield?

版本 1 (2013年4月7日): 全新出炉

© . All rights reserved.