由编辑器引发的思考,普通类是UnityEngine.Object?

​ 起因是我想在Unity的Inspector界面绘制一个普通类(类似每个Mono脚本的Script字段),一个普通类应该默认继承的是System.Object,而使用EditorGUILayout.ObjectField绘制的是一个UnityEngine.Object,该如何绘制呢?

我写了个测试代码如下:

1
2
3
4
5
6
7
public class NewClass 
{
public void Print()
{
Debug.Log("this NewClass");
}
}
1
2
3
4
public class NewMonoBehaviour2 : MonoBehaviour
{
public UnityEngine.Object obj;
}

此时我将新建的普通类拖上去居然能拖入!!!那么自己建的普通类属于UnityEngine.Object吗???

然后我又答应了一下这个obj所在的父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class NewMonoBehaviour2 : MonoBehaviour
{
public UnityEngine.Object obj;
private IEnumerator Start()
{
PrintInheritanceChain(obj.GetType());
yield return null;
}
public void PrintInheritanceChain(Type type)
{
Type baseType = type.BaseType;
if (baseType != null)
{
Debug.Log(" 继承 from: " + baseType.ToString());
PrintInheritanceChain(baseType); // 递归调用,打印父类的继承链
}
}
}

打印结果是:继承 form UnityEngine.TextAsset->继承 form UnityEngine.Object->继承 form System.Object

此时我怀疑的信了普通类也是继承了UnityEngine.Object,问了一下AI,都说普通类如果没有强声明继承System.Object,那就是隐氏继承了UnityEngine.Object

疑惑了一会我改了代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class NewMonoBehaviour2 : MonoBehaviour
{
public NewClass _newClass; //自定义类 不继承MonoBehaviour
public UnityEngine.Object obj;
private IEnumerator Start()
{
_newClass = new NewClass();
PrintInheritanceChain(_newClass.GetType());
yield return null;
}
public void PrintInheritanceChain(Type type)
{
Type baseType = type.BaseType;
if (baseType != null)
{
Debug.Log(" 继承 from: " + baseType.ToString());
PrintInheritanceChain(baseType); // 递归调用,打印父类的继承链
}
}
}

此时打印的只有一个,就是继承 form System.Object

查阅了资料最后得出结论

  • 不管是普通类还是继承了Mono的类,只要是C#文件,在编辑器中都会当成MonoScript文件来对待,把它当成Unity中可识别的资源(MonoScript - Unity 脚本 API),这里就解释了为什么一开始能拖到面板上,并且打印的是继承TextAssets,UnityObject…
  • 当在运行时,声明一个普通类需要通过new来声明,这个时候它是来自于.net内存中(先这么叫),所以就是继承System.Object

UnityEngine.Object和?. ??

首先微软没有提供?? ?. 这两的重写,所以UnityEngine.Object不支持,官方文档 Object - Unity 脚本 API

那么在如下代码为什么发现又是可行的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
public class NewMonoBehaviour2 : MonoBehaviour
{
public TestMono _testMono; //自定义脚本继承了MonoBehaviour
public BoxCollider2D _collider2D; //Unity中的Collider

private IEnumerator Start()
{
//物体添加了这个mono脚本和BoxCollider2D
_testMono ??= GetComponent<TestMono>(); //获取到了
_collider2D ??= GetComponent<BoxCollider2D>(); //没获取到
yield return null;
}
}

从结果看,大可以这么认为,自己的脚本继承Mono属于C#层,可以使用??,但是BoxCollider2D是Unity引擎的C++层,所以不适用??, 再加上雨松的一篇文章:Unity3D研究院之UnityEngine.Object和System.Object,就更确信无疑了。

但是我们知道继承的Mono也是来继承自UnityEngine.Object,官方说了不支持?? ?. 这里发现是支持的,还为自己找了借口,难道错了吗?

查看Unity 中的 .NET 概述 - Unity 手册中有说到

Unity C# 和 Unity C++ 共享 UnityEngine 对象

使用诸如 Object.DestroyObject.DestroyImmediate 等方法销毁 UnityEngine.Object 派生对象时,Unity 会销毁(卸载)原生对应对象。无法使用显式调用销毁 C# 对象,因为垃圾回收器会管理内存。一旦不再引用托管对象,垃圾回收器便会收集并销毁它。

如果再次访问已销毁的 `UnityEngine.Object,则 Unity 会为大多数类型重新创建原生对应对象。此重新创建行为的两个例外是 MonoBehaviourScriptableObject:一旦被销毁,Unity 便绝不会重新加载它们。

MonoBehaviour 和 ScriptableObject 会覆盖相等 (==) 和不相等 (!=) 运算符。因此,如果将销毁的 MonoBehaviour 或 ScriptableObject 与 null 进行比较,则当托管对象仍然存在且尚未进行垃圾收集时,运算符会返回 true。

因为 ???. 运算符不可重载,所以它们与从 UnityEngine.Object 派生的对象不兼容。在托管对象仍然存在的情况下对销毁的 MonoBehaviour 或 ScriptableObject 进行使用时,这些运算符不会返回与相等和不相等运算符相同的结果。

主要最后一句:因为 ???. 运算符不可重载,所以它们与从 UnityEngine.Object 派生的对象不兼容。在托管对象仍然存在的情况下对销毁的 MonoBehaviour 或 ScriptableObject 进行使用时,这些运算符不会返回与相等和不相等运算符相同的结果。

意思是如果对销毁的Mono进行?? ?.操作,不会返回正确的结果,代码如下

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
public class NewMonoBehaviour2 : MonoBehaviour
{
//自定义脚本继承了MonoBehaviour
public TestMono _testMono;
private IEnumerator Start()
{
//物体添加了这个mono脚本
_testMono ??= GetComponent<TestMono>();
Debug.Log(_testMono == null); //结果:false
yield return null;
//销毁 要等一帧 际的对象销毁操作始终延迟到当前更新循环结束,但始终在渲染前完成
//https://docs.unity.cn/cn/2020.3/ScriptReference/Object.Destroy.html
Destroy(_testMono);
yield return null;

Debug.Log(_testMono == null); //结果:true
_testMono ??= this.gameObject.AddComponent<TestMono>();
Debug.Log(_testMono == null); //结果:true

if (_testMono == null)
{
Debug.Log("testmono is null?"); //打印了该条信息
_testMono = this.gameObject.AddComponent<TestMono>();
Debug.Log(_testMono == null); //结果:false
}
}
}

从代码结果来看,销毁了之后通过??就是不对的结果。这里的销毁时Unity引擎对Mono C++上的销毁,但是C#托管代码还在(UnityEngine.Object重写了== !=,可以通过System.Object.ReferenceEquals(go, null)来判断是否还存在),此时用??判断结果是不为null,所以获取不到。

最后建议

  • 对于继承MonoBehaviour或者ScriptableObject +Unity内置类都不要使用?? ?. 只对自己使用的普通类使用