Last active
March 30, 2024 11:32
-
-
Save vangogih/02845828a8da824047ca8a9be71e1b28 to your computer and use it in GitHub Desktop.
Unity Ungine object comparison problem and solution.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// To test it just put this class inside PlayMode assembly in Unity Editor | |
using System.Collections; | |
using NUnit.Framework; | |
using Tests.Playmode.Stubs; | |
using UnityEngine; | |
using UnityEngine.TestTools; | |
using Object = UnityEngine.Object; | |
namespace Tests.Playmode.Stubs | |
{ | |
public interface IInterface | |
{ | |
string StringValue { get; set; } | |
void CallAnyClassMember(); | |
} | |
public class ClassicMonoBeh : MonoBehaviour, IInterface | |
{ | |
public string StringValue { get; set; } = "Hi, I'm classic MonoBeh!"; | |
public void CallAnyClassMember() | |
{ | |
var _ = gameObject; // Here we try to call any MonoBehaviour member | |
} | |
} | |
public struct CLRStruct : IInterface | |
{ | |
public string StringValue { get; set; } | |
public CLRStruct(string _) | |
{ | |
StringValue = "Hi, I'm small CLR class!"; | |
} | |
public void CallAnyClassMember() | |
{ | |
var _ = StringValue; | |
} | |
} | |
public class CLRClass : IInterface | |
{ | |
public string StringValue { get; set; } = "Hi, I'm CLR class!"; | |
public void CallAnyClassMember() | |
{ | |
var _ = StringValue; | |
} | |
} | |
} | |
namespace Tests.Playmode | |
{ | |
public sealed class NullInterfaceTest | |
{ | |
private GameObject m_root; | |
[UnitySetUp] | |
public IEnumerator SetUp() | |
{ | |
m_root = new GameObject("Root"); | |
yield return null; | |
} | |
[UnityTearDown] | |
public IEnumerator TearDown() | |
{ | |
Object.DestroyImmediate(m_root); | |
yield return null; | |
} | |
[Test] | |
public void CLRObject_Target_Success() | |
{ | |
var target = new CLRClass(); | |
var targetInterface = (IInterface)target; | |
target = null; | |
// Target goal | |
{ | |
Assert.IsTrue(target == null); | |
Assert.IsFalse(targetInterface == null); | |
Assert.DoesNotThrow(targetInterface.CallAnyClassMember); | |
} | |
} | |
[UnityTest] | |
public IEnumerator MonoBehaviour_Problem_FalseNegativeAsInterface() | |
{ | |
var targetMonoBeh = m_root.AddComponent<ClassicMonoBeh>(); | |
var targetInterface = (IInterface)targetMonoBeh; | |
Object.DestroyImmediate(targetMonoBeh); | |
yield return null; | |
// Expected | |
{ | |
Assert.IsTrue(targetMonoBeh == null); | |
} | |
// Actual | |
{ | |
Assert.IsFalse(targetInterface == null); // PROBLEM #1: false-negative result on interface object comparison | |
} | |
} | |
[UnityTest] | |
public IEnumerator MonoBehaviour_Problem_MissingRefExOnCallingInterface() | |
{ | |
var targetMonoBeh = m_root.AddComponent<ClassicMonoBeh>(); | |
var targetInterface = (IInterface)targetMonoBeh; | |
var targetClrClass = new CLRClass(); | |
var targetClrClassInterface = (IInterface)targetClrClass; | |
Object.DestroyImmediate(targetMonoBeh); | |
targetClrClass = null; | |
yield return null; | |
Assert.DoesNotThrow(targetClrClassInterface.CallAnyClassMember); | |
Assert.DoesNotThrow(() => | |
{ | |
var _ = targetClrClassInterface.StringValue; | |
}); | |
// MissingReferenceException if we call member who calls MonoBehavior part | |
Assert.Throws<MissingReferenceException>(targetInterface.CallAnyClassMember); | |
// BUT because string is a part of CLR type, not MonoBehaviour. Obvious, isn't it? | |
Assert.DoesNotThrow(() => | |
{ | |
var _ = targetInterface.StringValue; | |
}); | |
} | |
[UnityTest] | |
public IEnumerator MonoBehaviour_PossibleFix_ViaIs_Success() | |
{ | |
var targetMonoBeh = m_root.AddComponent<ClassicMonoBeh>(); | |
var targetInterface = (IInterface)targetMonoBeh; | |
var targetClrClass = new CLRClass(); | |
var targetClrClassInterface = (IInterface)targetClrClass; | |
var targetClrStruct = new CLRStruct(); | |
var targetClrStructInterface = (IInterface)targetClrStruct; | |
// before destroy | |
{ | |
Assert.IsFalse(IsNullUniversal(targetMonoBeh)); | |
Assert.IsFalse(IsNullUniversal(targetInterface)); | |
Assert.IsFalse(IsNullUniversal(targetClrClassInterface)); | |
Assert.IsFalse(IsNullUniversal(targetClrClassInterface)); | |
Assert.IsFalse(IsNullUniversal(targetClrStructInterface)); | |
Assert.IsTrue(IsNullUniversal<Object>(null)); | |
Assert.IsTrue(IsNullUniversal<object>(null)); | |
} | |
Object.DestroyImmediate(targetMonoBeh); | |
targetClrClass = null; | |
yield return null; | |
// after destroy | |
{ | |
Assert.IsTrue(IsNullUniversal(targetMonoBeh)); | |
Assert.IsTrue(IsNullUniversal(targetInterface)); | |
Assert.IsTrue(IsNullUniversal(targetClrClass)); | |
Assert.IsFalse(IsNullUniversal(targetClrClassInterface)); | |
Assert.IsTrue(IsNullUniversal<Object>(null)); | |
Assert.IsTrue(IsNullUniversal<object>(null)); | |
} | |
// SOLUTION. Or use abstract class ONLY for all classes who inherit from MonoBahavior | |
bool IsNullUniversal<T>(T instance) | |
{ | |
if (instance is UnityEngine.Object unityObject) | |
return unityObject == null; | |
return instance == null; | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment