Unity-Resharper-可能意外绕过Unity引擎对象的底层生命周期检查
(Ryuu: 附上原文地址 : Possible unintended bypass of lifetime check of underlying Unity engine object · JetBrains/resharper-unity Wiki)[1]
这是 Unity 特定的检查。此检查仅在 Unity 项目中运行。[2]
若从 UnityEngine.Object 派生的类型使用空合并 (??) 或空传播或条件 (?.) 运算符,则会显示此警告。 这些运算符不会使用 UnityEngine.Object 上声明的自定义相等运算符,将绕过 Unity 原生(native)对象的存活检测。 为了阐明意图,最好使用显式 null 或 bool 比较,或调用 System.Object.ReferenceEquals()。[3]
详情
从 UnityEngine.Object 派生的类型是托管 .NET 对象,在 C# 脚本中用于表示与使用原生 Unity 引擎对象。这两种类型的对象具有不同的生命周期。托管的 .NET 对象在没有更多引用时被垃圾收集,而本地 Unity 引擎对象在加载新场景或通过显式调用 UnityEngine.Object.Destroy() 时被销毁。这意味着托管的 .NET 对象指向的原生对象可能已被销毁。 [4]
UnityEngine.Object 类定义了自定义相等运算符,当与 null 进行比较时,这些运算符将检查底层原生 Unity 引擎对象是否已被破坏。换句话说, myMonoBehaviour == null 将检查是否已分配 myMonoBehaviour 变量,并且还将检查原生引擎对象是否已被销毁。可以使用布尔比较执行相同的检查,例如 if (myMonoBehaviour == true) 或 if (!myMonoBehaviour) 或是 if (myMonoBehaviour)。[5]
如果使用空合并或条件运算符,则表意不明确,并且可能绕过预期的生命周期检查。如果打算进行生命周期检查,推荐使用与 null 或布尔比较的显式比较。若不打算进行生命周期检查,请调用 System.Object.ReferenceEquals() 以明确表意。注意,对 object.ReferenceEquals() 的调用被编译器优化为简单的空检查,比调用自定义相等运算符更快。[6]
空合并运算符
以下示例的表意不明确:是检查 gameObject是否正确引用,还是检查原生 Unity 引擎对象是否已销毁?
1 | var go = gameObject ?? CreateNewGameObject(); |
若目的是检查底层引擎对象的生命周期,则此代码不正确,因为生命周期检查被绕过。使用显式 null 或 boolean 比较修复代码:[7]
1 | var go = gameObject != null ? gameObject : CreateNewGameObject(); |
若目的是确保 gameObject 变量已被初始化并分配了有效的 C# 引用,推荐使用显式调用 object.ReferenceEquals():[8]
1 | return !object.ReferenceEquals(gameObject, null) ? gameObject : CreateNewGameObject(); |
虽然这种更改稍显冗长,但表意十分明确。
空条件运算符
以下示例的表意同样不明确:
1 | monoBehaviour?.Invoke("Attack", 1.0f); |
同样的,如果目的是简单地检查 monoBehaviour 变量是否已正确初始化与引用,推荐使用显式调用 object.ReferenceEquals():
1 | if (!object.ReferenceEquals(monoBehaviour, null)) |
但是,如果目的是检查底层引擎对象的生命周期,推荐使用显式的 null 或 boolean 比较:[9]
1 | if (monoBehaviour != null) |
参阅
有关此主题的更多详细信息,请参阅 Unity 博客文章 “Custom == operator, should we keep it?”. [10]
已翻译:Unity-Resharper-可能意外绕过Unity引擎对象的底层生命周期检查
JetBrains 旧版 ReSharper Unity wiki 与当前 Inspectopedia 条目都将该问题归入 Unity 专用检查:https://github.com/JetBrains/resharper-unity/wiki/Possible-unintended-bypass-of-lifetime-check-of-underlying-Unity-engine-object、https://www.jetbrains.com/help/inspectopedia/Unity.NoNullCoalescing.html。 ↩︎
JetBrains Inspectopedia 的
Unity.NoNullCoalescing与Unity.NoNullPropagationinspection 均属于 Unity inspection,目标是UnityEngine.Object派生类型上的 null 语义:https://www.jetbrains.com/help/inspectopedia/Unity.NoNullCoalescing.html、https://www.jetbrains.com/help/inspectopedia/Unity.NoNullPropagation.html。 ↩︎Microsoft Learn 说明
??、?./?[]的 null 判断语义;C# operator overloading 文档说明x?.y、x?[y]、??、??=不能重载。因此这些语法不会进入UnityEngine.Object.operator ==的生命周期检查:https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-coalescing-operator、https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators、https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/operator-overloading。 ↩︎Unity 的 .NET in Unity 文档说明 Unity 脚本运行在 managed 环境中;Unity
Object.Destroy文档说明销毁的是 GameObject、Component 或 asset 等 Unity 对象;UnityObject文档还描述了 C# wrapper 与 native object 分离的状态:https://docs.unity.cn/2020.1/Documentation/Manual/overview-of-dot-net-in-unity.html、https://docs.unity.cn/Documentation/ScriptReference/Object.Destroy.html、https://docs.unity3d.com/6000.0/Documentation/ScriptReference/Object.html。 ↩︎Unity Scripting API 的
Object.operator ==文档说明,与null比较时会检查 managed object reference 和 native object pointer;UnityCsReference 中operator ==、operator !=和隐式bool转换最终走CompareBaseObjects:https://docs.unity3d.com/6000.0/Documentation/ScriptReference/Object-operator_eq.html、https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Scripting/UnityEngineObject.bindings.cs。 ↩︎JetBrains 的 Unity null inspections 和 Unity Manual 的 best practices 都强调区分 Unity null 检查与真实 C# null 检查;Unity 生命周期检查用
obj == null/obj != null,真实 C# 引用检查用object.ReferenceEquals或等价的 CLR 引用语义:https://www.jetbrains.com/help/inspectopedia/Unity.NoNullCoalescing.html、https://docs.unity.cn/6000.5/Documentation/Manual/programming-best-practices.html。 ↩︎JetBrains
Unity.NoNullCoalescinginspection 专门指出 Unity 对象上的??可能绕过底层 Unity engine object 的 lifetime check;Microsoft??文档说明该运算符只按左操作数是否为 null 选择分支:https://www.jetbrains.com/help/inspectopedia/Unity.NoNullCoalescing.html、https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-coalescing-operator。 ↩︎Microsoft
Object.ReferenceEquals文档说明它判断两个 object reference 是否为同一实例或是否都为null;Unity Manual 将它列为检查真实 C# null 引用的方式,而不是 Unity native object liveness 检查:https://learn.microsoft.com/en-us/dotnet/api/system.object.referenceequals、https://docs.unity.cn/6000.5/Documentation/Manual/programming-best-practices.html。 ↩︎JetBrains
Unity.NoNullPropagationinspection 指出 Unity 对象上的 null propagation / conditional access 可能绕过 Unity 生命周期检查;Microsoft member access operators 文档说明?.只在左操作数非 null 时访问成员:https://www.jetbrains.com/help/inspectopedia/Unity.NoNullPropagation.html、https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators。 ↩︎Unity 官方博客 Custom == operator, should we keep it? 解释了 fake null、C# wrapper 与 C++ native object 生命周期分离,以及移除自定义
==的兼容性风险;当前 Unity 博客地址为:https://unity.com/blog/engine-platform/custom-operator-should-we-keep-it。 ↩︎