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)를 이용할 수 있습니다.
 
오브젝트를 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이 아니기 때문입니다.