yield return的工作模式是什么?

13

当我有一个代码块时

static void Main()
{

  foreach (int i in YieldDemo.SupplyIntegers())
  {
    Console.WriteLine("{0} is consumed by foreach iteration", i);
  }
}


 class YieldDemo
  {
    public static IEnumerable<int> SupplyIntegers()
     {
         yield return 1;
         yield return 2;
          yield return 3;
       }
   }

我可以理解yield return背后的原理为:

  1. Main()调用SupplyIntegers()
  2. |1| |2| |3| 存储在连续的内存块中。"IEnumerator"指针移动到 |1|。
  3. 控制权从SupplyInteger()返回到Main()。
  4. Main()输出该值
  5. 指针移动到|2|,以此类推。

澄清:

(1) 通常情况下,在一个函数中只允许有一个有效的return语句。当存在多个yield return,yield return,...语句时,C#会如何处理?

(2) 一旦遇到了return语句,控制权就不会再回到SupplyIntegers(),如果允许这样做,那么yield将再次从1开始吗?我的意思是yield return 1?


关于一本书的问题:《C#深入》(Manning,Skeet),第6章。这是免费样章,并涵盖迭代器块。它并不是一个初学者的C#书籍(远非如此),但您很难找到比这更好的参考资料。 - Marc Gravell
如果允许提问“你没有写过任何书吗?”的权限 - user193276
不,我没有。我为一家出版社进行一些校对工作,并且偶尔写一些文章等等。但是没有我的书。 - Marc Gravell
如果你能做这个,我会很高兴,因为我看过你对一些问题的解释。它适用于从初学者到老手的程序员。 - user193276
4个回答

35

不,远非如此;我将为您编写一个详细版本……但它太复杂了!


请注意,如果您理解foreach实际上是:

using(var iterator = YieldDemo.SupplyIntegers().GetEnumerator()) {
    int i;
    while(iterator.MoveNext()) {
        i = iterator.Current;
         Console.WriteLine("{0} is consumed by foreach iteration", i);
    }
}

using System;
using System.Collections;
using System.Collections.Generic;
static class Program
{
    static void Main()
    {

        foreach (int i in YieldDemo.SupplyIntegers())
        {
            Console.WriteLine("{0} is consumed by foreach iteration", i);
        }
    }
}

 class YieldDemo
  {

    public static IEnumerable<int> SupplyIntegers()
     {
         return new YieldEnumerable();
       }
    class YieldEnumerable : IEnumerable<int>
    {
        public IEnumerator<int> GetEnumerator()
        {
            return new YieldIterator();
        }
        IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
    }
    class YieldIterator : IEnumerator<int>
    {
        private int state = 0;
        private int value;
        public int Current { get { return value; } }
        object IEnumerator.Current { get { return Current; } }
        void IEnumerator.Reset() { throw new NotSupportedException(); }
        void IDisposable.Dispose() { }
        public bool MoveNext()
        {
            switch (state)
            {
                case 0: value = 1; state = 1;  return true;
                case 1: value = 2; state = 2;  return true;
                case 2: value = 3; state = 3; return true;
                default: return false;
            }
        }
    }
}

正如你所看到的,它在迭代器中建立了一个状态机,由 MoveNext 推进状态机。我使用了一个 state 字段的模式,因为你可以看到这对于更复杂的迭代器是如何工作的。

重要的是:

  • 在迭代器块中的任何变量都会成为状态机上的字段
  • 如果你有一个 finally 块(包括 using),它放在 Dispose()
  • 导致 yield return 的代码部分变成一个 case(大致上)
  • yield break 变成一个 state = -1; return false;(或类似的)

C# 编译器如何实现这一点非常复杂,但它使编写迭代器变得非常容易。


2
令人难以置信的是,在这个回答真正有帮助之前,人们已经开始点赞了。 - Joren
1
@Joren:这就是Marc的优秀之处。;) 很棒的回答,Marc! - jrista
1
很简单,当Marc说他会发布一些东西时,你可以非常确定他会做到。 - kemiller2002
大家好,请推荐一些适合初学者的 C# 书籍,以丰富我的知识。 - user193276
1
@Joren说得对。我也不会投票支持它。但是,如果我看到(例如)Jon有类似的“进行中”标记,我就不会花费很多精力来重复它;那就是我的意图。 - Marc Gravell
显示剩余4条评论

3

这只是一种语法糖,.net会为您生成IEnumerator类并实现MoveNext、Current和Reset方法,然后生成IEnumarable类GetEnumerator方法返回该IEnumerator,您可以通过.net反编译器或ildasm查看这些神奇的类。

另请参见此处


2
简单来说,迭代器块(或带有yield语句的方法)会被编译器转换为一个由编译器生成的类。该类实现IEnumerator接口,而yield语句则被转换为该类的“状态”。
例如,下面的代码:
yield return 1;
yield return 2;
yield return 3;

可能会被转化为类似以下的形式:
switch (state)
{
    case 0: goto LABEL_A;
    case 1: goto LABEL_B;
    case 2: goto LABEL_C;
}
LABEL_A:
    return 1;
LABEL_B:
    return 2;
LABEL_C:
    return 3;

迭代器块可以看作是抽象的状态机。这段代码将会被 IEnumerator 的方法所调用。


1
简而言之,在等待马克的长篇版本时,当编译器看到yield语句时,在幕后为您构建一个新的自定义类实例,该实例实现了一个名为IEnumerator的接口,该接口具有方法Current()和MoveNext(),并跟踪您当前在迭代过程中的位置... 在上面的示例中,它还将跟踪要枚举的列表中的值。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接