当前位置: 首页 > article >正文

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。


http://www.kler.cn/a/535404.html

相关文章:

  • 【JavaScript】《JavaScript高级程序设计 (第4版) 》笔记-Chapter2-HTML 中的 JavaScript
  • hot100(8)
  • creator 接入zendesk Unified SDK 组件样式报错解决方法
  • SpringBoot+SpringDataJPA项目中使用EntityManager执行复杂SQL
  • 【多线程】线程池核心数到底如何配置?
  • React+AI 技术栈(2025 版)
  • Vue el-input密码输入框 按住显示密码,松开显示*;阻止浏览器密码回填,自写密码输入框;校验输入非汉字内容;文本框聚焦到内容末尾;
  • 68.浏览文件并选择文件 C#例子 WPF例子
  • 在K8S中,如何解决SVC容灾问题?
  • 想品客老师的第十二天:异步和promise
  • 图片PDF区域信息批量提取至Excel,基于QT和阿里云api的实现方案
  • Unity 简易的UI框架
  • C和Rust的一些区别
  • C中静态库和动态库的使用
  • 数据治理项目为什么沦为了PPT工程?
  • 2025.2.6(c++杂项补充及qt基础介绍)
  • Vue Dom截图插件,截图转Base64 html2canvas
  • H5+CSS+JS制作好看的轮播图
  • OSPF基础(2):数据包详解
  • 51单片机07 串口通信
  • 【C语言】常量指针和指针常量,指针数组和数组指针,指针函数和函数指针怎么区分?
  • vue2-nextTick
  • JAVA面试框架篇
  • 注册中心不知选哪个?Zookeeper、Eureka、Nacos、Consul和Etcd 5种全方位剖析对比
  • Python利用VideoCapture和FFmpeg读取多个rtsp流性能的比较
  • idea整合deepseek实现AI辅助编程