您现在的位置是:首页 >技术交流 >C#迭代器和Unity的Coroutine原理网站首页技术交流

C#迭代器和Unity的Coroutine原理

omegayy 2025-03-26 00:01:02
简介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接口。模拟一下执行流程:

  1. 当进行foreach遍历时,代码转为:

    using (var enumerator = Test().GetEnumerator())
     while (enumerator.MoveNext())
     {
     var element = enumerator.Current;
     }
    
  2. 执行GetEnumerator,将state设置为0。

  3. 执行MoveNext,state为0,将_current设置为1,state设置为1;return true;

  4. 下一次循环,继续执行MoveNext,state为1,将_current设置为2,state设置为2;return true;

  5. 下一次循环,继续执行MoveNext,state为2,将_current设置为3,state设置为3;return true;

  6. 下一次循环,继续执行MoveNext,state为3,state设置为-1;return false;

  7. 循环结束,完成和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。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。