C# IEnumerator yield 浅析

先看一个很简单的例子:

using System;

using System.Collections.Generic;

 

namespace ConsoleApplication4

{

    class Program

    {

        //一个返回类型为IEnumerable<int>,其中包含三个yield return

        public static IEnumerable<int> enumerableFunc()

        {

            yield return 1;

            yield return 2;

            yield break;//用来终止迭代

            yield return 3;

        }

 

        static void Main(string[] args)

        {

            //通过foreach循环迭代此函数

            foreach (int item in enumerableFunc())

            {

                Console.WriteLine(item);

            }

            Console.ReadKey();

        }

    }

}

最终输出1 2

 

 

 

 

.class auto ansi sealed nested private beforefieldinit '<enumerableFunc>d__0'

       extends [mscorlib]System.Object

       implements class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>,

                  [mscorlib]System.Collections.IEnumerable,

                  class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>,

                  [mscorlib]System.IDisposable,

                  [mscorlib]System.Collections.IEnumerator

{

  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )

} // end of class '<enumerableFunc>d__0'

 

说明是自动生成了一个类,实现IEnumerable和IEnumerator接口

这个类里面:

C# IEnumerator yield 浅析

 

应该可以理解,IEnumerable就是通过Current属性来记录现在已经遍历到哪里了

因为实现了IEnumerable和IEnumerator接口,所以就可以使用MoveNext和get_Current接口了

 

 

enumerableFunc的IL:

.method public hidebysig static class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>

        enumerableFunc() cil managed

{

  .custom instance void [mscorlib]System.Runtime.CompilerServices.IteratorStateMachineAttribute::.ctor(class [mscorlib]System.Type) = ( 01 00 30 43 6F 6E 73 6F 6C 65 41 70 70 6C 69 63   // ..0ConsoleApplic

                                                                                                                                        61 74 69 6F 6E 34 2E 50 72 6F 67 72 61 6D 2B 3C   // ation4.Program+<

                                                                                                                                        65 6E 75 6D 65 72 61 62 6C 65 46 75 6E 63 3E 64   // enumerableFunc>d

                                                                                                                                        5F 5F 30 00 00 )                                  // __0..

  // 代码大小       8 (0x8)

  .maxstack  8

  IL_0000:  ldc.i4.s   -2

  IL_0002:  newobj     instance void ConsoleApplication4.Program/'<enumerableFunc>d__0'::.ctor(int32)

  IL_0007:  ret

} // end of method Program::enumerableFunc

 

可以看出这个函数只是构造了一个对象而已

 

那么enumerableFunc是怎么知道当前已经迭代到了哪里的呢

看看MoveNext的IL的节选:

  IL_0001:  ldfld      int32 ConsoleApplication4.Program/'<enumerableFunc>d__0'::'<>1__state'

Ldfld表示:查找对象中其引用当前位于计算堆栈的字段的值。

说白了就是计算<>1__state的值

然后根据其值计算跳转表

  IL_0008:  switch     (

                        IL_001f,

                        IL_0021,

                        IL_0023,

                        IL_0025)

到对应的位置执行

 

比如:IL_001f:  br.s       IL_0029 //无条件地将控制转移到目标指令

 

è

 

IL_0029:  ldarg.0   // this指针的引用

  IL_002a:  ldc.i4.m1   //将整数值 -1 作为 int32 推送到计算堆栈上

  IL_002b:  stfld      int32 ConsoleApplication4.Program/'<enumerableFunc>d__0'::'<>1__state'

//用新值替换在对象引用或指针的字段中存储的值

  IL_0030:  nop

  IL_0031:  ldarg.0

  IL_0032:  ldc.i4.1    //将整数值 1 作为 int32 推送到计算堆栈上

  IL_0033:  stfld      int32 ConsoleApplication4.Program/'<enumerableFunc>d__0'::'<>2__current'

  IL_0038:  ldarg.0

  IL_0039:  ldc.i4.1

  IL_003a:  stfld      int32 ConsoleApplication4.Program/'<enumerableFunc>d__0'::'<>1__state'

  IL_003f:  ldc.i4.1

  IL_0040:  ret

 

最终相当于是<>1__state = 1, <>2__current = 1

 

接下来

IL_0041:  ldarg.0

  IL_0042:  ldc.i4.m1

  IL_0043:  stfld      int32 ConsoleApplication4.Program/'<enumerableFunc>d__0'::'<>1__state'

  IL_0048:  ldarg.0

  IL_0049:  ldc.i4.2

  IL_004a:  stfld      int32 ConsoleApplication4.Program/'<enumerableFunc>d__0'::'<>2__current'

  IL_004f:  ldarg.0

  IL_0050:  ldc.i4.2

  IL_0051:  stfld      int32 ConsoleApplication4.Program/'<enumerableFunc>d__0'::'<>1__state'

  IL_0056:  ldc.i4.1

  IL_0057:  ret

