前言
最近在写文件操作类的时候,我希望使用封装程度更高的类来定义和开启协程,通过这种提供更高可复用性的方式来异步完成复制文件和读取文件的操作,却接连出现问题。下面是这些问题的记录,虽然没能想出什么十分巧妙的解决方案,但至少写下来它的正确用法和注意事项可以加深我和读者对于 Unity 协程使用的理解:
尝试 1:不继承 MonoBehaviour
类
对于文件类操作,由于其不是对游戏内的对象本身进行的操作,且可复用性极高,因此我一开并不打算让这个类继承 MonoBehaviour
类,而是单独封装为一个静态类。
但很可惜,StartCoroutine()
函数作为 MonoBehaviour
类的成员,必须要通过继承了 MonoBehaviour
类的对象进行调用。而且脚本必须被挂载到某个游戏内的 entity 上才会生效,因此每个脚本文件中必须有和文件名相同的继承了 MonoBehaviour
类的类。
实际上这个问题并不太有什么解决的必要,因为到了这一步,我们就会发现封装静态类的必要不是很明显了。而且接下来还有更多无法这么做的理由。
尝试 2:封装为静态方法
于是我开始希望把这个类的方法封装为静态方法,这样可以通过不实例化来进行调用。在添加了 static
修饰符之后,我收到了如下报错信息:
An object reference is required for the non-static field, method, or property
各位可以看到这条报错的重点在于 non-static。没错,协程的迭代器函数是不能作为静态成员进行调用的。
于是,它只能作为非静态类的非静态成员函数被定义。
尝试 3:通过 new MonoBehaviour()
空对象调用
那么把它封装到一个不继承 MonoBehaviour
的类中如何呢?反正只有 StartCoroutine()
这个方法是需要由继承自 MonoBehaviour
的类的对象来进行调用,而使用 new MonoBehaviour()
即可实例化一个新的空对象,是不是用这个空对象就可以了?
NullReferenceException
然而事实上这依然不行,这条报错信息告诉我们实例化的对象不存在。
现在我们需要注意到一个事实:new MonoBehaviour()
语句所实例化的对象并不是真正的 Unity entity,而是托管在 C# 中的虚空的存在。
然而协同程序由协程调度程序运行,并且绑定到用于启动协程的 MonoBehaviour
对象。StartCoroutine()
是 MonoBehaviour
的实例成员。
因此它总是需要依托于一个确实存在在 Unity 中的 GameObject
对象,我们总是需要通过 AddComponent<>()
来完成类通过 Unity 组件进行的实例化才能正常调用,当然不想进行实例化的话就只能直接把开启协程的函数定义在已经挂载上的类中。
较为优雅的做法
不过大家应该也不难发现了,我们上面一直在讨论 StartCoroutine()
这么一个方法。事实上,这个方法的正确使用方式也是本篇博客所讨论的问题的主要限制,而我们定义的协程迭代器函数的主体是可以定义为静态方法的。
因此,一个较为整洁的写法可以是:
- 将协程迭代器封装成一个静态方法
- 将开启协程的入口写在继承自
MonoBehaviour
的与脚本文件名同名的类中 - 不挂载脚本,每次开启协程前动态
AddComponent
- 将上一步实例化产生的用于开启协程的组件的引用传入协程主体
- 在协程
yield break
之前把开启了自己的组件的引用Destroy()
掉,回收内存