最近在使用C#7的语法时遇到一个挺有意思的问题,深究其原理后发现值得写一篇文章。在C#中任何一个class类即使不写class XXX :System.Object 也会被默认继承System.Object,在C#中还有一个object关键字它和System.Object是一回事。所以UnityEngine.Object其实就是继承了System.Object的一个普通类对象,现在我们写一个普通的脚本
public class CustomScript : MonoBehaviour{}
在写一个脚本,我们把它挂在一个空的游戏对象上,保证游戏对象提前没有被绑定过Camera组件和CustomSript组件,神奇的一目出现了。
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。
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.
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运算符
public static implicit operator bool(A exists)
{
return !CompareBaseObjects(exists, null);
}
就可以if(a)来判断了,此时你会发现”A1″不会被打印出来而”A2″才会被打印出来。
A a = new A();
if (a)
{
Debug.Log("A1");
}
a.isAlive = true;
if (a)
{
Debug.Log("A2");
}
a.isAlive = true;
所以这就解释为什么??符号在处理自己写的脚本不报错,而处理系统脚本就会报错。原因是GetComponent<系统自带脚本>永远都不会返回null,所以还是下面的例子,如果挂在空对象上就会发现Camera组件不会被正常添加,而CustomScript自己写的脚本会被正常添加。
var camera = GetComponent<Camera>() ?? gameObject.AddComponent<Camera>();//Camera组件不会被添加
var custom = GetComponent<CustomScript>() ?? gameObject.AddComponent<CustomScript>(); //CustomScript组件正常被添加
还有在处理夺命连环null的情况下
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
感谢这么好回复, 我仔细研究下。