前些天遇到的一个问题:
在lua侧有个方法是用来检测gameobject是否为空的,代码如下:
这个方法在被某段代码调用时会偶发的报错,报错内容如下:
在某次报错时,从断点中看到如下信息:
从这里能看出:
1)首先这个object在lua看来不是空的,即不是nil
2)但是这个object又确实是空的,里面没有本该有的数据(而且ide也很贴心的提示了invalid C# object)
查了下xLua的FAQ,发现是unity对C# object的判空操作进行了重载,简而言之是这样的:当一个object为null时,unity出于让unity开发者使用起来更方便的考虑,在这个null object中加入许多辅助信息(比如堆栈信息之类的),此时虽然unity的判空操作认为这个object为空,但实际上并不是C#意义上的真正的空对象,此时lua就把这个伪空对象包装了一下传到lua侧使用。而Equals这个方法只会判断它的参数是不是空的,并不会判断自己所属的这个对象是不是空的,于是就出现了nullobject.Equals(param)的情况,就导致了报错。xLua也给出了解决方案:
下面展开讲一下上面段落里标注的文本
首先我们要知道虽然Unity使用C#开发,但Unity本身是C/C++的,于是object就有两套,一套是C++的,一套是C#的。C++那套是存储了所有数据的,而C#那套是对C++ object进行了封装,里面存储的是指向C++的指针。C#中对Unity引擎代码的调用(比如object.SetActive())都会在C++侧被执行。
两套object有两套生命周期,一套由C#的托管堆控制,另一套则在C++中由代码控制。举个例子:当Destroy方法被调用来销毁一个物体时,这个物体在C++侧存储的数据会被销毁(实际上Destroy被调用时只是给object设置了一个销毁标记,真正的销毁操作要等到update之后render之前。所以想要知道一个object是否被销毁要在update之后进行判断),这个物体上的C#组件将被移除,但这些组件仍会被C#侧持有,直到C#触发gc销毁这些组件。所以就会出现上面那样的问题:当一个C++ object被销毁以后,又去访问这个object,此时由于C#中的object还没有被销毁Unity就返回了一个伪空对象。
一些展开内容
1.判断是否相同的几个方法
查上面的资料的时候,看到了一些介绍unity对比较是否相等的操作进行重载的内容,这里也大概记一下。先讲一下C#的三个方法:
1)ReferenceEquals
public static bool ReferenceEquals (object objA, object objB);
这个方法是object中的一个静态方法并且不可被重写,它需要两个object类型的参数,最后返回一个bool值表示两个参数是否拥有相同的内存地址
2)Equals
public virtual bool Equals (object obj);
Equals也是object中的方法,不同的是它是个虚方法,它对于引用类型会默认比较持有的引用是否相同,对于值类型会默认比较各个字段。它只接受一个参数,所以只会判断参数是否为空,而不会判断自身,如果自身为空的话会抛出NullReferenceException的异常。同时也要注意参数需要与自身类型一致。
3)==
== 默认用于内置的值类型比较,如果想要应用于别的类型则需要重载。注意重载时 == 和 != 要成对存在。
这些方法在Unity中的表现:
unity在UnityEngine.Object中重载了 ==,所以继承自UnityEngine.Object的类型都会使用该方法。对于别的类型则会使用C#默认的 ==。由于unity在重载 == 的逻辑中添加了自定义的空检查逻辑:除了检查object是否真的为空以外,还会检查这个object是不是伪空对象,导致使用 == 进行比较运算时会比较耗时。而另外两种方法则和C#中的没有区别。
2.关于?.、?[]、??、??=
unity只是重载了 == ,并没有去重载这个操作符,所以尽管这些操作符也有空检查但却是C#的意义上的,是不走unity自定义的空检查的(别的资料大多表述为这些操作符会跳过unity的生命周期检查),所以使用这些操作符可能会和使用 == 得出不同的结果。
【一些参考资料】
【1】xLua下调用GetComponent时返回值不是nil的坑
【2】xLua无法判断Unity对象为nil的问题
【3】Unity’s Scripting Duality and Object Destruction
【4】Custom == operator, should we keep it?
【5】Unity-自定义==运算符,我们应该保留它吗 (【4】的翻译)
【6】Unity-UnityEngine.Object的自定义比较
【7】Possible unintended bypass of lifetime check of underlying Unity engine object
【8】Unity-Resharper-可能意外绕过Unity引擎对象的底层生命周期检查(【7】的翻译)