It could, be that’s not really necessary. Plus Unity objects can’t be instantiated with “new”, which would clash with the “using” pattern one would expect to work with IDisposable.
The only issue here is Unity choosing an abstraction that forcefully agglomerate the destroyed/disposed unmanaged resource state with the managed null concept, leading to behavior inconsistencies between various ways of checking for null.
Nope, you’re wrong here. “An object is faked as null when the underlying unmanaged object is destroyed” applies to :
- The bool implicit cast :
if (object)
- The “==” and “!=” operators :
if (object == null)
and if (object != null)
- The
Equals()
method : if (object.Equals(null))
It doesn’t applies to :
- The
ReferenceEquals()
method
- The
is null
/ is not null
C# constructs
- The
??
and ?.
C# operators
And note that you can actually write code to detect if the native object exists or not, for example by doing :
bool isDestroyed = !ReferenceEquals(unityObject, null) && unityObject == null
IMO, the “null can also means destroyed” concept wasn’t that bad of an idea. In 99.9% of practical uses cases it functionally works and allow to write more compact code. If that concept didn’t exists and was only available as a discrete property, you would have to replace every object != null
or (bool)object
statement with a object != null && !object.isDestroyed
.
The main issue is obscure behavior inconsistencies with the newer C# constructs, and that’s the main reason I think it should be obsoleted and replaced with a IsDestroyed
property.
The other reason is performance. The overloaded operators/methods are awfully slow, although that specific issue is mainly due to their runtime (player) implementation being encumbered by an editor-specific call stack. Those methods/operators could be 3 to 8 times faster by having a discrete implementation directly checking for the UnityEngine.Object.m_CachedPtr
field instead of having a non-inlined 3-4 methods deep call stack, as well as being written with an early out for the most common case (see the “LegacyEquality” example at the end of this post).
IMO, the ideal implementation would be this :
public class UnityObject
{
internal IntPtr m_CachedPtr;
public bool IsDestroyed
=> m_CachedPtr == IntPtr.Zero;
public static implicit operator bool(UnityObject unityObject)
=> unityObject != null && unityObject.m_CachedPtr != IntPtr.Zero;
}
public static class UnityObjectExtensions
{
public static T DestroyedAsNull<T>(this T unityObject) where T : UnityObject
{
if (unityObject == null || unityObject.m_CachedPtr == IntPtr.Zero)
return null;
return unityObject;
}
}
This mean you get the best of both worlds. You can now do this :
// all those checks are functionally equivalent
if (!unityObject)
if (unityObject == null || unityObject.IsDestroyed)
if (unityObject.DestroyedAsNull() == null)
if (unityObject.DestroyedAsNull() is null)
// Null coalescing and null conditional support
float f = unityObject.DestroyedAsNull()?.someFloatField ?? 0f;
This keeps the ability to coalesce null checking and destroyed state in a compact check, but avoid the inconsistent behavior between various way of checking for null, unless the intent and behavior is clearly expressed by using the DestroyedAsNull() method.
It would also provide a relatively safe upgrade paths for existing projects. Unity could analyze usages of the == and != operators against null and insert the DestroyedAsNull() method, making those calls functionally equivalent :
if (unityObject == null) // before
if (unityObject.DestroyedAsNull() == null) // after
(unityObject == anotherUnityObject) // before
if (unityObject.DestroyedAsNull() == anotherUnityObject.DestroyedAsNull()) // after
The only case where this doesn’t result in the same behavior is if you are comparing two different objects for equality and both are destroyed. In the current implementation they aren’t considered equal, which is inconsistent with the behavior of the second example, as null == null
returns true.
To have a truly safe migration path, object equality comparisons should be replaced with a “LegacyEquality” method, something like :
public static bool LegacyEquality(UnityEngine.Object unityObject, UnityEngine.Object otherUnityObject)
{
if (unityObject == otherUnityObject) return true;
if (unityObject == null && otherUnityObject.m_CachedPtr == IntPtr.Zero) return true;
if (otherUnityObject == null && unityObject.m_CachedPtr == IntPtr.Zero) return true;
return false;
}