以往我在做空判断的时候,都是这样写的:
1 | if (gameobject != null) |
或者是
1 | if (gameobject == null) return; |
最近换了Rider, 他给我来了句提示与 'null' 的比较开销较大
,奇奇怪怪,我换成了 if(gameobject)
就没有这些提示了,那么这两种写法到底有什么区别呢?
要讨论这两个问题,还真不是很简单,我们首先要了解Object
,这里的Object
是System.Object
,也是UnityEngine.Object
。
System.Object
大家都很好理解,我们来看看UnityEngine.Object
吧。
UnityEngine.Object
继承自system.Object
,是Unity
所涉及所有物体的基类。
但是他们也有区别,不得不谈的就是销毁
和空判断
。
这是最常见的问题,在UnityEngine中我们销毁一个对象时,使用“==”操作符与null比较结果为true,但是可以发现,我们还是可以引用这个变量,这主要是由于UnityEngine的==操作符在销毁后即将Object置位null,但是对象本身需要被GC后才能达到真正意义上的销毁,也就是System.Object中的null;
这里涉及到了托管的问题,如果不了解也不要紧,我可以告诉你从 UnityEngine.Object 继承的对象,包括托管和非托管两部分,当调用 Destroy 时,销毁的只是非托管部分,托管部分只能通过 C# 的垃圾回收器进行回收。
什么意思呢,我们直接来看Destroy
会更清晰一点
1 | /// <summary> |
Unity 自己的文档讲的很清楚了, 我们来看“了解托管堆”这一节
垃圾回收器定期运行(__注意:__具体运行时间视平台而定)。这时将扫描堆上的所有对象,将任何不再引用的对象标记为删除。然后会删除未引用的对象,从而释放内存。
Unity 用了一种很讨巧的方法,他们把 destroy
掉的内存标记为 'null'
,事实上,这些内存并没有被回收,而是在等待 C# 的 GC
把他们回收掉。
我们再来看看Unity
对于==
的重载
1 | public static bool operator ==(Object x, Object y) => Object.CompareBaseObjects(x, y); |
还有其对布尔类型的重载
1 | public static implicit operator bool(Object exists) => !Object.CompareBaseObjects(exists, (Object) null); |
可以发现CompareBaseObjects
方法的实现也是绕不开的,我们来看看
1 | private static bool CompareBaseObjects(Object lhs, Object rhs) |
看到这里,其实我们已经对之前的问题有了回答了
UnityEngine.Object
中的布尔类型重载和==
运算符重载,他们都是通过CompareBaseObjects
来实现的
他们俩本质上都是采用了 Unity
层面上的检查。
对于这点,我们也可以证明
请看方法IsNativeObjectAlive
1 | private static bool IsNativeObjectAlive(Object o) |
Unity
先调用o.GetCachedPtr()
方法检查目标对象的本机指针是否还存在, 要知道 IntPtr
并非 Unity
自己鼓捣出来的玩意, 他是.NET Framework
和 .NET Core
中的一个特殊的结构体,用于表示目标对象的本地指针. 如果本地指针不存在,那么就会调用Object.DoesObjectWithInstanceIDExist
方法,这个方法会检查目标对象的实例ID是否存在,如果存在具有给定实例ID的对象,则返回 true.
通过这种方法实现的空判断,实质上开销是特别大的,有些人会使用System.Object.ReferenceEquals
,比如这样
1 | if(System.Object.ReferenceEquals(gameobject, null)) |
这样做,在性能上确实是有很大提升的,但是读到这里的朋友估计已经猜到我要讲什么了.
在执行destroy
方法后,目标对象实质上并没有被回收,他只是被打上了一个'tag'
的标签,而变量仍然在指向着堆对象.为什么==
可以呢,因为Unity
已经把他重载了.