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

C# 中的迭代器 – 深入探讨

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (4投票s)

2014 年 3 月 28 日

CPOL

5分钟阅读

viewsIcon

15595

C# 中的迭代器

引言

本文深入分析了 C# 的 yield 关键字在底层是如何工作的。

如果您对 yield 关键字一无所知,或者从未用过它,请查看我在 我的原始博客上的 C# 迭代器文章CodeProject 上的文章

使用迭代器很简单,但了解它的底层工作原理总是有好处的,对吧?

好吧,为了便于理解,让我们举一个简单的 C# 方法示例,该方法返回一个值列表。

这是代码

public class InDepth
    {
        static IEnumerator DoSomething()
        {
            yield return "start";

            for (int i = 1; i < 3; i++)
            {
                yield return i.ToString();
            }

            yield return "end";
        }
    }

很简单,不是吗?让我们看看编译后的代码

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace YieldDemo
{
  public class InDepth
  {
    public InDepth()
    {
      base..ctor();
    }

    private static IEnumerator DoSomething()
    {
      InDepth.<DoSomething>d__0 doSomethingD0 = new InDepth.<DoSomething>d__0(0);
      return (IEnumerator) doSomethingD0;
    }

    [CompilerGenerated]
    private sealed class <DoSomething>d__0 : IEnumerator<object>, IEnumerator, IDisposable
    {
      private object <>2__current;
      private int <>1__state;
      public int <i>5__1;

      object IEnumerator<object>.Current
      {
        [DebuggerHidden] get
        {
          return this.<>2__current;
        }
      }

      object IEnumerator.Current
      {
        [DebuggerHidden] get
        {
          return this.<>2__current;
        }
      }

      [DebuggerHidden]
      public <DoSomething>d__0(int <>1__state)
      {
        base.\u002Ector();
        this.<>1__state = param0;
      }

      bool IEnumerator.MoveNext()
      {
        switch (this.<>1__state)
        {
          case 0:
            this.<>1__state = -1;
            this.<>2__current = (object) "start";
            this.<>1__state = 1;
            return true;
          case 1:
            this.<>1__state = -1;
            this.<i>5__1 = 1;
            break;
          case 2:
            this.<>1__state = -1;
            ++this.<i>5__1;
            break;
          case 3:
            this.<>1__state = -1;
            goto default;
          default:
            return false;
        }
        if (this.<i>5__1 < 3)
        {
          this.<>2__current = (object) this.<i>5__1.ToString();
          this.<>1__state = 2;
          return true;
        }
        else
        {
          this.<>2__current = (object) "end";
          this.<>1__state = 3;
          return true;
        }
      }

      [DebuggerHidden]
      void IEnumerator.Reset()
      {
        throw new NotSupportedException();
      }

      void IDisposable.Dispose()
      {
      }
    }
  }
}

震惊!我只写了不到 10 行代码,但编译器却生成了太多行。好吧,编译器会创建自动生成的有限状态机来实现 yield 功能。让我们检查一下已编译的代码。

总体观察

  1. 所示代码不是有效的 C# 代码:是的,代码无效。我们将使用有效的 C# 代码来编写程序和逻辑,如果编译器使用相同的有效代码,它会在编译过程中导致与方法和变量声明的冲突。
  2. 其中一些方法带有 [CompilerGenerated][DebuggerHidden] 属性。编译器生成的属性将编译器生成的元素与用户生成的元素区分开来,而 DebuggerHidden 属性会阻止方法进行调试。
  3. <DoSomething>d__0 实现了三个接口:IEnumerator<object>IEnumeratorIDisposable,但我们只实现了一个接口。好吧,即使我们实现了非泛型形式的 IEnumerator,编译器也实现了一个泛型形式的 IEnumeratorIEnumerator<object> 暗示了另外两个接口。

<DoSomething>d__0 中发生了大量的魔法。让我们仔细看看。

  1. 方法中声明了三个变量。分别是 <>1__state<>2__current<i>5__1<>1_state 负责跟踪代码已执行到的位置。<>2__current 将返回迭代器的当前值。<i>5__1 只是计数变量。
  2. Statecurrent 被声明为 private,而 count 被声明为 public。如果我们在 Iterator 块中使用任何参数,那些变量也将是 public
  3. 这里有一个重要的注意事项。DoSomething() 方法调用 <DoSomething>d__0,该方法始终向构造函数传递 0。此参数可能因 Iterator 块使用的返回类型而异。例如,如果我们使用 IEnumerable<int> 作为返回类型,则它会传递初始值“-2”,而不是 0
  4. Current 属性有两个版本。它们都返回 <>2__currentMoveNext()ResetDispose 是已实现的方法。
  5. Reset() 方法始终抛出 NotSupportedException 异常。这通常符合 C# 规范。
  6. 您在 Iterator 块中编写的任何代码都会进入 MoveNext() 方法。它总是一个 switch 语句。currentstatecount 的值在此方法本身中被修改。您可以观察到 switch 的条件语句是当前状态。根据当前状态,值被修改并返回。

迭代器不是独立运行的。当调用迭代器方法时,它只是被创建。实际过程从调用 MoveNext() 开始。MoveNext() 被反复调用,直到达到 yield breakyield return 或方法末尾。

在迭代器中需要注意的一个重要事项是,您不能从具有关联 catch 块的 try 块中进行 yield,也不能从具有 catchfinally 块的 try 块中进行 yield。但是,您可以从仅具有 finally 块而没有 catchtry 块中进行 yield

