什么是迭代器?

在使用迭代器之前,我们如何遍历一个结构?

很简单,用一个for循环就可以遍历了

for(int i = 0;i< nums.Count; i++)
{
    Debug.Log(nums[i]);
}

很自然对不对?但是,考虑两个问题:

  1. 为什么要从0开始遍历?
  2. 为什么每次是+1?

这两个问题,看起来莫名其妙,但是很关键。现在,数组是我们的遍历对象,数组是如何定义的,其实我们是不知道的(下标从0开始只是一种约定)

此时的遍历逻辑,完全是我们外部定义的, 是我们自己写的,而非数组本身提供的,这并不符合设计的初衷

考虑一下,如果要用for遍历一个字典,怎么办?没有办法,因为我们完全不知道内部结构,没法给出遍历逻辑

迭代器模式

迭代器模式提供一个方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示

在C#中,继承自IEnumerable或者IEnumerator的类可以认为是一个可以迭代的类

IEnumerator

迭代器,继承自此接口的类,需要提供迭代方法

public class Students : IEnumerator
{
    private int index;
        
    private string id;
    private string name;
    private string score ;
        
    public bool MoveNext()
    {
        if (index < 3)
            return true;
        return false;
    }

    public void Reset()
    {
        throw new System.NotImplementedException();
    }

    public object Current
    {
        get
        {
            switch (index)
            {
                case 0:
                    index++;
                    return id;
                case 1:
                    index++;
                    return name;
                case 2:
                    index++;
                    return score;
                default:
                    return null;
            }
        }
    }

    public Students(string id, string name, string score)
    {
        this.id = id;
        this.name = name;
        this.score = score;
    }
        
}
public class Coroutine : MonoBehaviour
{
    private void Start()
    {
        Students s = new Students("1", "A", "90");
        while (s.MoveNext())
        {
            Debug.Log(s.Current);
        }
    }
}

image-20211120205324514

很明显,只要继承了迭代器接口,我们就能够用MoveNext以及Current来访问一个集合了,此时,集合的遍历逻辑是定义在类内部的,我们不需要知道他的实现,也不需要自己定义遍历逻辑

但此时还不支持使用foreach进行遍历,我们是手动通过while进行的遍历

IEnumerable

意为可迭代的,基础自此接口的类,需要返回一个迭代器

public class Students : IEnumerable, IEnumerator
{
    private int index;

    private string id;
    private string name;
    private string score;

    public bool MoveNext()
    {
        if (index < 3)
            return true;
        return false;
    }

    public void Reset()
    {
        throw new System.NotImplementedException();
    }

    public object Current
    {
        get
        {
            switch (index)
            {
                case 0:
                    index++;
                    return id;
                case 1:
                    index++;
                    return name;
                case 2:
                    index++;
                    return score;
                default:
                    return null;
            }
        }
    }

    public Students()
    {
    }

    private Students(string id, string name, string score)
    {
        this.id = id;
        this.name = name;
        this.score = score;
    }

    public IEnumerator GetEnumerator()
    {
        return new Students("123", "AAA", "99");
    }
}
public class Coroutine : MonoBehaviour
{
    private void Start()
    {
        Students s = new Students();
        foreach (var student in s)
        {
            Debug.Log(student);
        }
    }
}

image-20211120210838131

通过断点,我们可以彻底搞明白,这个foreach到底对IEnumerable做了什么

  1. 最开始,先进入in,调用获取可迭代对象的方法,获取一个new Student() image-20211120211058316

image-20211120211244029

  1. 随后调用MoveNext方法,判断迭代器是否可以进行迭代

    image-20211120211422702

  2. 如果迭代器可以进行迭代,那么再从Current方法中获取当前值

    image-20211120211612344

那结果很明显了,foreach就是一个语法糖,编译器会把foreach编程成类似如下结构

var enumerator = collection.GetEnumerator();
while (enumerator.MoveNext()) 
{
    var item = enumerator.Current;
    {
        // foreach中的操作
    }
}

yield return

通过上面两个案例,很清楚什么是迭代器了,但是,我想说,创建这么一个类真的好麻烦…

于是又出现另一个语法糖yield

我们重新定义一下上面的方法

public class Coroutine : MonoBehaviour
{
    private void Start()
    {
        var test = Test();
        while (test.MoveNext())
        {
            Debug.Log(test.Current);
        }
    }

    private IEnumerator Test()
    {
        Debug.Log("AAAA");
        yield return 1;
        Debug.Log("BBBB");
        Debug.Log("CCCC");
        yield return 2;
        Debug.Log("DDDD");
        yield return 3;
    }
}

image-20211121133515499

如果你试图从一个常规的函数角度来思考这个Test方法,那一定会非常,非常,非常困惑…

事实上,这是一个语法糖,Test并不是一个常规函数,编译器会为我们做好许多其他的操作…

我们查看一下反编译代码,会发现编译器确实帮我们生成了一个Test类

image-20211121132831041