相当于<>1__state = 2, <>2__current = 2

 

这就是yield return的控制

 

 

全部的MoveNext的IL如下:

 

.method private hidebysig newslot virtual final

        instance bool  MoveNext() cil managed

{

  .override [mscorlib]System.Collections.IEnumerator::MoveNext

  // 代码大小       106 (0x6a)

  .maxstack  2

  .locals init ([0] int32 V_0)

  IL_0000:  ldarg.0

  IL_0001:  ldfld      int32 ConsoleApplication4.Program/'<enumerableFunc>d__0'::'<>1__state'

  IL_0006:  stloc.0

  IL_0007:  ldloc.0

  IL_0008:  switch     (

                        IL_001f,

                        IL_0021,

                        IL_0023,

                        IL_0025)

  IL_001d:  br.s       IL_0027

  IL_001f:  br.s       IL_0029

  IL_0021:  br.s       IL_0041

  IL_0023:  br.s       IL_0058

  IL_0025:  br.s       IL_0061

  IL_0027:  ldc.i4.0

  IL_0028:  ret

  IL_0029:  ldarg.0

  IL_002a:  ldc.i4.m1

  IL_002b:  stfld      int32 ConsoleApplication4.Program/'<enumerableFunc>d__0'::'<>1__state'

  IL_0030:  nop

  IL_0031:  ldarg.0

  IL_0032:  ldc.i4.1

  IL_0033:  stfld      int32 ConsoleApplication4.Program/'<enumerableFunc>d__0'::'<>2__current'

  IL_0038:  ldarg.0

  IL_0039:  ldc.i4.1

  IL_003a:  stfld      int32 ConsoleApplication4.Program/'<enumerableFunc>d__0'::'<>1__state'

  IL_003f:  ldc.i4.1

  IL_0040:  ret

  IL_0041:  ldarg.0

  IL_0042:  ldc.i4.m1

  IL_0043:  stfld      int32 ConsoleApplication4.Program/'<enumerableFunc>d__0'::'<>1__state'

  IL_0048:  ldarg.0

  IL_0049:  ldc.i4.2

  IL_004a:  stfld      int32 ConsoleApplication4.Program/'<enumerableFunc>d__0'::'<>2__current'

  IL_004f:  ldarg.0

  IL_0050:  ldc.i4.2

  IL_0051:  stfld      int32 ConsoleApplication4.Program/'<enumerableFunc>d__0'::'<>1__state'

  IL_0056:  ldc.i4.1

  IL_0057:  ret

  IL_0058:  ldarg.0

  IL_0059:  ldc.i4.m1

  IL_005a:  stfld      int32 ConsoleApplication4.Program/'<enumerableFunc>d__0'::'<>1__state'

  IL_005f:  ldc.i4.0

  IL_0060:  ret

  IL_0061:  ldarg.0

  IL_0062:  ldc.i4.m1

  IL_0063:  stfld      int32 ConsoleApplication4.Program/'<enumerableFunc>d__0'::'<>1__state'

  IL_0068:  ldc.i4.0

  IL_0069:  ret

} // end of method '<enumerableFunc>d__0'::MoveNext

 

 

 

 

循环体的IL

IL_000d:  call       class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> ConsoleApplication4.Program:: enumerableFunc()

//首先执行enumerableFunc

 

IL_0012:  callvirt   instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()

//获得迭代器

 

  IL_0017:  stloc.0

  .try

  {

    IL_0018:  br.s       IL_002a

    IL_001a:  ldloc.0

IL_001b:  callvirt   instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()

//获得当前位置

 

IL_0020:  stloc.1

//从计算堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中

 

    IL_0021:  nop

    IL_0022:  ldloc.1

    IL_0023:  call       void [mscorlib]System.Console::WriteLine(int32)

    IL_0028:  nop

    IL_0029:  nop

    IL_002a:  ldloc.0

IL_002b:  callvirt   instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()

//执行MoveNext

 

    IL_0030:  brtrue.s   IL_001a

    IL_0032:  leave.s    IL_003f

  }  // end .try

  finally

  {

    IL_0034:  ldloc.0

    IL_0035:  brfalse.s  IL_003e

    IL_0037:  ldloc.0

IL_0038:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()

//捕获到异常执行Dispose

 

    IL_003d:  nop

    IL_003e:  endfinally

  }  // end handler

 

========

Unity Coroutine

另一个我们经常看到yield return的地方是在Unity Coroutine中

同样先看例子:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class testa : MonoBehaviour {

    public IEnumerator testco()
    {
        Debug.Log("test1");
        yield return new WaitForSeconds(3);
        Debug.Log("test2");
    }

    // Use this for initialization
    void Start () {
        Debug.Log("s1");
        StartCoroutine(testco());
        Debug.Log("s2");
    }
    
    // Update is called once per frame
    void Update () {
        
    }
}

 

