首页 >Unity3D频道 >【Unity3D研究院之游戏开发】 > Unity3D研究院之UnityEngine.Object和System.Object(一百一十五)
2020
04-17

Unity3D研究院之UnityEngine.Object和System.Object(一百一十五)

最近在使用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的情况下

8b0d88d5ly1gdvvpf0sl2j215d0u0tdc

        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系统自带的组件上就行,其他地方自己的脚本或者自己写的类都完全没问题。

最后编辑:
作者:雨松MOMO
专注移动互联网,Unity3D游戏开发
捐 赠写博客不易,如果您想请我喝一杯星巴克的话?就进来看吧!

Unity3D研究院之UnityEngine.Object和System.Object(一百一十五)》有 3 条评论

  1. SunHowe说:

    补充一点 当尝试访问一个被销毁的组件时,在Editor上会抛出MissingComponentException,但在真机(Android)上运行时,触发的Exception是NullReferenceException。注意这里组件的引用本身不为null, 只是被销毁了。

  2. pankycat说:

    这篇文章有一定误导性,之前我已经在微博上给博主提了,但是博主貌似没有完全理解问题的关键。
    问题不在于内置组件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

留下一个回复

你的email不会被公开。 必填项已用 * 标注。