Unity C# > 유니티 오브젝트의 fake null

Date:     Updated:

카테고리:

태그:

🚀 Destroy

  • 오브젝트가 Destroy 될 때
    • 오브젝트가 메모리 해제되면 유니티의 Object== 연산자 오버로딩으로 인하여 해제된 객체를 == null 비교 연산할 땐 그 객체를 참조하는 참조 변수들은 댕글링 포인터가 되는 것이 아닌 “null”이 되도록 처리된다.
    • 해제된 오브젝트의 컴포넌트를 참조하는 것들을 잘 확인해야 한다. 크러쉬가 날테니까! 오브젝트가 해제되면 당연히 그 오브젝트의 컴포넌트들에도 참조할 수 없다.

🚀 fake null

✈ UnityEngine.Object의 구성

모든 게임 오브젝트들이 상속 받는 UnityEngine.Object 클래스

UnityEngine.Object 클래스는 C++ 유니티 엔진 코드에서 생성한 C++ 객체인 nativeObject 객체에 대한 포인터를 지니고 있는, 이를 감싸 Wrapping한 C# 클래스이다.

  • UnityEngine.Object C# 클래스
    • Pointer 👉 C++ 유니티 엔진 코드에서 생성한 C++ 객체인 nativeObject 객체의 주소를 담음

유니티에서 UnityEngine.Object의 static 멤버 함수인 Destroy 함수는 유니티 게임 오브젝트를 파괴하는 함수이다. 이렇게 되면 사실은 게임 오브젝트(UnityEngine.Object 인스턴스) 내부의 C++ nativeObject 만 메모리 해제된다. 이를 감싸는 C# 클래스인 게임 오브젝트(`UnityEngine.Object` 인스턴스)는 C# 객체이기 때문에 해제될 수 없다. ✨C# 객체를 해제할 수 있는건 오직 가비지 콜렉터 뿐이기 때문이다.✨ (유니티 엔진 자체는 C++ 코드로 구현되어 있는듯 하다.)

  • 유니티 오브젝트 Destroy 호출시
    • Native C++ Object 👉 메모리 해제. 바로 삭제 됨.
    • UnityEngine.Object 👉 메모리 해제 되지 않음. 나중에 가비지 콜렉터가 알아서 해제 함.
      • 유니티 C++ 엔진 코드에서 Pointer 는 null 로 만든다. Native C++ Object이 해제 되면 포인터는 null로 초기화시킴

게임 오브젝트를 파괴하고나면 그 실체인, UnityEngine.Object가 참조했던 C++ 오브젝트는 삭제되지만 C# 오브젝트인 UnityEngine.Object은 남아 있다.

즉 알맹이(C++ NativeObject)만 사라지고 껍데기(UnityEngine.Object)는 가비지 콜렉터가 처리해줄 때까지 계속 메모리 상에 존재하는 것이다.


✈ UnityEngine.Object의 == 연산자 오버로딩

Destroy 했던 게임 오브젝트를 대상으로 == 연산을 하면 “null” 이 리턴된다. 👉 fake null

Destroy 후 가비지 콜렉터의 삭제를 기다리며 남아있는 UnityEngine.Object== 연산시 == 연산자 오버로딩을 호출한다. UnityEngine.Object C# 인스턴스는 삭제되지 않았기 때문에 이렇게 피연산자가 되는 것이 가능하다는 것을 알 수 있다.

(알맹이인 C++ 객체가 있어야 하는 기타 메소드 호출 멤버 변수 호출 등등은 불가능하지만 껍데기의 연산자 오버로딩은 호출 가능한 것으로 보인다.)

namespace UnityEngine
{
    public class Object
    {
        public static bool operator ==(Object x, Object y);
        public static bool operator !=(Object x, Object y);

        public static implicit operator bool(Object exists);

이렇게 UnityEngine.Object==!= 그리고 UnityEngine.Object가 묵시적으로 bool로 형변환 되려고 할 때 호출되는 implicit operator도 오버로딩이 되어 있다.

