Unity中协程与迭代器的关系

Unity中非常重要的异步功能就是协程。讲协程和线程的区别以及它们的用法的文章很多,讲Unity执行协程的底层原理的也很多,这篇文章旨在讨论二者中间的部分,就是协程是怎么来的,它为什么要有固定格式。

迭代器

很多人都知道协程是基于C#迭代器实现的,因此为了了解协程,我们先来了解迭代器。

如何实现foreach

首先假设我们没有foreach。当我们想遍历一个List时,就只能用for循环或while循环(二者本质都一样,以下都用for循环讲解)实现,循环变量为List的下标。这样一段代码就很自然地写出来了。

List<int> integerList = new List<int>() { 1, 2, 3 };
for (int i = 0; i < integerList.Count; i++)
{
    Debug.Log(i);
}

接下来我们打算遍历一个Dictionary。Dictionary同样能获取Count,但是此时我们发现没有哪个循环变量能索引到Dictionary中的值,因为它靠的是key来索引而不是下标。也就是我们无法知道Dictionary的内部结构,就无法从外部一一遍历它的内容。那有什么方法解决这个问题呢?答案就是迭代器模式

迭代器在外部暴露一个MoveNext的接口用于往下遍历,当遍历结束时返回false。然后提供Current属性获取当前遍历到的值。至于MoveNext是如何遍历内部结构的,外部是不用管的。C#的迭代器接口如下:

public interface IEnumerator
{
    object Current { get; }
    bool MoveNext();
    void Reset();
}

而Dictionary实现了IEnumerable接口,它要求实现一个GetEnumerator函数返回迭代器供外部使用。这时候我们就能用循环实现迭代Dictionary了。

Dictionary<int, string> intStringDic = new Dictionary<int, string>();
for (var iter = intStringDic.GetEnumerator(); iter.MoveNext();)
{
    Debug.Log(iter.Current.Value);
}

注意在迭代器模式下MoveNext实现了往下遍历和判断遍历是否结束两个功能,因此这里将MoveNext放在判断遍历是否结束的地方就好。

这样的迭代器模式写法非常常见,因此C#把这种写法用语法糖包了一层,变成了原生的语法支持,那就是foreach。

手写一个迭代器

接下来我们手写一个迭代器来实现foreach遍历输出周一到周日的英文缩写。

class WeekEnumerator : IEnumerator
{
    string[] weekdays = { "Mon", "Tues", "Wed", "Thurs", "Fri", "Sat", "Sun" };
    int index = -1;
    public object Current => weekdays[index];
    public bool MoveNext()
    {
        index++;
        return index < weekdays.Length;
    }
    public void Reset()
    {
        index = -1;
    }
}

上述代码继承了IEnumerator并实现了其声明的函数。注意index初始是-1,因为foreach会先执行一次MoveNext再获取Current。有迭代器还不够因为foreach调用的是GetEnumerator()方法,而上述代码被我写在Mono脚本里作为内部类,所以我们在Mono脚本里写一个GetEnumerator方法。

public IEnumerator GetEnumerator()
{
    return new WeekEnumerator();
}

这时候我们可以在Start方法中这样调用foreach来实现迭代:

void Start()
{
    foreach (string weekday in this) //in this。正确且有趣的写法。
    {
        Debug.Log(weekday);
    }
}

一个注意点是foreach不要求必须实现IEnumerable,只需要有GetEnumerator方法就够了。

迭代器就此写完了。但是上一节讲过类似的迭代器非常常见,每一个我们都要实现IEnumerator,太麻烦了。C#也这么认为,因此它提供了一个简洁到不能再简洁的语法方便我们写迭代器。

yield return

上面的一大堆代码,通过C#提供的yield return语法可以这么实现:

public IEnumerator GetEnumerator()
{
    yield return "Mon";
    yield return "Tues";
    yield return "Wed";
    yield return "Thurs";
    yield return "Fri";
    yield return "Sat";
    yield return "Sun";
}

需要非常注意的是yield return和return是两个相差很大的关键字,他们俩的关系就跟javascript和java的关系一样。

 C#在遇到带有yield return关键字的函数时,会在底层包装成一个迭代器返回。该迭代器每次调用MoveNext的时候都会执行到下一个yield return,并将Current设置为yield return的值。

看上述的代码,有没有觉得似曾相识,没错,它就是一个协程函数。

协程状态机

经过上面的过程,我们对协程函数已经有了一个本质的了解。而Unity实现协程是通过内置的协程状态机实现的,它参与Unity的生命周期,并对协程进行状态管理。

当我们使用StartCoroutine时,Unity会把协程函数返回的迭代器放进协程状态机里,依据Current的不同在特定的时间再次调用迭代器。比如协程函数执行到“yield return new WaitForEndOfFrame();”,说明当前迭代器的Current是WaitForEndOfFrame类型的实例,那么该迭代器会在LateUpdate之后由协程状态机执行一次MoveNext()。我们可以用下面的代码模拟一个协程函数的执行。

using System.Collections;
using UnityEngine;

public class CoroutineSimulator : MonoBehaviour
{
    void Start()
    {
        //开启协程
        CoroutineSimulate(WaitLog());
    }

    //模拟等待
    class MyWaitForSeconds
    {
        public bool KeepWaiting { get => _seconds > 0; }
        private float _seconds;
        public MyWaitForSeconds(float seconds)
        {
            _seconds = seconds;
        }
        public void Tick(float tick)
        {
            _seconds -= tick;
        }
    }
    private IEnumerator cEnumerator;
    //在Update中模拟管理协程
    void Update()
    {
        if (cEnumerator != null)
        {
            if (cEnumerator.Current is MyWaitForSeconds)
            {
                MyWaitForSeconds myWait = cEnumerator.Current as MyWaitForSeconds;
                if (myWait.KeepWaiting)
                {
                    myWait.Tick(Time.deltaTime);
                }
                else
                {
                    cEnumerator.MoveNext();
                }
            }
            else
            {
                cEnumerator.MoveNext();
            }
        }
    }
    //模拟开启协程
    void CoroutineSimulate(IEnumerator enumerator)
    {
        cEnumerator = enumerator;
    }

    //一般的协程函数
    IEnumerator WaitLog()
    {
        Debug.Log("Current is " + Time.time);
        yield return new MyWaitForSeconds(1);
        Debug.Log("Wait 1s and Now is " + Time.time);
        yield return new MyWaitForSeconds(2);
        Debug.Log("Wait 2s and Now is " + Time.time);
    }
}

执行结果如图

注意上述代码不代表Unity的协程状态机真实执行逻辑,它要复杂得多。MyWaitForSeconds也跟WaitForSeconds的实现不一致。这里只是简单实现类似功能而已。 

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值