到目前为止,我们一直从 Iterator 块返回 IEnumerator。让我们将 IEnumerator 替换为 IEnumerable。还要注意,之前从

Iterator 块返回的 IEnumerator 是一个非泛型版本。我们将再次使用具有泛型形式的 IEnumerable 来实现 Iterator 块。修改后的代码如下:

static IEnumerable<string> DoSomething(){
    yield return "start";

    for (int i = 1; i < 3; i++)
    {
        yield return i.ToString();
    }

    yield return "end";
}

另外,让我们把编译后的代码放在这里。我们将检查 IEnumerable 实现有什么新变化。这是代码

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace YieldDemo
{
  public class InDepth
  {
    public InDepth()
    {
      base..ctor();
    }

    private static IEnumerable<string> DoSomething()
    {
      InDepth.<DoSomething>d__0 doSomethingD0 = new InDepth.<DoSomething>d__0(-2);
      return (IEnumerable<string>) doSomethingD0;
    }

    [CompilerGenerated]
    private sealed class <DoSomething>d__0 : IEnumerable<string>, 
    IEnumerable, IEnumerator<string>, IEnumerator, IDisposable
    {
      private string <>2__current;
      private int <>1__state;
      private int <>l__initialThreadId;
      public int <i>5__1;

      string IEnumerator<string>.Current
      {
        [DebuggerHidden] get
        {
          return this.<>2__current;
        }
      }

      object IEnumerator.Current
      {
        [DebuggerHidden] get
        {
          return (object) this.<>2__current;
        }
      }

      [DebuggerHidden]
      public <DoSomething>d__0(int <>1__state)
      {
        base..ctor();
        this.<>1__state = param0;
        this.<>l__initialThreadId = Environment.CurrentManagedThreadId;
      }

      [DebuggerHidden]
      IEnumerator<string> IEnumerable<string>.GetEnumerator()
      {
        InDepth.<DoSomething>d__0 doSomethingD0;
        if (Environment.CurrentManagedThreadId == this.<>l__initialThreadId && this.<>1__state == -2)
        {
          this.<>1__state = 0;
          doSomethingD0 = this;
        }
        else
          doSomethingD0 = new InDepth.<DoSomething>d__0(0);
        return (IEnumerator<string>) doSomethingD0;
      }

      [DebuggerHidden]
      IEnumerator IEnumerable.GetEnumerator()
      {
        return (IEnumerator) this.System.Collections.Generic.IEnumerable<System.String>.GetEnumerator();
      }

      bool IEnumerator.MoveNext()
      {
        switch (this.<>1__state)
        {
          case 0:
            this.<>1__state = -1;
            this.<>2__current = "start";
            this.<>1__state = 1;
            return true;
          case 1:
            this.<>1__state = -1;
            this.<i>5__1 = 1;
            break;
          case 2:
            this.<>1__state = -1;
            ++this.<i>5__1;
            break;
          case 3:
            this.<>1__state = -1;
            goto default;
          default:
            return false;
        }
        if (this.<i>5__1 < 3)
        {
          this.<>2__current = this.<i>5__1.ToString();
          this.<>1__state = 2;
          return true;
        }
        else
        {
          this.<>2__current = "end";
          this.<>1__state = 3;
          return true;
        }
      }

      [DebuggerHidden]
      void IEnumerator.Reset()
      {
        throw new NotSupportedException();
      }

      void IDisposable.Dispose()
      {
      }
    }
  }
}

观察

  1. 首先,DoSomething() 方法的返回类型已更改为 IEnumerable<string>
  2. 同样,值得注意的是,传递给 <DoSomething>d__0() 构造函数的参数已从 0 更改为 -2
  3. 编译器生成的 <DoSomething>d__0 类除了其他接口外,还实现了 IEnumerable<string>IEnumerableIEnumerator<string>
  4. 密封类中 IEnumerator<int> 的实现与 IEnumerator 几乎相同。Current 属性只是返回当前值,Reset 抛出相同的异常,MoveNext() 具有相同的逻辑。
  5. 添加了一个 private 变量 <>l__initialThreadId,在构造函数中设置为当前线程。

那么,发生了什么?当创建 IEnumerable<string> 的实例时,会调用 GetEnumerator() 方法,该方法返回一个 IEnumerator 接口,并继续执行 IEnumerator 中的方法。此外,对集合的只读访问已启用。正是 MoveNext() 方法被一遍又一遍地操作以惰性地返回这些值。

为什么对 DoSomething 构造函数的初始调用从 0 更改为 -2?好吧,这些是告诉编译器它们处于什么状态的代码。以下是状态机运行的状态:

  • 0:表示“工作尚未开始”(之前)。
  • -1:表示“工作正在进行中”(运行中)或“工作已完成”(之后)。
  • -2:这特定于 IEnumerable。这是调用 GetEnumerator 之前的 IEnumerable 的初始状态。
  • 大于 0:表示恢复状态。

另外,这里需要注意的一点是,-2 状态是 IEnumerable 特有的。其他状态是 IEnumerator 特有的。因此,当 IEnumerable 调用 GetEnumerator 方法时,状态将更改为 0,依此类推,因为它返回 IEnumerator 接口。

就是这样!乍一看,这看起来很奇怪,但当我们开始慢慢理解时,它比我们预期的要容易得多。

请分享您对此帖子的想法和评论!谢谢!

© . All rights reserved.