Under Construction

Prometheus Lab

Portfolio & Resources

Back to Blog
Unity20 min2 views

Khác nhau giữa object C# và unity Engine Object

Author

Minh Khoa

Author

Chào các bạn, hôm nay chúng ta sẽ cùng mổ xẻ một chủ đề tưởng chừng như cơ bản nhưng lại là "cái bẫy" tử thần đối với rất nhiều lập trình viên Unity, kể cả những người đã có kinh nghiệm: Sự khác biệt giữa C# Object (System.Object) và Unity Engine Object (UnityEngine.Object) trong việc xử lý Null và Destruction.

image.png## 1. Bản chất của hai thế giới: Managed và Unmanaged

Để hiểu được lý do cốt lõi, chúng ta cần biết kiến trúc sâu xa của Unity. Khi code trong Unity, bạn đang đứng làm cầu nối giữa hai thế giới:

  1. Thế giới Managed (C#): Nơi chứa các C# object (System.Object), được cấp phát trên bộ nhớ Heap. Bộ nhớ này được dọn dẹp tự động bởi hệ thống Garbage Collector (GC) của công cụ chạy C# (Mono/IL2CPP).
  2. Thế giới Native/Unmanaged (C++): Nơi chứa cấu trúc dữ liệu thực sự của Unity (đại diện cho GameObjectTransformMonoBehaviourTexture2D...). Chúng được viết bằng C++ để tối ưu hiệu năng và hệ thống cốt lõi của Unity lấy quyền tự trực tiếp quản lý bộ nhớ của chúng.

Khi bạn tạo một GameObject trong C#, Unity tạo ra một đối tượng C++ nặng nề dưới nền, đồng thời tạo ra một lớp vỏ bộ đệm (wrapper) bằng C# để bạn có thể tương tác với đối tượng C++ đó. 👉 Tức là: UnityEngine.Object thực chất chỉ là cấu trúc vỏ bọc C# mỏng chứa một con trỏ (pointer) liên kết với đối tượng C++ cốt lõi dưới nền.

2. Quá trình Hủy đối tượng (Destruction)

Sự khác biệt lớn nhất nằm ở cách 2 dạng object này bị tiêu hủy:

  • C# Object tiêu chuẩn: Bạn không thể "chủ động" tiêu diệt nó bằng lệnh. Bạn chỉ có thể hủy bỏ tất cả reference (gán tham chiếu bằng null). Hệ thống Garbage Collector (GC) của C# sẽ tự đến dọn dẹp vào một lúc nào đó trong tương lai với một quy trình non-deterministic (bạn không đoán chắc được thời điểm nó hủy).
  • UnityEngine.Object: Bạn quyết định mọi thứ bằng cách gọi Destroy(gameObject). Hàm này sẽ tiêu diệt ngay lập tức (hoặc ở cuối frame) đối tượng gốc của Unity ở không gian C++. TUY NHIÊN, cái vỏ bọc C# mà bạn đang giữ tham chiếu không hề bị hủy ngay, nó vẫn nằm chình ình trên vùng nhớ C# (Heap) cho tới khi GC đến dọn.

3. Cú lừa "Fake Null" (Xử lý Null)

Chính vì điều kì lạ là "thể xác C++" đã biến mất nhưng "cái vỏ C#" vẫn sống dai, một bài toán nan giải xuất hiện: Làm sao C# biết hệ thống tài nguyên dưới c++ vỏ một UnityEngine.Object đã bị Destroy?

Về logic C# thông thường, cái vỏ vẫn còn trên memory thì myObject == null sẽ trả về false. Nhưng khoan đã, khi bạn viết if (myObject == null) trong Unity sau khi Destroy, nó lại trả về true! Rõ ràng C# object đâu có thực sự null?

Bí mật của Unity: Overload toán tử ==  !=

Unity cố tình ghi đè (overload) toán tử ==  != đối với class gốc UnityEngine.Object. Thay vì hành vi tự nhiên là so sánh xem vùng memory C# của lớp vỏ có trỏ tới null chuẩn hay không, hàm so sánh này của Unity đi xuống kiểm tra xem "đối tượng Native C++ mà lớp vỏ này trỏ tới có tồn tại/còn sống không?".

  • Nếu Native C++ object đã chết hoặc bị Destroy -> Unity báo true. Không quan tâm vỏ C# còn hay mất. Hiện tượng này giới lập trình Unity chuyên sâu gọi là "Fake Null".

Nếu bạn dùng hàm thuần C# object.ReferenceEquals(myObject, null) sẽ trả về false - đây là cách nhận biết thực sự lớp vỏ C# vẫn chưa bị GC dọn.

Những cái bẫy chết người từ "Fake Null"

Vì thuật toán Overload == chỉ giải quyết được các đoạn code cơ bản, nó dần làm hỏng và vỡ các cú pháp hiện đại của C# (kể từ C# 6.0), cụ thể là:

  • Toán tử ?. (Null-conditional operator)
  • Toán tử ?? (Null-coalescing operator)

Các toán tử này của chuẩn C# được thiết kế nhúng sâu ở cấp độ ngôn ngữ lõi (IL). Chúng bypass (bỏ qua hoàn toàn) hàm toán tử == bị overload của Unity, và trực tiếp thẩm định lớp vỏ bộ nhớ C# có bị Null thực sự hay không.

Tất nhiên vỏ bọc C# chưa hề Null (chưa bị GC gom rác)! Do đó, bộ kiểm tra Toán tử ?. cho rằng biến hợp lệ và cấp phép truy xuất các hàm của obj đó. Lúc này lệnh truy xuất được gửi xuống gọi C++ object nhưng... nó đã ăn Destroy().

BÙM! Exception: MissingReferenceException bắn đỏ rực thẻ console!

Ví dụ hụt chân dễ gặp:

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. Giải pháp & Best Practices tối thượng

  1. Tuân thủ cách check Null truyền thống (Classic check): Khi làm việc với các object kế thừa từ UnityEngine.Object (MonoBehaviourTransformGameObject...), hãy ngoan ngoãn sử dụng if (obj == null) hoặc if (obj != null). Đừng cố viết tắt.
  2. Nói KHÔNG với ?.  ?? cho UnityEngine.Object: Tuyệt đối tránh sử dụng các syntax "đường ngọt" (sugar syntax) này của C# trừ phi đó là các object thuần tuý (System.ObjectList, các class thuần C# do bạn tạo không kế thừa theo framework Engine).
  3. Thêm nữa, Unity cũng hỗ trợ khả năng ép kiểu ngầm định về boolean, bạn có thể viết rất ngầu là if (!myPlayer) để kiểm tra NULL. Cách này thường được các source code lớn của Unity áp dụng rộng rãi.

5. Summary

  • C# Object (System.Object): GC tự động quản lý vòng đời. Tham chiếu gỡ bỏ -> GC dọn -> Null hoàn toàn.
  • UnityEngine.Object: Kiến trúc 2 mặt (Vỏ C# và Core C++). Vòng đời Core được hủy thủ công xác định (Deterministic) bằng hàm Destroy(), nhưng vỏ chết Không xác định (Non-Deterministic) phụ thuộc GC.
  • Toán tử == của Unity Object là một "Fake Null", nó báo mất khi mất C++ core, che đậy bộ mặt thật của vỏ C#.
  • Cấm gõ toán tử ?.  ?? với UnityEngine.Object.