在 Unity3D 中总是要进行对象的销毁和Null 检查,虽然经常用,但是其中的一些细节,如果不太注意,很有可能就会踩坑,下面就总结一下 UnityEngine.Object
类型的销毁,以及空值检测相关问题。
Object.Destroy Vs Object.DestroyImmediate
Unity3D 提供了两种方式来销毁对象(GameObject/Component),分别是 Object.Destroy
和Object.DestroyImmediate
,两者区别主要有两点:
- Destroy 提供了延迟销毁参数,可以延迟
x
秒后进行销毁,即使不指定延迟参数,Destroy也将推迟到当前更新结束,渲染开始之前执行; - DestroyImmediate 调用立即销毁对象;
User Manual 中关于 Object.Destroy 的相关说明 : Actual object destruction is always delayed until after the current Update loop, but will always be done before rendering.
关于Unity3D的事件更新流程,可以参考用户手册.
1 | public class NewBehaviourScript : MonoBehaviour |
输出结果如下:
1 | G1 destroy? ::False |
Object 对象的 Null 检测
从 UnityEngine.Object
继承的对象,包括托管和非托管两部分,当调用 Destroy
时,销毁的只是非托管部分,托管部分只能通过 C# 的垃圾回收器进行回收,先看下面一个例子。
1 | public class Sample02 : MonoBehaviour |
输出结果:
1 | Is g1 destroy? True |
可以看出来,通过 g1 == null
来判断Object
是否被销毁,而通过ReferenceEquals
则没有办法判断,这是因为 g1
一直指向堆对象,除非我们显示的执行g1 = null
,ReferenceEquals(g1,null)
才能返回 ture
;
C# 引用类型默认也会调用ReferenceEquals
来比较相等性,那么为什么 g1 == null
却返回 true
呢,这是因为 基类UnityEngine.Object
重写了 operator==
操作符,具体的代码如下:
1 | public static bool operator==(Object x,Object y) |
这里顺便说一句,Object
类还重写了 operator bool
操作符,所以 if(g1){}
的含义是,当g1
非空时,执行相应的代码块。
接口类型的 Null 检测
在 C# 中,比较接口类型的相等性时,总是会采用RenferenceEquals
来判断,(对于值类型,需要先进行装箱,再进行比较),一般情况下,这点不会引起上面问题,但是当自定义的 MonoBehaviour
类实现某个接口时,如果对该接口类型进行相等性比较,其结果就可能不正确,还是看下面一个例子。
1 | public class Sample02 : MonoBehaviour, IGreet |
输出结果如下:
1 | Is obj destroy? True |
是不是对结果感到很奇怪,其根本原因是因为,对于接口类型调用operator==
,总是会调用 ReferenceEqual
进行比较,这一点可以通过反编译 IL
代码进行验证,而 sample02 == null
则会调用基类方法。
那么对于接口类型,应该如何判断接口对象是否被销毁了呢,很简单,只需要给接口添加个判断方法,还是上面的例子,修改后的接口如下:
1 | public class Sample02 : MonoBehaviour, IGreet |
在 UGUI 源码中,ICanvasElement 接口就是采用这种方法来实现,UIBehavior 类继承自 MonoBehavior,并实现了 ICanvasElement 接口
对于 Object 类型,避免使用 ?? 和 ?. 操作符
在用户手册中,明确说明了 Object 类型不支持 null-conditional operator (?.) 和 the null-coalescing operator (??),这是因为 Object 类型,并没有提供这两个操作符的重载,可能造成某些不正确的行为,看下面例子。
1 | public class Sample02 : MonoBehaviour |
输出结果:
1 | newGo == null : True |
产生这一结果的原因还是因为,operator ??
默认采用 ReferenceEquals
来做空判断,造成最后newGo
指向了已经销毁的 g1
。
虽然大多数情况下,使用operator ??
和operator ?.
也能得到正确的结果,但是最好避免使用。
总结
上面啰里啰嗦说了一大堆,其实主要想说的就四点:
- Destroy 是延迟销毁,销毁时机是当前帧更新结束,渲染开始之前;
- 除非变量显示设置为
null
,否则ReferenceEquals(obj,null)
总是返回false
; - C# 中的
interface
采用ReferenceEquals
进行比较,无论值类型还是引用类型; MonoBehavior
实现接口时,对于接口类型变量,避免使用interfaceTypeObj == null
来判断是否销毁;