迭代器与协程
什么是迭代器?
在使用迭代器之前,我们如何遍历一个结构?
很简单,用一个for循环就可以遍历了
for(int i = 0;i< nums.Count; i++)
{
Debug.Log(nums[i]);
}
很自然对不对?但是,考虑两个问题:
- 为什么要从0开始遍历?
- 为什么每次是+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);
}
}
}
很明显,只要继承了迭代器接口,我们就能够用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);
}
}
}
通过断点,我们可以彻底搞明白,这个foreach到底对IEnumerable做了什么
- 最开始,先进入in,调用获取可迭代对象的方法,获取一个new Student()
-
随后调用MoveNext方法,判断迭代器是否可以进行迭代
-
如果迭代器可以进行迭代,那么再从Current方法中获取当前值
那结果很明显了,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;
}
}
如果你试图从一个常规的函数角度来思考这个Test方法,那一定会非常,非常,非常困惑…
事实上,这是一个语法糖,Test并不是一个常规函数,编译器会为我们做好许多其他的操作…
我们查看一下反编译代码,会发现编译器确实帮我们生成了一个Test类
而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();
}
}