C#迭代器和Unity的Coroutine原理
Enumeration
它提供了foreach对集合进行遍历的机制,它由两部分组成:enumerator和enumerable object。
enumerator是指向一个序列的光标,它是只读的、只能向前的。需要实现下面两个接口之一:
- System.Collections.IEnumerator
- System.Collections.Generic.IEnumerator
foreach语句就是在一个enumerable object上进行遍历。它是一个序列的逻辑表示,它产生一个指向自己的光标。需要实现下面两个接口之一:
- Implements IEnumerable or IEnumerable
- Has a method named GetEnumerator that returns an enumerator
这个模式的示例如下:
class Enumerator // Typically implements IEnumerator or IEnumerator<T>
{
public IteratorVariableType Current { get {...} }
public bool MoveNext() {...}
}
class Enumerable // Typically implements IEnumerable or IEnumerable<T>
{
public Enumerator GetEnumerator() {...}
}
一个高维度的遍历代码如下:
foreach (char c in "beer")
Console.WriteLine (c);
它的底层实现如下:
using (var enumerator = "beer".GetEnumerator())
while (enumerator.MoveNext())
{
var element = enumerator.Current;
Console.WriteLine (element);
}
Iterators/迭代器
foreach是enumerator的使用者,iterator则是enumerator的生产者。如下代码:
using System;
using System.Collections.Generic;
class Test
{
static void Main()
{
foreach (int fib in Fibs(6))
Console.Write (fib + " ");
}
static IEnumerable<int> Fibs (int fibCount)
{
for (int i = 0, prevFib = 1, curFib = 1; i < fibCount; i++)
{
yield return prevFib;
int newFib = prevFib+curFib;
prevFib = curFib;
curFib = newFib;
}
}
}
其中的yield return表示从enumerator返回的下一个元素。这里的Fibs方法就是迭代器。
一个method、property、indexer包含一或多个yield return语句都是Iterator迭代器。iterator必须返回下面四个接口类型:
// Enumerable interfaces
System.Collections.IEnumerable
System.Collections.Generic.IEnumerable<T>
// Enumerator interfaces
System.Collections.IEnumerator
System.Collections.Generic.IEnumerator<T>
编译器把iterator方法转换成一个私有的类型,其实现了IEnumerable或者IEnumerator,函数中的代码块转成current/MoveNext这些必要接口实现。
底层实现
下面通过简单的代码,了解C#编译器如何把yield语句的迭代器实现的。
public class Class2
{
public IEnumerable Test()
{
yield return 1;
yield return 2;
yield return 3;
}
}
使用Dotpeek查看其底层实现
[IteratorStateMachine(typeof (Class2.Enumerable))]
public IEnumerable Test()
{
return (IEnumerable) new Class2.Enumerable(-2);
}
[CompilerGenerated]
private sealed class Enumerable :
IEnumerable<object>,
IEnumerable,
IEnumerator<object>,
IEnumerator,
IDisposable
{
private int state;
private object _current;
private int _initialThreadId;
[DebuggerHidden]
public Enumerable(int _param1)
{
base.ctor();
this.state = _param1;
this._initialThreadId = Environment.CurrentManagedThreadId;
}
bool IEnumerator.MoveNext()
{
switch (this.state)
{
case 0:
this.state = -1;
this._current = (object) 1;
this.state = 1;
return true;
case 1:
this.state = -1;
this._current = (object) 2;
this.state = 2;
return true;
case 2:
this.state = -1;
this._current = (object) 3;
this.state = 3;
return true;
case 3:
this.state = -1;
return false;
default:
return false;
}
}
object IEnumerator<object>.Current
{
[DebuggerHidden] get
{
return this._current;
}
}
object IEnumerator.Current
{
[DebuggerHidden] get
{
return this._current;
}
}
[DebuggerHidden]
IEnumerator<object> IEnumerable<object>.GetEnumerator()
{
Class2.Enumerable enumerator;
if (this.state == -2 && this._initialThreadId == Environment.CurrentManagedThreadId)
{
this.state = 0;
enumerator = this;
}
else
enumerator = new Class2.Enumerable(0);
return (IEnumerator<object>) enumerator;
}
}
这里为了方便理解,只保留了关键的代码。如书中提到,编译器生成了一个私有的Enumerable类,它实现了IEnumerator和IEnumerable接口。模拟一下执行流程:
-
当进行foreach遍历时,代码转为:
using (var enumerator = Test().GetEnumerator()) while (enumerator.MoveNext()) { var element = enumerator.Current; }
-
执行GetEnumerator,将state设置为0。
-
执行MoveNext,state为0,将_current设置为1,state设置为1;return true;
-
下一次循环,继续执行MoveNext,state为1,将_current设置为2,state设置为2;return true;
-
下一次循环,继续执行MoveNext,state为2,将_current设置为3,state设置为3;return true;
-
下一次循环,继续执行MoveNext,state为3,state设置为-1;return false;
-
循环结束,完成和Iterator中yield return相同的遍历结果。
Iterator内部实现,是把代码中yield return的部分移动到MoveNext简单状态机当中,执行过程中切换不同的状态来进行执行流的控制。
引申到Unity的Coroutine
StartCoroutine函数接受的参数是IEnumerator,我们可以写一个Iterator传入,MonoBehavior把这些迭代器集中的在每帧进行enumerator.MoveNext操作,从而实现了协程的效果。外部看来像魔法一样,在不同的帧跳转到同一个函数内部的yield return之后继续执行。
void Start()
{
StartCoroutine(TestCorou1());
}
IEnumerator TestCorou1()
{
Debug.Log("CoroutineBegin " + Time.frameCount);
yield return null;
yield return TestCorou2();
Debug.Log("CoroutineEnd " + Time.frameCount);
}
IEnumerator TestCorou2()
{
Debug.Log("TestCorou2 " + Time.frameCount);
for (int i = 0; i < 10; i++)
{
yield return null;
}
Debug.Log("TestCorou2 End " + Time.frameCount);
}
// Out Put
CoroutineBegin 1
TestCorou2 2
TestCorou2 End 12
CoroutineEnd 12
根据上面代码执行结果,猜测Coroutine内部机制会对YieldInstruction和IEnumerator进行特殊处理。
目前猜测伪码大致如下:
// StartCoroutine内部:
StartCoroutine(IEnumerator e)
if (e.MoveNext())
{
enumList.Add(e);
}
// Update中
Update()
{
foreach (var e in enumList)
{
var current = e.Current;
if (current is YielInstruction)
{
//等待有关操作,不了解其接口
e.wait();
}
else if (current is IEnumerator)
{
var ce = current as IEnumerator;
if (!ce.MoveNext())
e.MoveNext();
}
else
{
e.MoveNext();
}
}
}
如果yield return的是一个IEnumerator,那么后续Coroutine中,会等遍历直到它的MoveNext返回false才继续执行上一级的Iterator。