看一段代码:
Object a = new Object();
Debug.Log(a);
var b = a;
Debug.Log(b);
DestroyImmediate(a);
Debug.Log(b);
输出为三个null
当然一开始a 和 b 不应该是null。这儿输出为null只是因为它们指向的object的ToString()返回"null"而已。
同样如果使用 == 或 != 去比较 a,也会得到a指向null的假象。在Unity的逻辑中,这个没啥用的空Object就是null。
然后把上面的Object换成GameObject再看一下:
GameObject a = new GameObject();
Debug.Log(a);
var b = a;
Debug.Log(b);
DestroyImmediate(a);
Debug.Log(b);
这次前面两个log输出正常的对象字符串表示,而最后一个Log输出null。原因和上面一样。只是一开始GameObject没有被视为null,只有当Destroy之后,才被视作null。但是这给你一个假象:当多个引用指向同一个GameObject时,如果该GameObject被Destroy,则所有指向它的引用都被置为null。多么神奇的操作啊!在c++中应该是产生一堆野指针。而在c#这种使用GC的语言中,我们其实并不能主动去"Destroy"一个对象,只能把所有的引用都置为null,最终通过GC来释放对象。但是Unity搞了一个神奇的操作,通过Destroy把对象的内部状态设置为null(所谓的fake null),然后通过重载 ==, !=, ToString()等操作符和函数,让外部认为所有指向这个对象的引用是指向null。那么这样的对象,什么时候真正被从堆上删除呢?我猜是GC扫描的时候,fake null同样起作用,对垃圾回收器说我是空对象,来收我啊。。
那么这样好吗?
首先,这么做很Unity,非常符合Unity的设计哲学。为了游戏不容易崩溃,降低编程门槛,开辟更大的市场,Unity这么做已经成功了。但是这么做也有隐患。比如把最后一句Log改成这样:Debug.Log(b?.name);
这会产生一个异常:MissingReferenceException: The object of type 'GameObject' has been destroyed but you are still trying to access it.
因为?.操作是真的在比较引用,如果引用存在就会执行.name。虽然Unity想各种方法让引用看起来是null,但是这儿并不起作用。
那么我们应该怎么做更好?我觉得不能依赖于Unity的fake null功能,如果对象被Destroy,该置空的引用应该手动置空,这样更安全,而且也容易养成好的编程习惯,这样在你迁移到非Unity平台之后,才不至于出问题。