Unity null 확인 방법(약간의 최적화편)

== 연산자

Unity에서 ==  연산자를 사용하게 되면 C++(네이티브 객체)에서도  null 체크를 하게 되고 C#(유니티 객체)에서도  null을 체크하게 됩니다.
 
총 2번의 null을 체크하게 되는 것이므로 정확도는 올라가나 리소스를 잡아먹는 문제가 발생합니다.

using System.Collections;
using UnityEngine;

public class TestNullCheck : MonoBehaviour
{
    public GameObject go;
   
    private IEnumerator Start()
    {
        go = new GameObject();

        Destroy(go);

        yield return null;

        CheckUnityObject(go);
        CheckNativeObject(go);
    }


    private void CheckUnityObject(GameObject go)
    {
        if(go == null)
        {
            Debug.Log("Unity GameObject is null");
        }
        else
        {
            Debug.Log("Unity GameObject is not null");
        }
    }

    private void CheckNativeObject(object go)
    {
        if (go == null)
        {
            Debug.Log("Native GameObject is null");
        }
        else
        {
            Debug.Log("Native GameObject is not null");
        }
    }
}

GameObject vs object

디버깅을 통해 확인해본 결과 유니티 객체(GameObject)는 null이지만 네이티브 오브젝트(object)는 null이 아님을 볼 수 있습니다.
 
🔽이러한 상태를 "Fake Null"이라고 합니다.

더보기

실제 삭제된 건 "네이티브 오브젝트"고 유니티의 오브젝트는 삭제되지 않고 메모리상에 남아 있지만 없어진 것처럼 행동해야 하므로 null을 반환하게 된다.


유니티 객체 즉, C#객체를 해제할 수 있는 건 오직 가비지 콜렉터뿐이기 때문에,  GC가 처리해 줄 때까지 계속 메모리상에 존재하게 된다.

 

 
Fake Null 때문에 UniyuEngine.Object 클래스에서는 (==) 연산자를 오버로딩하여 네이티브 객체의 존재 여부까지 판단해서 비교한 결과를 돌려주고 있어서 총 2번 계산을 하게 됩니다.
 
네이티브 리소스의 실제 여부까지 확인할 필요가 없는 경우에는 원시 오브젝트만 비교하면 되며object.ReferenceEquals(go, null)를 이용할 수 있습니다.

 

+ object.ReferenceEquals 보다 최근에는 is null, is not null 방식을 더 많이 사용하곤 합니다(저는).
 
오브젝트를 Destroy 한 후에는 유니티 오브젝트null체크를 해야 합니다.
 Destroy 한 후에는  해당 변수에 null을 명시적으로 넣어주는 것을 추천합니다.
 
반대로, 단순한 캐싱이나, 생성된 후 파괴 되지 않는 것이 보장된 예를 들어, 싱글톤같은 경우 유니티 오브젝트가 아닌 네이티브 오브젝트object.ReferenceEquals(go, null)로  null을 비교하는 것이 성능상 좋습니다.


bool 타입으로 묵시적 형변환

Unity에는 오버로드된 암시적 부울 연산자로 인해 if문에서 직접 GameObject(또는 다른 UnityEngine.Object 파생 유형)의 존재를 확인할 수 있습니다.

public GameObject go;

void Update() 
{
    if (go) 
    {
        Debug.Log("GameObject exists or is active");
    } 
    else 
    {
        Debug.Log("GameObject is null or has been destroyed");
    }
}

암시적 부울 연산자란?

명시적으로 'bool'로 변환되지 않고 'if' 문과 같은 부울 컨텍스트에서 특정 유형의 개체를 평가할 수 있도록 하는 C#의 연산자입니다.

public class UnityObject
{
    private IntPtr nativePtr; 

    public static implicit operator bool(UnityObject obj)
    {
        return obj != null && obj.nativePtr != IntPtr.Zero;
    }
}

 

if (obj != null) { ... }

