unity协程_Unity中的Yield Return是个什么鬼?

unity协程_Unity中的Yield Return是个什么鬼?_第1张图片

写完以后发现了这篇博文,看完觉得自己太浅薄了。

Coroutine,你究竟干了什么?_tkokof1的专栏-CSDN博客​blog.csdn.net
unity协程_Unity中的Yield Return是个什么鬼?_第2张图片

好多年没怎么用Unity,回过头来一看已经2020版本了,哈哈,当年最早还是用1.7版本,一帮菜鸟级码农在出租屋里写着大量if嵌套的丑陋代码,后来自己跟着一位大神开始C#+Unity之不归路,当时还是3.5版本,而自己做了个小项目的时候升到了4.3。真是神奇的旅程。

然而Unity不变的还是StartCoroutine和Yield Return这种古老的神仙级写法。

查阅了度娘,对于yield return究竟是什么鬼,有几篇文章涉及到其用法,但是并没有深挖其根源。而在当今async/await模式和Task已经大行其道的时候,是否还有必要使用yield return,确实还是值得略作研究的。

YIELD是什么东东

首先我们看看码农世界以外早就存在的yield:

unity协程_Unity中的Yield Return是个什么鬼?_第3张图片

当年跟小老板去美帝,两人胆子也算肥的,老板英文一般般,我驾照都没有,两眼一抹黑就上了路,在很多地方看到“U-Turn Yield”之类标记。对于U-Turn,我们在出发前已经研究过了,就是掉头嘛!如果干脆是一个红色圆圈圈中间一个斜杠杠,这倒好理解了,但是很多地方没有这斜杠杠,而是写了“u-turn yield to ...”这样的文字,我们就懵了,也不敢掉头,一直等到后面车不耐烦滴喇叭,才一咬牙掉了过去。

回来后研究了,原来就是不禁止掉头,但是必须礼让......所以“Yield”实际上翻译过来应该是“礼让三先”或者“宁挺三分不抢一秒”的意思。

所以回到编程世界,我相信C#之父当年构造yield语法的时候必然是开过车的.....

yield return是Unity发明的吗?

那么yield return是Unity发明的吗?

嗯,但凡有C#驾照的码驴们应该都知道IEnumerable和IEnumerator。如果我们自己写了一个类似Array或者List这样的集合型数据结构,并且试图用foreach遍历这个类的时候,系统就会告诉你,没有实现IEnumerable,不许开车。

所以很明显,yield return是C#语言层次的东西。

“这也能说明yield关键字其实是一种语法糖,最终还是通过实现IEnumberable、IEnumberable、IEnumberator和IEnumberator接口实现的迭代功能。”
c# yield关键字原理详解 - blueberryzzz - 博客园​www.cnblogs.com
unity协程_Unity中的Yield Return是个什么鬼?_第4张图片

unity下的Coroutine和yield return

在一篇文章中我们看到,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,或者其它类型,则语法检测根本通不过。

unity协程_Unity中的Yield Return是个什么鬼?_第5张图片

注意,这里var result的类型,可以看到是Coroutine,而这个Coroutine本质上应该就是一个迭代器(不知道我推测是否准确),但是Unity没有进一步通过Coroutine对外暴露IEnumerator,因此我们拿到的这个result对象啥都不能做:

unity协程_Unity中的Yield Return是个什么鬼?_第6张图片
IEnumerator对外暴露了一个Current因此能够获取return返回值,而Coroutine啥都没有了

因此,StartCoroutine作用其实就是启动运行一个Coroutine协程,而Coroutine则是Untiy的发明,继承自YieldInstruction,并且具有NativeHeader和RequiredByNativeCode属性标签。至此,我们已经无法继续往下看Coroutine和这个YieldInstruction里面有什么(YieldInstruction我们后面再说)。

unity协程_Unity中的Yield Return是个什么鬼?_第7张图片
Coroutine没有对外返回IEnumerator

unity协程_Unity中的Yield Return是个什么鬼?_第8张图片
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协程_Unity中的Yield Return是个什么鬼?_第9张图片

呵呵,看来Unity并不希望在Coroutine上面做更多文章啦。大致猜测Unity就是基于C#的Yield Return机制来实现的Coroutine,而这个Coroutine目的是实现类似于多线程或者当今的async/await来提高耗时任务的处理效率,但却不想把语法搞得太复杂,以免降低整体代码框架的稳定性。

yield return WaitForSeconds又是什么

好,前面我们已经知道yield return后面跟什么返回值并没有差异,但是为什么在unity中会有WaitForSeconds,WaitForEndOfFrame或者www呢?

仔细看,yield return WaitForSeconds正确的语法实际上是:

yield return new WaitForSeconds(seconds);

这里有个new关键字。因此我们就明白了,WaitForSeconds其实是unity命名空间下的一个class而已。

unity协程_Unity中的Yield Return是个什么鬼?_第10张图片
等待时长的参数在构造器入口传入

从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机制并没有彻底搞清楚

为什么不用httpclient?

之所以研究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-api​stackoverflow.com
  • Some platforms don't support anything from the System.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就需要按照其官方示例方法,使用协程。对于这一点,我是有点小抗拒的,毕竟觉得这样的写法略不优雅,感觉有点小不舒服。不过跟从官方写法,这也是风险比较小的一条路线。

你可能感兴趣的:(unity协程,unity打断点直接卡住)