  • ==, != 연산자 오버로딩
    • UnityEngine.Object(유니티 게임 오브젝트)가 같은 지를 비교하는데, Destroy 된 게임 오브젝트도 비교가 가능하다.
      • Destroy 된 게임 오브젝트의 UnityEngine.Object C# 인스턴스 부분은 삭제되지 않았기 때문이다.(가비지 콜렉터가 해야 됨)
      • 👉Destroy 된 오브젝트는 사실 실제로 삭제된건 C++ Native Object 이고 UnityEngine.Object C# 클래스 부분은 아직도 메모리 상에서 없어지지 않아 실제로 null이 된 것은 아니지만 없어진 것 처럼 행동해야 하기 때문에 “null”로 처리된다. 이게 바로 ✨fake null✨ 이다.
  • bool 묵시적 형변환 가능
    • 그래서 if (gameObject) 같은 표현이 가능하다. 유니티 게임 오브젝트는 bool로 자동 형변환 된다. 이 오버로딩 때문에 가능한 일!
      • 👉 Destroy 된 오브젝트는 false 로 리턴되도록 오버로딩 구현이 되어 있다.


image

아직 _stat(private)에 컴포넌트가 할당 되본적도 없는 상태일 땐 이렇게 진짜 null인 상태이다.

단! public 혹은 [SerializeField]로 선언이 되어 유니티 에디터에서 할당을 받아야하는데 아직 한번도 되지 않은 경우는 null이 아닌 fake null로 처리 됨.

image

_lockTarget을 Destory 시킨 후 살펴보니 "null"로 되어 있다. 그냥 null이 아닌 "null"fake null이다. _lockTarget가 포인터로 참조했던 C++ 객체 알맹이는 날라갔지만 _lockTarget 인스턴스의 UnityEngine.Object 껍데기는 남아있기 때문에 사실은 null이 아닌 것이다. 그러나 알맹이가 사라졌으므로 진짜 null은 아니지만 이를 null로 처리되게끔 _lockTarget가 fake null인 “null” 상태라고 포장해주는 것이다. 이에 대한 처리가 UnityEngine.Object의 == 연산자 오버로딩에 구현이 되어 있다. 따라서 진짜 null 이 된게 아님에도 Destroy된 오브젝트 == null 하면 True 로 리턴이 되게끔!

public GameObject go;

Destroy(go);
if (go == null)
    Debug.Log("as UnityEngine.Object");
if ((object)go == null)
    Debug.Log("as System.Object");
💎출력💎

as UnityEngine.Object
  • objectSystem.Object의 줄임말이다.
  • C#에 존재하는 모든 클래스는 이 System.Object을 상속 받는다.
    • UnityEngine.Object도 마찬가지다.
    • 따라서 UnityEngine.Objectobject로 업캐스팅 할 수 있다.
  • go == null
    • go 오브젝트는 사실 시스템상으론 null 이 아니지만 == null에서 True로 처리되기 위해, 댕글링 포인터를 막기 위해 fake null 이 되어 있는 상태다.
    • UnityEngine.Object== 연산자 오버로딩에 이 같은 내용이 반영이 되어 있다.
    • 따라서 이건 == 연산자 오버로딩에 의해 True 로 처리 된다.
  • (object)go == null
    • go 오브젝트를 System.Object로 형변환했으니 ==은 더 이상 UnityEngine.Object의 오버로딩한 ==가 아닌 그냥 일반적인 ==가 된다.
    • 실제로 go 오브젝트 (UnityEngine.Object인스턴스)는 null이 된 것이 아니였으므로 이땐 false로 처리된다.
      • UnityEngine.Object== 연산자가 호출되지 않기 때문에


✈ 오브젝트 null 체크의 단점

null 연산 사용시 의도치 않은 결과 발생

1진짜 null이 되는 것이 아니라 fake null 이기 때문에 혹시라도 추후 C#의 ??, ? 같은 null 판별 연산을 할 때 원치 않는 결과가 나올 수 있다. 진짜 null이 아니기 때문이다. (??,? 같은 null 연산은 유니티 게임 오브젝트에겐 적용하지 않는게 좋겠다.)

public GameObject go1;
public GameObject go2;

Destroy(go1);

go1 ?? go2; // go1이 null이면 go2 리턴, 아니라면 go1 그대로 리턴

go2가 리턴되는 일은 없다. go1은 Destroy 된 후 null은 아니지만(C# UnityEngine.Object 아직 해제 안됨) 댕글링 포인터가 되지 않도록, ==, bool 형변환 등등에서 null로 처리되도록 하기 위하여 fake null 이 된 상태다. 따라서 진짜 null인건 아니기 때문에 go1은 파괴 되었음에도 불구하고 go2로 리턴되지 않는다.


오브젝트의 null 체크 연산은 매우 비싸다.

출처 및 참고 블로그

if (component == null) or if (go == null) 이런 오브젝트, 컴포넌트 비교 연산은 심지어 GetComponent 보다 비싸다.

UnityEngine.Object 의 == 연산자는 유니티 오브젝트와 진짜 null을 비교할 때는 (go == null), 즉 한쪽만 null일 때는 null이 아닌 그 한쪽 오브젝트의 네이티브 C++ 객체의 유무 또한 체크하고 MonoBehaviour, ScriptableObject 등으로 캐스팅도 하는 등등 함수도 2 번 호출하고.. 아무튼 비용이 많이 든다. 유니티 오브젝트가 Destroy 되어 C++ 알맹이가 없는 상태면 null과 같다고 fake 처리 해주기 위해서다.

위 블로그 링크에서 UnityEngine.Object의 implicit bool== 연산자 코드를 확인하자!! 둘 다 한 쪽만 null인 경우엔 비용이 비싸지는 것을 확인할 수 있다. 차라리 둘 다 null이 아니거나 둘 다 null이면 별도의 캐스팅, 함수 호출 없이 금방 빠져 나오는 것을 확인할 수 있음.


✈ 오브젝트 null 체크를 대신 할 추천 방법

  • 1️⃣ 오브젝트를 bool로 묵시적 형변환
    • 느리지만 안정적.
    • UnityEngine.Object는 implicit bool 로 암시적 형변환 연산자 오버로딩 또한 되어 있다. Destroy 된 오브젝트면 false 로 형변환되게끔!
    • 여전히 fake null 인 상태인 것에선 ==과 전혀 다름이 없긴 하지만 좀 더 안정적
      if (gameObject)
        ///...
      
  • 2️⃣ Destroy 후 진짜 null 대입
    • 안정적. *(근데 굳이 코드 한 줄을 더..? C++ 같잖아!)
      Destroy(go);
      go = null;
      
  • 3️⃣ System.Object로 형변환하여 비교 (혹은 obejct.ReferenceEquals 함수 사용)
    • 훨씬 빠름. fake null 상태라도 null 과 같다고 표현하려고 노력 하는.. UnityEngine.Object의 연산자 오버로딩을 거치지 않기 때문!
    • 그냥 UnityEngine.Object 로서의 있는 그대로 모습을 빠르게 비교
      UnityEngine.Object obj;
      
      // 👉 obj 가 진짜 null이라면 True
      ReferenceEquals(obj, null);  
      if ((object)obj == null)
      
      // 👉 obj가 falke null 이라면 True. 
      // Destroy 후 obj 는 fake null 상태가 됨. (진짜 null 상태가 아님)
      // (object)obj == null 의 결과는 False. obj 는 진짜 null이 아니므로.
      // obj 또한 그 자체로 null이 사실 아니므로 obj 하나로는 True인 상태
      // false && true 의 결과는 false.
      !ReferenceEquals(obj, null) && obj; 
      if (((object)obj == null && obj) == false)
      


reference



🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

UnityDocs 카테고리 내 다른 글 보러가기

댓글 남기기