写完以后发现了这篇博文,看完觉得自己太浅薄了。
Coroutine,你究竟干了什么?_tkokof1的专栏-CSDN博客blog.csdn.net好多年没怎么用Unity,回过头来一看已经2020版本了,哈哈,当年最早还是用1.7版本,一帮菜鸟级码农在出租屋里写着大量if嵌套的丑陋代码,后来自己跟着一位大神开始C#+Unity之不归路,当时还是3.5版本,而自己做了个小项目的时候升到了4.3。真是神奇的旅程。
然而Unity不变的还是StartCoroutine和Yield Return这种古老的神仙级写法。
查阅了度娘,对于yield return究竟是什么鬼,有几篇文章涉及到其用法,但是并没有深挖其根源。而在当今async/await模式和Task已经大行其道的时候,是否还有必要使用yield return,确实还是值得略作研究的。
首先我们看看码农世界以外早就存在的yield:
当年跟小老板去美帝,两人胆子也算肥的,老板英文一般般,我驾照都没有,两眼一抹黑就上了路,在很多地方看到“U-Turn Yield”之类标记。对于U-Turn,我们在出发前已经研究过了,就是掉头嘛!如果干脆是一个红色圆圈圈中间一个斜杠杠,这倒好理解了,但是很多地方没有这斜杠杠,而是写了“u-turn yield to ...”这样的文字,我们就懵了,也不敢掉头,一直等到后面车不耐烦滴喇叭,才一咬牙掉了过去。
回来后研究了,原来就是不禁止掉头,但是必须礼让......所以“Yield”实际上翻译过来应该是“礼让三先”或者“宁挺三分不抢一秒”的意思。
所以回到编程世界,我相信C#之父当年构造yield语法的时候必然是开过车的.....
那么yield return是Unity发明的吗?
嗯,但凡有C#驾照的码驴们应该都知道IEnumerable和IEnumerator。如果我们自己写了一个类似Array或者List这样的集合型数据结构,并且试图用foreach遍历这个类的时候,系统就会告诉你,没有实现IEnumerable,不许开车。
所以很明显,yield return是C#语言层次的东西。
“这也能说明yield关键字其实是一种语法糖,最终还是通过实现IEnumberablec# yield关键字原理详解 - blueberryzzz - 博客园www.cnblogs.com、IEnumberable、IEnumberator 和IEnumberator接口实现的迭代功能。”
在一篇文章中我们看到,yield return 0和yield return 1,yield return null完全没有区别。那么这究竟是为什么呢?
首先看StartCoroutine:
//先有一个Coroutine方法:
IEnumerator Test()
{
string response = string.Empty;
yield return 0;
textBox.text = "YieldTest";
}
//然后调用Test
void Start()
{
var result = Test();
}
在Test1方法路口处打断点,然后运行程序,发现Test方法更本没运行。
如果把Start方法中的语句改为:
var result = StartCoroutine( Test()) ;
就一切正常了(这也是Unity官方给出的示例方式)。
如果把Test方法返回值类型改为void,或者其它类型,则语法检测根本通不过。
注意,这里var result的类型,可以看到是Coroutine,而这个Coroutine本质上应该就是一个迭代器(不知道我推测是否准确),但是Unity没有进一步通过Coroutine对外暴露IEnumerator,因此我们拿到的这个result对象啥都不能做:
IEnumerator对外暴露了一个Current因此能够获取return返回值,而Coroutine啥都没有了因此,StartCoroutine作用其实就是启动运行一个Coroutine协程,而Coroutine则是Untiy的发明,继承自YieldInstruction,并且具有NativeHeader和RequiredByNativeCode属性标签。至此,我们已经无法继续往下看Coroutine和这个YieldInstruction里面有什么(YieldInstruction我们后面再说)。
Coroutine没有对外返回IEnumerator YieldInstruction只有一个无参构造函数再做一个试验。因为IEnumerable是具有泛型形式的,同样我们看到IEnumerator也有泛型,那么是否就能够通过yield return像实现foreach那样返回单个值呢?我们把Coroutine代码改成:
IEnumerator Test2()
{
string response = string.Empty;
yield return "YieldTest2";
}
void Start()
{
var result = StartCoroutine(receiver = Test2().Current);
}
结果在Visual Studio中语法检查通过,并且可以看到IEnumerator.Current就是我们想要的string类型值了。附加到Unity进程开始调试Unity却提示错误:
呵呵,看来Unity并不希望在Coroutine上面做更多文章啦。大致猜测Unity就是基于C#的Yield Return机制来实现的Coroutine,而这个Coroutine目的是实现类似于多线程或者当今的async/await来提高耗时任务的处理效率,但却不想把语法搞得太复杂,以免降低整体代码框架的稳定性。
好,前面我们已经知道yield return后面跟什么返回值并没有差异,但是为什么在unity中会有WaitForSeconds,WaitForEndOfFrame或者www呢?
仔细看,yield return WaitForSeconds正确的语法实际上是:
yield return new WaitForSeconds(seconds);
这里有个new关键字。因此我们就明白了,WaitForSeconds其实是unity命名空间下的一个class而已。
等待时长的参数在构造器入口传入从C#语法层面来看,yield return是返回值的,但是unity通过Coroutine封装去掉了传值接口。既然是return,就必须先获得一个对象实体,所以yield return后面需要通过new构造一个新的值(当然也可以用现有的对象实体,就不需要new了)。
这样的话,叠加前面所说yield return后面跟什么返回值都无所谓的说法,那么我们也可以返回Action啦?
public Text textBox;
IEnumerator Test()
{
HttpClient client = new HttpClient();
string response = string.Empty;
//yield return 后面跟的Action根本不执行
yield return new Action(
async () =>
response = await HttpClientGet("http://www.baidu.com"));
textBox.text = response;
}
果然在Visual Studio里面语法检查也是没有问题的,但是执行的时候发现Action从未执行,textbox的内容因此一直是空的。想了一下,应该是因为代码只是生成了一个新的action对象,但是没有invoke,所以并不会执行。但是如果要在return的时候Invoke,而Invoke方法的返回值类型是void,与前面定义的IEnumerator返回类型不匹配,这样写是不行的。
既然Action不能执行,那WaitForSeconds又是怎么实现等待的?回到前面WaitForSeconds的从元数据信息,我们看到在构造器中传入了一个seconds参数,而WaitForSeconds又是继承自YieldInstruction的,因此猜想是不是在构造函数中实现了所需的等待?那我们也搞一个!
public class UnityYieldTest : YieldInstruction
{
public UnityYieldTest(int milliseconds)
{
Thread.Sleep(milliseconds);
}
}
//----------------------------------------------------------
//Unity Script代码
//......
void Start()
{
//var result = StartCoroutine(Test3());
}
IEnumerator Test3()
{
yield return new UnityYieldTest(1000);
timerBox.text = "5";
yield return new UnityYieldTest(1000);
timerBox.text = "4";
yield return new UnityYieldTest(1000);
timerBox.text = "3";
yield return new UnityYieldTest(1000);
timerBox.text = "2";
yield return new UnityYieldTest(1000);
timerBox.text = "1";
yield return new UnityYieldTest(1000);
timerBox.text = "0";
yield return new UnityYieldTest(1000);
textBox.text = "BINGO!!!";
}
执行结果:
知乎视频www.zhihu.com但是:与WaitForSeconds不同的地方是,我没法先new一个UnityYieldTest然后每次yield return这个实例。因为这样不会每次都执行构造函数,也就使得其中的thread等待执行不到。因此,unity的yield return机制并没有彻底搞清楚。
之所以研究Unity的yield return机制是因为在写网络相关代码的时候发现居然不能愉快地用httpclient和async/await这样已经比较流畅的语法。关于httpclient和unityWebRequest,可以参考这篇:c# - HttpClient和Unity的UnityWebRequest / WWW API之间的区别之所以研究Unity的yield return机制是因为在写网络相关代码的时候发现居然不能愉快地用httpclient和async/await这样已经比较流畅的语法。关于httpclient和unityWebRequest,可以参考这篇:
https://stackoverflow.com/questions/50160380/difference-between-httpclient-and-unitys-unitywebrequest-www-apistackoverflow.comSystem.Net
namespace. One of this is WebGL
. This means that HttpClient
will not even compile when you switch your platform to WebGL. UnityWebRequest
works fine with WebGL.主要问题是httpclient基于.Net Framework 4实现的,因此必须使用http://Systme.Net命名空间,而如果编译到不支持NETFramework的环境下,例如WebGL,就会发生一些问题。当然啦,这里还解释了UnityWebRequest的一些其它好处。
而使用UnityWebRequest就需要按照其官方示例方法,使用协程。对于这一点,我是有点小抗拒的,毕竟觉得这样的写法略不优雅,感觉有点小不舒服。不过跟从官方写法,这也是风险比较小的一条路线。