而Test类也确实继承了IEnumerator

[CompilerGenerated]
private sealed class <Test>d__1 : IEnumerator<object>, IEnumerator, IDisposable
{
	//...
}

此时再查看我们调用Test方法的那一行

private void Start()
{
	IEnumerator test;
	test = this.Test();
	while (test.MoveNext())
	{
		Debug.Log(test.Current);
	}
}

发现没?就是实例化了一个Test类而已

我们再来仔细研究一下编译器帮助我们生成的这个类

[CompilerGenerated]
private sealed class <Test>d__1 : IEnumerator<object>, IEnumerator, IDisposable
{
	private int <>1__state;

	private object <>2__current;

	public Coroutine <>4__this;
    
    // ...
}

首先是构造方法

[DebuggerHidden]
public <Test>d__1(int <>1__state)
{
	this.<>1__state = <>1__state;
}

就是给状态赋值,但事实上在上面的图中可以发现,实例化这个类的时候是没有任何参数传入的,所以默认的<>1__state就是0

最关键的MoveNext方法

private bool MoveNext()
{
	switch (this.<>1__state)
	{
		default:
			return false;
		case 0:
			this.<>1__state = -1;
			Debug.Log((object)"AAAA");
			this.<>2__current = 1;
			this.<>1__state = 1;
			return true;
		case 1:
			this.<>1__state = -1;
			Debug.Log((object)"BBBB");
			Debug.Log((object)"CCCC");
			this.<>2__current = 2;
			this.<>1__state = 2;
			return true;
		case 2:
			this.<>1__state = -1;
			Debug.Log((object)"DDDD");
			this.<>2__current = 3;
			this.<>1__state = 3;
			return true;
		case 3:
			this.<>1__state = -1;
			return false;
	}
}

default我们不管

首先会让state=-1,大概是一种保护作用吧

随后调用迭代器至yield return(包括)之间的所有方法,并且yield return的返回值就是current

然后让当前状态+1

如果我们不调用movenext,那current就会一直保持上一次的结果,如果我们一次movenxet都没有使用过,那么current会是Null

协程

首先,说在开头,协程,他就是一个迭代器的扩展!

用过协程的都知道,协程可以在每一帧进行一次迭代,而不是一次性就迭代完,这和我们上面写的迭代器可能有所不同

因为我们只有一个迭代器…用的还是while一次性迭代到死,那必然是不可能实现协程这种分帧指行的能力…

事实上,在我们上面的例子中,迭代器和while循环没有什么不同…

此时,我们还需要一个协程管理类!

当然在此之前,我们再考虑一下,Unity的协程是可以返回很多类型的,比如Null啊,WaitForSecond啊,这里Unity的封装肯定比我复杂,我们简单提供一下示例

先定义一个接口用于等待

public interface IWait
{
    bool Tick();
}

实现两个等待类型

public class WaitForSeconds : IWait
{
    private float waitTime;

    public WaitForSeconds(float waitTime)
    {
        this.waitTime = waitTime;
    }

    public bool Tick()
    {
        waitTime -= Time.deltaTime;
        return waitTime <= 0;
    }
}

public class WaitForFrames : IWait
{
    private int waitFrame;

    public WaitForFrames(int waitFrame)
    {
        this.waitFrame = waitFrame;
    }
        
    public bool Tick()
    {
        waitFrame -= 1;
        return waitFrame <= 0;
    }
}

协程管理器

public class CoroutineManager
{
    private LinkedList<IEnumerator> coroutines = new LinkedList<IEnumerator>();

    public void StartCoroutine(IEnumerator ie)
    {
        coroutines.AddLast(ie);
    }

    public void OnUpdate()
    {
        var node = coroutines.First;
        while (node != null)
        {
            var ie = node.Value;
            var flag = true;
                
            // 如果是等待类型
            if (ie.Current is IWait)
            {
                var wait = ie.Current as IWait;
                if (wait!=null && wait.Tick())
                {
                    flag = ie.MoveNext();
                }
            }
            // null类型
            else if (ie.Current is null) 
            {
                flag = ie.MoveNext();
            }
            else
            {
                flag = ie.MoveNext();
            }

            if (!flag)
            {
                coroutines.Remove(node);
            }
            node = node.Next;
        }
    }
}

其实很简单,我们只是遍历了当前所有的迭代器,并对每一个迭代器的状态进行判断而已

再提醒一下,MoveNext会调用迭代器到下一次yield return(包括)之间的所有方法,而yield return的返回值就是Current

此时在主工程中调用即可

public class Coroutine : MonoBehaviour
{
    private CoroutineManager manager;
    private void Start()
    {
        manager = new CoroutineManager();
        manager.StartCoroutine(Test());
    }

    private IEnumerator Test()
    {
        Debug.Log(1);
        yield return new WaitForSeconds(1);
        Debug.Log(2);
        yield return new WaitForFrames(20);
        Debug.Log(3);
    }

    private void Update()
    {
        manager.OnUpdate();
    }
}

2