执行了输出s1 test1 之后,协程会返回其被调用的地方继续执行,三秒钟之后才会继续testco中test2的地方继续执行

这个在实现上,除了UnityEngine.MonoBehaviour中做的控制,单纯的C#代码所做的事情与之前看过的遍历如出一辙

直接上IL:

C# IEnumerator yield 浅析

很熟悉对不对

同样是自动生成了一个类,累的名字叫做 <testco>d__0

并且其实现了

[mscorlib]System.Collections.Generic.IEnumerator`1<object>,

[mscorlib]System.Collections.IEnumerator, 

[mscorlib]System.IDisposable

 

Start函数中除了输出s1 s2,中间调用了

  IL_000e:  call       instance class [mscorlib]System.Collections.IEnumerator testa::testco()

IL_0013:  call       instance class [UnityEngine]UnityEngine.Coroutine [UnityEngine] UnityEngine.MonoBehaviour:: StartCoroutine(class [mscorlib]System.Collections.IEnumerator)

 

那么test1 和 test2的输出在哪里?与上一个例子相似,是在<testco>d__0的MoveNext中实现的:

.method private hidebysig newslot virtual final 
        instance bool  MoveNext() cil managed
{
  .override [mscorlib]System.Collections.IEnumerator::MoveNext
  // 代码大小       88 (0x58)
  .maxstack  2
  .locals init ([0] int32 V_0)
  IL_0000:  ldarg.0
  IL_0001:  ldfld      int32 testa/'<testco>d__0'::'<>1__state'  //获得state的值
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  brfalse.s  IL_0012 //如果state是0
  IL_000a:  br.s       IL_000c
  IL_000c:  ldloc.0
  IL_000d:  ldc.i4.1  //数字1入栈
  IL_000e:  beq.s      IL_0014  //如果state == 1
  IL_0010:  br.s       IL_0016
  IL_0012:  br.s       IL_0018
  IL_0014:  br.s       IL_0044  //则转到44去执行
  IL_0016:  ldc.i4.0
  IL_0017:  ret   //这就return了,因为state无效,根本不知道该怎么执行,这个时候current也是无效的,不可访问,否则会报错
  IL_0018:  ldarg.0   //state是0的情况,走下面输出test1的流程
  IL_0019:  ldc.i4.m1
  IL_001a:  stfld      int32 testa/'<testco>d__0'::'<>1__state'
  IL_001f:  nop
  IL_0020:  ldstr      "test1"
  IL_0025:  call       void [UnityEngine]UnityEngine.Debug::Log(object)  //输出test1
  IL_002a:  nop
  IL_002b:  ldarg.0
  IL_002c:  ldc.r4     3.    //float值3.0 入栈 
  IL_0031:  newobj     instance void [UnityEngine]UnityEngine.WaitForSeconds::.ctor(float32)  

//所以UnityEngine.WaitForSeconds是一个类


  IL_0036:  stfld      object testa/'<testco>d__0'::'<>2__current'
  IL_003b:  ldarg.0
  IL_003c:  ldc.i4.1
  IL_003d:  stfld      int32 testa/'<testco>d__0'::'<>1__state'
  IL_0042:  ldc.i4.1
  IL_0043:  ret
  IL_0044:  ldarg.0 //接下来就是输出test2了,所以可以看出来,还是用state来控制的在什么地方开始执行
  IL_0045:  ldc.i4.m1
  IL_0046:  stfld      int32 testa/'<testco>d__0'::'<>1__state'
  IL_004b:  ldstr      "test2"
  IL_0050:  call       void [UnityEngine]UnityEngine.Debug::Log(object)
  IL_0055:  nop
  IL_0056:  ldc.i4.0
  IL_0057:  ret
} // end of method '<testco>d__0'::MoveNext

 

那么与上一个例子中的enumerableFunc所对应的testco函数呢?

.method public hidebysig instance class [mscorlib]System.Collections.IEnumerator 
        testco() cil managed
{
  // 代码大小       14 (0xe)
  .maxstack  8
  IL_0000:  ldc.i4.0
  IL_0001:  newobj     instance void testa/'<testco>d__0'::.ctor(int32)
  IL_0006:  dup
  IL_0007:  ldarg.0
  IL_0008:  stfld      class testa testa/'<testco>d__0'::'<>4__this'
  IL_000d:  ret
} // end of method testa::testco

还是构造了<testco>d__0类的对象,一样的

 

那么再次调用testco()的时机是如何控制的呢?这个应该是Monobehavior自己来控制的,除非看源码才能深入理解了吧。

按照下面这个图:

C# IEnumerator yield 浅析

Coroutine相关函数的执行,是在LateUpdate之后,不管你使用yield,yield WaitForSeconds......