Skip to content

Instantly share code, notes, and snippets.

@vangogih
Last active March 30, 2024 11:32
Show Gist options
  • Save vangogih/02845828a8da824047ca8a9be71e1b28 to your computer and use it in GitHub Desktop.
Save vangogih/02845828a8da824047ca8a9be71e1b28 to your computer and use it in GitHub Desktop.
Unity Ungine object comparison problem and solution.
// 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