在Unity中,IEnumerator 是一个接口,用于实现协程(Coroutine)。协程是一种特殊的函数,它可以在多个帧之间暂停和恢复执行,这在执行长时间操作时非常有用,比如加载资源、等待网络请求等。
正如上述AI的回复,协程的返回对象是IEnumerator,也就是非泛型枚举器的基类接口,IEnumerator的定义大体如下:
public interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}
- Current属性:用于获取集合中当前迭代位置对应的元素
- MoveNext方法:用于将迭代位置的索引推进到下个位置,如果成功则返回true,已经指向到末尾则返回false
- Reset方法:将当前迭代位置索引重置为初始位置,初始位置通常为第一个元素之前,这样再次调用MoveNext时,Current值即为迭代器中的第一个元素
枚举器可用于读取集合中的数据,但不能用于修改基础集合。
最初,枚举器位于集合中的第一个元素之前。 在读取 Current值之前,必须调用 MoveNext 方法将枚举器前进到集合的第一个元素; 否则,Current 未定义。
Current 返回相同的对象,直到调用 MoveNext 或 Reset。 MoveNext 将 Current 设置为下一个元素。
如果 MoveNext 传递集合的末尾,则枚举器位于集合中的最后一个元素之后,MoveNext 返回 false。 当枚举器处于此位置时,对 MoveNext 的后续调用也会返回 false。 如果最后一次调用 MoveNext 返回 false,则 Current 未定义。
若要再次将 Current 设置为集合的第一个元素,可以调用 Reset(如果已实现)后跟 MoveNext。 如果未实现 Reset,则必须创建新的枚举器实例才能返回到集合的第一个元素。
如果对集合进行了更改(例如添加、修改或删除元素),则枚举器的行为是未定义的。
枚举器不具有对集合的独占访问权限;因此,通过集合进行枚举本质上不是线程安全的过程。 即使集合同步,其他线程仍可以修改集合,这会导致枚举器引发异常。 若要保证枚举期间的线程安全性,可以在整个枚举期间锁定集合,也可以捕获由其他线程所做的更改导致的异常。
更多迭代器相关资料可以查阅IEnumerator 接口
yield return
在Unity中一个IEnumerator函数通常有以下几种不同的yield return定义:
- yield return null:这告诉Unity在当前帧结束时暂停协程的执行,并在下一帧开始时继续执行。
- yield return new WaitForSeconds(time):这告诉Unity等待指定的时间后继续执行协程。
- yield return StartCoroutine(anotherCoroutine):这允许一个协程等待另一个协程完成。
- yield return new WaitUntil(condition):这告诉协程等待直到指定的条件为真。
- yield return new WaitWhile(condition):这告诉协程等待直到指定的条件为假。
假设我们在Unity中定义了如下的迭代器函数:
IEnumerator TestEnumerator()
{
yield return null;
Debug.Log("我是第一个yield return执行后执行的");
yield return null;
Debug.Log("我是第二个yield return执行后执行的");
yield break;
Debug.Log("我是yield break执行后执行的");
yield return null;
}
此时,我们不使用StartCoroutine对TestEnumerator函数开启协程,而是通过直接调用的方式:
void Start()
{
IEnumerator test = TestEnumerator();
}
此时控制台将不会有任何输出。然后调用test.MoveNext()后,第一个yield return才执行:
void Start()
{
IEnumerator test = TestEnumerator();
Debug.Log("第一个yield return执行" + test.MoveNext());
}
此时输出:

如果继续调用MoveNext:
void Start()
{
IEnumerator test = TestEnumerator();
Debug.Log("第一个yield return执行" + test.MoveNext());
Debug.Log("第二个yield return执行" + test.MoveNext());
}
此时控制台将输出:

也就是当第二次调用test.MoveNext()时,第一个yield return与第二个yield return之间的代码将会执行,此时Current指向了第二个yield return。再次调用test.MoveNext后:
void Start()
{
IEnumerator test = TestEnumerator();
Debug.Log("第一个yield return执行" + test.MoveNext());
Debug.Log("第二个yield return执行" + test.MoveNext());
Debug.Log("第三个yield break执行即中止" + test.MoveNext());
}
输出结果为:

从输出结果看最后一次的test.MoveNext()返回了false,也就是Current指向了yield break并返回了false,上面说到,MoveNext返回false,说明迭代器终止,为了验证该结论,我们可以再调用MoveNext():
void Start()
{
IEnumerator test = TestEnumerator();
Debug.Log("第一个yield return执行" + test.MoveNext());
Debug.Log("第二个yield return执行" + test.MoveNext());
Debug.Log("第三个yield break执行即中止" + test.MoveNext());
Debug.Log("第四个yield return无法执行" + test.MoveNext());
}
输出结果为:

也就是yield break确实让迭代器终止了。
为什么yield return能暂停代码的执行?
首先,我没有实际看过Unity的任何源码,但是综合各方面资料来看,一个IEnumerator函数的定义会在编译阶段内部自定义了一个类似xxxTestEnumeratorxxx的类或结构体,内部会把IEnumerator函数内的局部变量变为类或结构体中的属性,而yield return之间的代码会封装在该类或结构体中的xxxMoveNextxxx()函数中,每个yield return间的代码段会抽取到该MoveNext函数内。以下是实现该原理的伪代码:
using System;
using System.Collections;
public class CoroutineSimulator : IEnumerator
{
private enum State
{
Start,
WaitForOne,
PrintSurprise,
WaitForThree,
End
}
private State currentState;
public CoroutineSimulator()
{
currentState = State.Start;
}
public object Current => null; // 实现 IEnumerator 接口的 Current 属性
public bool MoveNext()
{
switch (currentState)
{
case State.Start:
Console.WriteLine("Coroutine starts.");
currentState = State.WaitForOne;
break;
case State.WaitForOne:
Console.WriteLine("Waited for 1 second.");
currentState = State.PrintSurprise;
break;
case State.PrintSurprise:
Console.WriteLine("Surprise!");
currentState = State.WaitForThree;
break;
case State.WaitForThree:
Console.WriteLine("Waited for 3 seconds.");
currentState = State.End;
break;
case State.End:
Console.WriteLine("Coroutine has ended.");
return false; // 结束协程
}
return true; // 继续协程
}
public void Reset()
{
currentState = State.Start; // 重置协程状态
}
}
class Program
{
static void Main(string[] args)
{
CoroutineSimulator coroutine = new CoroutineSimulator();
while (coroutine.MoveNext()) // 模拟协程的执行
{
// 在这里,每次调用 MoveNext 都会执行协程的下一个步骤
}
}
}
大致是这么个意思。回归到Unity内置的StartCoroutine,它内部的MoveNext当条件满足的时候会自动调用,如:yield return null,当下一帧到来的时候就会自动调用MoveNext()