原文:http://gad.qq.com/program/translateview/7170767
译者:陈敬凤(nunu) 审校:王磊(未来的未来)
每个Unity的开发者应该都对协同程序非常的熟悉。对于很多Unity的开发者而言,协同程序就是用来编写大量异步和延时任务的一种方法。如果你不在乎速度的话,有非常非常多的特殊方法可以在任何所需的时间暂停和恢复执行。在实践中,它们可以营造一种并发函数的幻觉 (虽然他们与线程无关!)。然而,协同程序会有一些问题,许多程序员在使用协同程序的时候会偶然发现。让我们仔细看看这些问题。
协同程序的内部机制
那么在Unity协同程序的内部到底发生了什么?Unity协同程序到底是如何工作的?我们没有直接访问Unity源代码的权利,但是我们可以从手册中收集证据,并且通过C#的知识我们可以或多或少的假设它们到底是如何工作的。让我们尽量精简这个示例代码:
1
2
3
4
5
6
7
8
9
|
StartCoroutine(TestCoroutine());
//…
IEnumerator TestCoroutine()
{
Debug.Log(
"Hello there!"
);
yield
return
new
WaitForSeconds(2);
Debug.Log(
"Hello from future!"
);
}
|
你不需要什么特殊的才能,就能很容易的看出这段代码将在终端部分打印出“Hello there!”并在2秒之后打印出” Hello from future! “。但它是如何做到这一点的?要理解协同程序必须首先看下函数的签名——更准确地说,是函数的返回类型。IEnumerator作为一种对集合进行迭代的方法。它控制着从一个对象的执行转移到序列中下一个对象的执行。
为了做到这一点,它声明了两个非常重要的成员变量:一个是Current(当前)属性,它会引用枚举器(或者可以说是游标)目前正在访问的元素,另外一个是MoveNext()函数,它在移动到下一个元素的同时会计算新的Current(当前)值。它也有一个Reset()函数,这个函数会负责将枚举器设置到它的初始位置,但是我们跳过这一部分。
现在由于IEnumerator只是一个接口,并不显式地指定当前类型(除了是一个对象以外我们一无所知)。我们可以做任何我们想要的事情来计算下一个对象。MoveNext()函数只会做这项工作,并且我们已经访问到序列的最后一个元素的时候会返回fasle。
迭代器模块
如果是在一个纯C#的环境,我们可以轻松地在一个特化的迭代器模块里面“实现”这个接口。在实践中,c#编译器会将迭代器模块转换成状态机。这是一些会返回IEnumerator类型并且使用yield return语句来返回值的函数(是的,有可能是多个值)。
调用会使MoveNext()函数只是简单的返回true,并且无论你是从哪返回都将当前的位置保存到Current变量里面。如果你想要停止枚举器的话,你可以简单的调用yield break,这将确保MoveNext()函数返回false并终止整个序列(可以把它想象成在一个循环中进行break)。下面是这么做的一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
using
System;
using
System.Collections.Generic;
class
TestIEnumerator
{
static
readonly
string
inEnumerator =
"####"
;
static
IEnumerator GetTexts()
{
Console.WriteLine(inEnumerator +
"First line of GetTexts()"
+ inEnumerator);
Console.WriteLine(inEnumerator +
"Just before the first yield"
+ inEnumerator);
yield
return
"First returned text"
;
Console.WriteLine(inEnumerator +
"Just after the first yield"
+ inEnumerator);
Console.WriteLine(inEnumerator +
"Just before the second yield"
+ inEnumerator);
int
b = 2;
int
a = 5 + b;
yield
return
"Second returned text - "
+ a;
Console.WriteLine(inEnumerator +
"Just after the second yield"
+ inEnumerator);
}
static
void
Main()
{
Console.WriteLine(
"Calling GetTexts()"
);
IEnumerator iterator = GetTexts();
Console.WriteLine(
"Calling MoveNext()...\n"
);
bool
returnedValue = iterator.MoveNext();
Console.WriteLine(
"\nReturned value = {0}; Current = {1}"
, returnedValue, iterator.Current);
Console.WriteLine(
"Calling MoveNext() again...\n"
);
returnedValue = iterator.MoveNext();
Console.WriteLine(
"\nReturned value = {0}; Current = {1}"
, returnedValue, iterator.Current);
Console.WriteLine(
"Calling MoveNext() again...\n"
);
returnedValue = iterator.MoveNext();
Console.WriteLine(
"\nReturned value = {0} - stopping"
, returnedValue);
Console.ReadKey();
}
}
|
在你自己的电脑上编译这个程序,你会得到以下输出:
下面的例子使用了通用接口IEnumerator,但是正如前面所描述的那样,你可以使用常规的IEnumerator接口,在这个接口里面你的Current变量可以是任意类型。这也正是Unity对于协同程序的要求。
所以,请记住这一点,我们对于Unity到底与协同程序做了什么开始有了一个比较清晰的认识。StartCoroutine函数将协同程序添加到某个容器之中。Unity遍历StartCoroutine中执行的每一个协同程序并执行这些协同程序的MoveNext()函数,而这些函数会来继续执行他们之前中断的工作。正如上面的例子所显示的那样,它会在yield return语句之间评估表达式的值并返回一个值。如果它返回的是false的话,那么显然是在告诉Unity中止这个协同程序(它只是刚刚完成而已)。如果它返回的是true的话,,它会检查当前的属性(记住,这是一个非泛型接口!)并且看下是否有熟悉的类型。还记得第一个例子之中的WaitForSeconds()吗?Unity看到这个函数然后暂停了这个协同程序两秒钟。事实上,它实际上是从YieldInstruction基类型继承而来,你还有以下Unity可以识别的类型:
1) WaitForEndForFrame:在所有的摄像机和GUI都被渲染之后,会在这一帧的结尾来继续这个协同程序。
2) WaitForFixedUpdate:会等到下一个以固定帧速率更新的函数。
3) Coroutine类型自身,这是一个你可以用在之后协同程序的信息。
4) CustomYieldInstruction:这是引入用来写你自己的自定义yield语句-仅仅需要继承这个类然后覆盖keepWaiting属性。