Unity’s Biggest Trap: null That Isn’t Null
Minh Khoa
Author
Today, let’s dissect a topic that seems basic but is actually a deadly “trap” for many Unity developers, including experienced ones:The difference between C# Object (System.Object) and Unity Engine Object (UnityEngine.Object) in handling Null and Destruction.
## 1. The nature of two worlds: Managed and Unmanaged
To understand the core reason, we need to know Unity’s deeper architecture. When you code in Unity, you are standing as a bridge between two worlds:
- **Managed world (C#):**The place that contains C# object (
System.Object)s, allocated in Heap memory. This memory is automatically cleaned up by the Garbage Collector system (GC) of the C runtime# (Mono/IL2CPP). - **World Native/Unmanaged (C++):**The place that contains the actual data structures of Unity (that represent
GameObject,Transform,MonoBehaviour,Texture2D...). They are written in C++ to optimize performance, and Unity’s core system takes direct control of managing their memory.
When you create aGameObjectin C#Unity creates a heavy C++ object in the background, while also creating a**wrapper shell (wrapper)**in C# so that you can interact with that C++ object. 👉 That is:UnityEngine.Objectis in fact just a thin C# shell structure containing a*pointer (pointer)*linked to the core C++ object in the background.
2. Object Destruction process (Destruction)
The biggest difference lies in how these two types of object are destroyed:
- **C# Object standard:**You cannot “actively” destroy it with a command. You can only remove all references (assign references with
null). The Garbage Collector system (GC) of C# will come to clean it up at some point in the future with a process non-deterministic (whose destruction time you cannot predict). - UnityEngine.Object:You decide everything by calling
Destroy(gameObject). This function willdestroy immediately (or at the end of the frame) the original Unity object in C space++. HOWEVER, the C shell# that you are holding a reference tois not destroyed right away at all, it still sits there on C memory# (Heap) until GC it is cleaned up.
3. The “Fake Null” trick (Null handling)
It is precisely because of the strange fact that the “C body++” has disappeared but the “C shell#” still persists that a thorny problem arises:How does C# know that the resource system under the c++ shell one UnityEngine.Object has been Destroyed?
In ordinary C logic# , if the shell is still in memory then itmyObject == nullwill returnfalse. But wait, when you writeif (myObject == null)in Unity after Destroy, it returnstrue! Clearly C# object is not actuallynull?
Unity’s secret: Operator overloading == and !=
Unity deliberatelyoverrides (overload) operator == and !=for the base classUnityEngine.Objectinstead of the natural behavior of checking whether the C memory area# of the shell class points tonullthe standard one or not, Unity's comparison function goes down and checks whether**"the Native C object++ that this shell class points to exists/is still alive?"**.
- If the Native C++ object is dead or Destroyed -> Unity reports
true. It does not care whether the C shell# is still there or gone. This phenomenon is called by advanced Unity programmers**"Fake Null"**.
If you use the pure C function#
object.ReferenceEquals(myObject, null)will returnfalse- this is the real way to tell whether the C shell class# has not yet been GC cleaned up.
Deadly traps from "Fake Null"
Because the Overload algorithm==only handles basic code fragments, it gradually breaks and shatters modern C syntax# (since C# 6.0), specifically:
- Operator
?.(Null-conditional operator) - Operator
??(Null-coalescing operator)
These operators in the C standard# are designed to be deeply embedded at the core language level (IL). They bypass (completely bypass) the operator function==overloaded by Unity, and directly assessthe C memory shell class# whether it is truly Null.
Of course, the C wrapper# is not Null at all (has not yet been GC garbage-collected)! Therefore, the Operator checker?.considers the variable valid and permits access to that object's functions. At this point, the access command is sent down to call the C++ object, but... it has hitDestroy().
BAM! Exception: MissingReferenceException a bright red console card is thrown!
A common foot-gun example:
public class DestroyExample : MonoBehaviour
{
public GameObject myPlayer;
void Start() {
Destroy(myPlayer); // Hủy object bên C++
// 1. Dùng quy chuẩn Classic -> HOẠT ĐỘNG HOÀN HẢO! Nhờ Overload == hỗ trợ
if (myPlayer != null) {
Debug.Log(myPlayer.name);
} else {
Debug.Log("Player đã tèo"); // Sẽ in ra dòng này
}
// 2. Dùng cú syntax mới của C# ("hiện đại" nhưng hại điện) -> GÂY LỖI CRASH
// Dấu '?.' cho rằng myPlayer != null (vì vỏ C# còn) -> tiếp tục gọi property .name
// -> Unity cố mò chọt vào Object C++ đã chết -> MissingReferenceException!
Debug.Log(myPlayer?.name);
// 3. Fallback toán tử ?? cũng dễ ăn hành:
GameObject clone = myPlayer ?? new GameObject(); // Sẽ giữ nguyên trả về Object hỏng (myPlayer) chứ KHÔNG khởi tạo new GameObject
}
}
4. Ultimate Solutions & Best Practices
- **Follow the traditional Null-checking method (Classic check):**When working with objects inherited from
UnityEngine.Object(MonoBehaviour,Transform,GameObject...), kindly useif (obj == null)orif (obj != null). Do not try to abbreviate. - Say NO to
?.and??**for UnityEngine.Object:**Absolutely avoid using these "sweet" syntax (sugar syntax) of C# unless they are pure objects (System.Object,Listpure C classes# that you create and do not inherit from the Engine framework). - Furthermore, Unity also supports implicit boolean casting, so you can write something very cool like
if (!myPlayer)to check NULL. This approach is often widely used by large Unity codebases.
5. Summary
- C# Object (System.Object): GC automatically manages the lifecycle. Dereferenced references -> GC clean up -> completely Null.
- **UnityEngine.Object:**The two-faced architecture (C shell# and C Core++). The Core lifecycle is deterministically destroyed manually (Deterministic) by the function
Destroy()but the dead shell is not deterministic (Non-Deterministic) dependent GC. - The operator of Unity Object is a
=="Fake Null"; it reports missing when the Core, covering the true face of the C++ Prohibit operator overloading#. - and
?.with.????? UnityEngine.Object.