암시적 부울 연산자를 사용하면 위 코드를 아래 코드처럼 간단하게 표현할 수 있습니다.

if (obj) { ... }

암시적 부울 연산자를 사용할 때 주의점은 다음과 같습니다.

  1. 암시적 부울 연산자를 사용할 때 내부적으로 추가 검사가 수행됩니다.
  2. 매우 자주 실행되는 코드의 중요 섹션의 경우 일반적으로 null을 확인하기 위해 ReferenceEquals 메서드를 사용하는 것이 더 성능이 좋습니다.
  3. ?? 또는 ?.와 같은 연산자를 사용하는 것은 "Fake Null" 상황으로 인해 까다로울 수 있습니다.
    실제로는 null일 것이라고 예상할 수 있지만 "Fake Null" 상태일 뿐이므로 예기치 않은 동작이 발생합니다.
    아래서 다시 설명해보겠습니다.

무작정 ReferenceEquals 메서드를 사용해서는 안되는 이유

 

 

?? , ?. 연산자 주의

GameObject go = new GameObject("TestObject");
Destroy(go);

if (go == null)
{
    Debug.Log("GameObject appears null"); 
}
else
{
    Debug.Log("GameObject is not null");
}

다음 코드를 실행하면 if(go == null) 이 true이므로 "GameObject appears null" Log가 출력됩니다.

if (object.ReferenceEquals(go, null))
{
    Debug.Log("GameObject is truly null");
}
else
{
    Debug.Log("GameObject still exists in memory"); 
}

그러나 C# 네이티브 오브젝트는 null이 아니므로 "GameObject still exists in memory" 로그가 출력됩니다.
 
위 내용을 숙지하고 다음 내용을 봐주시길 바랍니다.
 

GameObject go1 = new GameObject("FirstObject");
GameObject go2 = new GameObject("SecondObject");
Destroy(go1);

GameObject result = go1 ?? go2;

null 병합 연산자(??)는 "Fake Null" 동작으로 인해 예기치 않은 결과를 제공할 수 있습니다.
 
'go1'이 파괴 되었으므로 'result'가 "SecondObject"를 참조할 것으로 예상할 수 있습니다.

하지만 'result'는 여전히 'go1' 참조(소멸된 객체)를 가리킵니다.

GameObject go = new GameObject("TestObject");
Destroy(go);

var tran = go?.transform;

null 조건부 연산자(?.)도 예기치 않게 동작할 수 있습니다.
'go'가 파괴 되었더라도 'tran'은 null이 아닙니다. 
사용할 수 없는 파괴된 객체(go)의 트랜스폼을 가리키게 됩니다.


Serialization 주의

public으로 선언한 유니티 오브젝트들은 아무것도 넣지 않으면 Fake Null상태가 되기 때문에 반드시 주의하셔야 합니다. 

(좌) False 반환 (우) True 반환

public class MyComponent : MonoBehaviour
{
    public GameObject publicGameObject;
    
    void Start()
{
    if (publicGameObject == null)
    {
        Debug.Log("publicGameObject appears null using ==");
    }

    if (object.ReferenceEquals(publicGameObject, null))
    {
        Debug.Log("publicGameObject is truly null using ReferenceEquals");
    }
    else
    {
        Debug.Log("publicGameObject is not truly null using ReferenceEquals");
    }
}

}

publicGameObject == null => true 
object.ReferenceEquals(publicGameObject, null) => false
 

  • 인스펙터에서 이러한 필드에 아무 것도 지정하지 않으면 "Fake Null"를 저장합니다. 
  • 코드에서 == null을 사용하여 해당 필드를 확인하면 null이거나 할당되지 않았음을 나타내는 true를 반환합니다.
  • 그러나 object.ReferenceEquals(publicGameObject, null) 메서드를 사용하면 false를 반환합니다. 
    이는 관리되는 메모리에서 필드가 엄격하게 null이 아니기 때문입니다.