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
2
3
var go = gameObject != null ? gameObject : CreateNewGameObject();
// 也可使用隐式的 bool 转换运算符进行同样的检测
go = gameObject ? 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
2
if (!object.ReferenceEquals(monoBehaviour, null))
monoBehaviour.Invoke("Attack", 1.0f);

但是,如果目的是检查底层引擎对象的生命周期,推荐使用显式的 null 或 boolean 比较:[9]

1
2
3
4
5
if (monoBehaviour != null)
monoBehaviour.Invoke("Attack", 1.0f);
// 也可使用隐式的 bool 转换运算符
if (otherBehaviour)
otherBehaviour.Invoke("Attack", 1.0f);

参阅

有关此主题的更多详细信息,请参阅 Unity 博客文章 “Custom == operator, should we keep it?”. [10]

已翻译:Unity-Resharper-可能意外绕过Unity引擎对象的底层生命周期检查


  1. JetBrains 旧版 ReSharper Unity wiki 与当前 Inspectopedia 条目都将该问题归入 Unity 专用检查:https://github.com/JetBrains/resharper-unity/wiki/Possible-unintended-bypass-of-lifetime-check-of-underlying-Unity-engine-objecthttps://www.jetbrains.com/help/inspectopedia/Unity.NoNullCoalescing.html↩︎

  2. JetBrains Inspectopedia 的 Unity.NoNullCoalescingUnity.NoNullPropagation inspection 均属于 Unity inspection,目标是 UnityEngine.Object 派生类型上的 null 语义:https://www.jetbrains.com/help/inspectopedia/Unity.NoNullCoalescing.htmlhttps://www.jetbrains.com/help/inspectopedia/Unity.NoNullPropagation.html↩︎

  3. Microsoft Learn 说明 ???. / ?[] 的 null 判断语义;C# operator overloading 文档说明 x?.yx?[y]????= 不能重载。因此这些语法不会进入 UnityEngine.Object.operator == 的生命周期检查:https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-coalescing-operatorhttps://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operatorshttps://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/operator-overloading↩︎

  4. Unity 的 .NET in Unity 文档说明 Unity 脚本运行在 managed 环境中;Unity Object.Destroy 文档说明销毁的是 GameObject、Component 或 asset 等 Unity 对象;Unity Object 文档还描述了 C# wrapper 与 native object 分离的状态:https://docs.unity.cn/2020.1/Documentation/Manual/overview-of-dot-net-in-unity.htmlhttps://docs.unity.cn/Documentation/ScriptReference/Object.Destroy.htmlhttps://docs.unity3d.com/6000.0/Documentation/ScriptReference/Object.html↩︎

  5. Unity Scripting API 的 Object.operator == 文档说明,与 null 比较时会检查 managed object reference 和 native object pointer;UnityCsReference 中 operator ==operator != 和隐式 bool 转换最终走 CompareBaseObjectshttps://docs.unity3d.com/6000.0/Documentation/ScriptReference/Object-operator_eq.htmlhttps://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Scripting/UnityEngineObject.bindings.cs↩︎

  6. 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.htmlhttps://docs.unity.cn/6000.5/Documentation/Manual/programming-best-practices.html↩︎

  7. JetBrains Unity.NoNullCoalescing inspection 专门指出 Unity 对象上的 ?? 可能绕过底层 Unity engine object 的 lifetime check;Microsoft ?? 文档说明该运算符只按左操作数是否为 null 选择分支:https://www.jetbrains.com/help/inspectopedia/Unity.NoNullCoalescing.htmlhttps://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-coalescing-operator↩︎

  8. Microsoft Object.ReferenceEquals 文档说明它判断两个 object reference 是否为同一实例或是否都为 null;Unity Manual 将它列为检查真实 C# null 引用的方式,而不是 Unity native object liveness 检查:https://learn.microsoft.com/en-us/dotnet/api/system.object.referenceequalshttps://docs.unity.cn/6000.5/Documentation/Manual/programming-best-practices.html↩︎

  9. JetBrains Unity.NoNullPropagation inspection 指出 Unity 对象上的 null propagation / conditional access 可能绕过 Unity 生命周期检查;Microsoft member access operators 文档说明 ?. 只在左操作数非 null 时访问成员:https://www.jetbrains.com/help/inspectopedia/Unity.NoNullPropagation.htmlhttps://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators↩︎

  10. 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↩︎