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接口
这个类里面:
应该可以理解,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:
很熟悉对不对
同样是自动生成了一个类,累的名字叫做 <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自己来控制的,除非看源码才能深入理解了吧。
按照下面这个图:
Coroutine相关函数的执行,是在LateUpdate之后,不管你使用yield,yield WaitForSeconds......