The Unity script needs the Editor and the setup listed below. The separate C# companion practice runs in SharpRune without UnityEngine.
Two lifetimes behind one Unity reference
A UnityEngine.Object reference can connect two different lifetimes: a managed wrapper reachable by C# and an underlying engine object with its own destruction rules. Destroying the engine object does not imply that every managed reference immediately becomes an ordinary null reference. Unity overloads equality so a wrapper whose native object is gone can compare equal to null. Understanding that split is useful when a list, callback or longer-lived service retains a reference after a scene object has been removed from the running game.
The important question is which comparison a particular expression performs. Unity's overloaded equality considers the native object's existence, whereas ReferenceEquals asks about managed reference identity. Both answers can therefore be correct while looking contradictory. The lesson deliberately retains a wrapper, destroys a separate target and observes it later. The host must remain alive so its coroutine can finish. This experiment is about the engine-object boundary; it does not show an object being reclaimed immediately by the managed garbage collector or prove that a retained wrapper is harmless.
Portable C# companion: ordinary aliases
The SharpRune companion uses System.Object rather than UnityEngine.Object. owner and alias initially refer to the same ordinary object. Assigning null to owner changes that variable only; alias still holds its reference. Consequently alias == null prints False, and ReferenceEquals(owner, null) prints True. No object is destroyed by the assignment, and no Unity operator participates. This simple alias trace helps separate changing a variable from changing an object's lifetime, but its two lines must not be presented as an implementation of Unity's detached-wrapper behavior.
object? owner = new object(); object alias = owner; owner = null;
Console.WriteLine(alias == null); Console.WriteLine(ReferenceEquals(owner, null));Unity Editor: observe a separate destroyed target
In the Unity Editor, create Host and Target as separate GameObjects and assign Target to DestroyProbe.cs on Host. The routine saves a wrapper reference, calls Destroy(target), yields to a later frame, and then labels both comparisons. After destruction, the retained wrapper's Unity null comparison reports True while object.ReferenceEquals reports False. The initial guard rejects assigning the host itself, since removing the coroutine's own GameObject would prevent the observation. This script belongs in a real Unity project; SharpRune does not supply a substitute engine destruction scheduler.
private IEnumerator Start()
{
if (target == null || target == gameObject) { Debug.LogError("Assign a separate target object.", this); yield break; }
GameObject wrapper = target;
Destroy(target);
yield return null;
Debug.Log("Unity null: " + (wrapper == null), this);
Debug.Log("Managed null: " + object.ReferenceEquals(wrapper, null), this);
}C# null syntax does not share one mechanism
Null-conditional and null-coalescing syntax is not a universal replacement for a Unity equality check. The C# operators ?. and ?? use managed null semantics rather than calling Unity's overloaded equality in the same manner. Changing a variable's static type to object can also change which operator is selected. Review the expression and its types before copying a guard from ordinary .NET code. Keep Destroy scheduling separate from garbage collection as well: observing a missing engine object says nothing by itself about when all managed aliases become unreachable.
Two independent null-boundary experiments
First, run an independent Editor observation that logs both comparisons before Destroy and again after the supplied yield. Predict False for both null checks before destruction and the differing answers afterward. Hint: retain the same wrapper variable across the two observations and leave Host alive. Check the labels rather than reading a bare True as proof of one particular mechanism. As a separate safety check within that experiment, deliberately assign Host once and confirm that the original validation rejects it before starting the destructive observation.
Second, work only in a fresh console copy and add a third variable that aliases the original object before owner is cleared. Clear alias afterward and check that the third reference still compares nonnull. Hint: draw three variable slots pointing to one object; erasing one arrow does not erase the other arrows. No Unity-specific result should appear in this practice. Explain which observation demonstrates ordinary reference assignment and why reproducing a destroyed Unity wrapper would require the engine's actual type and lifetime behavior instead of a renamed System.Object.
Review the wrapper and the engine object
Finish by describing the target's engine existence, the wrapper's managed reachability and each comparison's meaning independently. Use a later-frame observation for the supplied destruction experiment and preserve a separate host. If a persistent collection retains destroyed targets, decide when that collection should remove them rather than assuming Unity's null comparison clears storage automatically. The portable trace remains False then True for ordinary aliases; the Editor trace remains separately labeled Unity null and Managed null. Those labels make the boundary explicit and prevent one experiment from claiming more than it establishes.
- Keep Host distinct from the target being destroyed.
- Name the static type used by each equality expression.
- Distinguish Unity equality from ReferenceEquals and ?. semantics.
- Do not equate native destruction with immediate managed collection.
Unity Editor example
Set up in Unity 6
- Create an empty Host and a separate Cube named Target, then attach DestroyProbe.cs to Host.
- Assign Target in the Inspector; do not assign Host or its GameObject.
- Enter Play mode and observe the two comparison messages after Target is destroyed.
- Compare the same references before destruction in a copy, keeping managed-null and Unity-null observations labeled.
- Do not replace the explicit Unity check with ?. or ?? when guarding access to destroyed engine wrappers.
DestroyProbe.cs
Download Unity script ↗using System.Collections;
using UnityEngine;
public sealed class DestroyProbe : MonoBehaviour
{
[SerializeField] private GameObject target;
private IEnumerator Start()
{
if (target == null || target == gameObject) { Debug.LogError("Assign a separate target object.", this); yield break; }
GameObject wrapper = target;
Destroy(target);
yield return null;
Debug.Log("Unity null: " + (wrapper == null), this);
Debug.Log("Managed null: " + object.ReferenceEquals(wrapper, null), this);
}
}
What to observe in Unity
After the target is destroyed, Unity null reports True while Managed null reports False for the retained wrapper. The host remains alive to complete the observation.
The console practice below isolates the C# logic. It does not run a Unity scene or emulate Unity engine callbacks.
C# companion practice
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
object? owner = new object(); object alias = owner; owner = null;
Console.WriteLine(alias == null); Console.WriteLine(ReferenceEquals(owner, null));
C# practice output
False True
The real Unity probe intentionally compares overloaded == with ReferenceEquals after destruction.