最近在使用C#7的语法时遇到一个挺有意思的问题,深究其原理后发现值得写一篇文章。在C#中任何一个class类即使不写class XXX :System.Object 也会被默认继承System.Object,在C#中还有一个object关键字它和System.Object是一回事。所以UnityEngine.Object其实就是继承了System.Object的一个普通类对象,现在我们写一个普通的脚本
1 2 3 |
public class CustomScript : MonoBehaviour{} |
在写一个脚本,我们把它挂在一个空的游戏对象上,保证游戏对象提前没有被绑定过Camera组件和CustomSript组件,神奇的一目出现了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class Test : MonoBehaviour { void Start() { bool flag = GetComponent<Camera>() == null; bool flag1 = GetComponent<CustomScript>() == null; Debug.Log(flag + " " + flag1); // log: true , true flag = ((object)GetComponent<Camera>() == null); flag1 = ((object)GetComponent<CustomScript>() == null); Debug.Log(flag + " " + flag1); // log: false , true } } |
为什么(GetComponent<Camera>() == null)结果是true但是((object)GetComponent<Camera>() == null);结果就成了false了呢?而自己写的脚本CustomSript两次输出的结果都是一致的,显然系统自带的组件和自己写的脚本是有区别的,但是它们有什么区别呢?
通过打断点你可以发现即使游戏对象没有绑定Camera组件,其实 var a = GetComponent<Camera>() 也是不为空的,原因就出在GetComponent这里,它返回系统自带组件永远都不为空,而返回自定义脚本则根据情况返回空。其实Unity就是用了重载 == 、!=、bool运算符让你得到了看似正确的结果。接着我们也来写个类模拟一下它的原理类。
这里我们定义了一个class A类,并且重载了==和!=运算符,即使A对象不等于null也要同时满足isAlive等于true的情况下才视为A对象不为Null。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class A { public bool isAlive=false; private static bool CompareBaseObjects(A lhs, A rhs) { bool flag = (object)lhs == null; bool flag2 = (object)rhs == null; if (flag2 && flag) { return true; } if (flag2) { return !lhs.isAlive; } if (flag) { return !rhs.isAlive; } return false ; } public static bool operator ==(A x, A y) { return CompareBaseObjects(x, y); } public static bool operator !=(A x, A y) { return !CompareBaseObjects(x, y); } } |
来看代码吧,虽然a对象已经被new了,但是 (a==null)还是返回了true,只到我们设置了a.isAlive=true后,在判断(a==null)才返回false.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
A a = new A(); bool flag = (a == null); bool flag1 = ((object)a == null); Debug.Log(flag + " " + flag1); // log: true , false a.isAlive = true; flag = (a == null); flag1 = ((object)a == null); Debug.Log(flag + " " + flag1); // log: false , false |
接着在A类中继续重写bool运算符
1 2 3 4 5 6 |
public static implicit operator bool(A exists) { return !CompareBaseObjects(exists, null); } |
就可以if(a)来判断了,此时你会发现”A1″不会被打印出来而”A2″才会被打印出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
A a = new A(); if (a) { Debug.Log("A1"); } a.isAlive = true; if (a) { Debug.Log("A2"); } a.isAlive = true; |
所以这就解释为什么??符号在处理自己写的脚本不报错,而处理系统脚本就会报错。原因是GetComponent<系统自带脚本>永远都不会返回null,所以还是下面的例子,如果挂在空对象上就会发现Camera组件不会被正常添加,而CustomScript自己写的脚本会被正常添加。
1 2 3 4 |
var camera = GetComponent<Camera>() ?? gameObject.AddComponent<Camera>();//Camera组件不会被添加 var custom = GetComponent<CustomScript>() ?? gameObject.AddComponent<CustomScript>(); //CustomScript组件正常被添加 |
还有在处理夺命连环null的情况下
1 2 3 4 |
GetComponent<Camera>()?.gameObject.SetActive(true);//报错 GetComponent<CustomScript>()?.gameObject.SetActive(true);//不报错 |
会提示 MissingComponentException: There is no ‘Camera’ attached to the “XXXXXXXX” game object, but a script is trying to access it.错误。请注这并不是报错,而是被C++底层抛出一个叫MissingComponentException的异常,因为GetComponent<Camera>()并不为空,所以它可以试图去访问gameObject对象。
所以在使用 ?? 和 ?. 语法时,只要记住别使用在Unity系统自带的组件上就行,其他地方自己的脚本或者自己写的类都完全没问题。
- 本文固定链接: https://www.xuanyusong.com/archives/4713
- 转载请注明: 雨松MOMO 于 雨松MOMO程序研究院 发表
补充一点 当尝试访问一个被销毁的组件时,在Editor上会抛出MissingComponentException,但在真机(Android)上运行时,触发的Exception是NullReferenceException。注意这里组件的引用本身不为null, 只是被销毁了。
这篇文章有一定误导性,之前我已经在微博上给博主提了,但是博主貌似没有完全理解问题的关键。
问题不在于内置组件GetComponent返回是否是空。而是在于==null这个写法的语义。
Unity官方重写了所有UnityEngine.Object的==nul和!=null。==null 不仅会判断C#对象是否是null,也会判断底层的C++对象是否Destroy。
就是说如果你写了if(go != null)(go可以是gameObject,也可以是任何MonoBehaviour),那么只要go destroy了,C#这边的引用是否置空就无关紧要了,下面的代码都不会执行。
而现在?.和??这类新特性,只会判断C#的引用是否置空,不会考虑C++底层的对象是否已经Destroy。你也别想override这几个操作符,C#不允许你override他们。
如果在项目中批量的将大量==null替换为.?写法,会造成原来不会走到的分支被走到、并且触发异常。
做这种修改前请一定小心仔细并仔细测试。个人建议是不要在GameObject和MonoBehaviour上使用这几个操作符,除非你非常确定自己置空了所有引用。
Unity的这个特性今天看来有局限性,当使用泛型方法的时候还有更大的坑。官方对此也非常纠结。但是为了兼容老的代码短期内应该不会修改。
如果你使用Rider作为IDE,这种写法会直接报警,仔细查看说明确保你知道自己在做什么。
参考链接:
https://issuetracker.unity3d.com/issues/c-number-6-null-conditional-access-operator-dot-throws-missingcomponentexception-instead-of-recognizing-as-null
https://forum.unity.com/threads/unity-2017-and-null-conditional-operators.489176/
https://github.com/JetBrains/resharper-unity/wiki/Possible-unintended-bypass-of-lifetime-check-of-underlying-Unity-engine-object
感谢这么好回复, 我仔细研究下。