EveryDay.DevUp

[Unity] Fake null 본문

Unity

[Unity] Fake null

EveryDay.DevUp 2021. 12. 20. 21:10

Fake null이란?

- Unity Object의 == 또는 != 연산자에서 null을 비교했을 때와 System.Object의 null을 비교 했을 때 값이 다르게 나오는 현상을 말한다.

public class NullCheckTest : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(CheckNullTest());
    }

    IEnumerator CheckNullTest()
    {
        GameObject go = new GameObject();
        Destroy(go);

        WaitForEndOfFrame wfs = new WaitForEndOfFrame();
        /// Destory의 경우 바로 삭제되지 않고 한 프레임이 지나야 
        /// 삭제되기 때문에 WaitForEndOfFrame추가
        yield return wfs;

        if ( go == null)
        {
            Debug.Log(" Game Object is Null");
        }

        if( (object)go != null)
        {
            Debug.Log(" System Object is Not Null ");
        }
    }
}

Destroy로 GameObject를 삭제 했을 때 go == null의 경우 True가 나오지만, go를 System.Object로 형변환 후 비교했을 때는 null이 아닌것으로 체크된다.

Unity Object의 경우 C++ 네이티브 객체를 래핑하여 사용하는데, Destroy를 할 경우 래핑한 객체는 삭제되지만 Unity Object는 GC에서 메모리를 해제하기전까지 남아 있기 때문에 해당 현상이 발생한다.

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);
     }
 }

Object Class 안의 ==, !=, bool 형변환 함수가 재정의되어 있는데, Unity에서는 Destroy 된 오브젝트의 null 체크를 유연하게 대응하기 위해 해당 함수를 재정의해서 사용하고 있다.

System.Object의 경우 GC가 호출되기 전까지는 실제 null 값이 아니기 때문에 Unity에서 null 체크와 다른 체크를 하게 된다.

Fake null 이슈

System.Object로 비교했을 때와 Unity Object로 비교했을 때 이슈가 있다면 Unity Object 로만 비교해서 사용하면 문제가 없을 것 같지만, Fake Null이 의도치 않은 상황에서 나올 수 있기 때문에 유의할 필요가 있다.

1. ?, ?? ( null 조건 연산자, null 병합 연산자 ) 사용 시 의도치 않은 참조 에러가 발생할 수 있다.

- ? ( null 조건 연산 ) 의 경우 ? 앞에 있는 객체가 null 일 경우 null을 return하고, 아니면 다음의 속성이나 메소드를 실행

- ?? ( null 병합 연산자 )의 경우 ?? 앞에 있는 객체가 null이 아닌 경우 왼쪽 피연산자의 값을, null인 경우 오른쪽 피연산자의 값을 반환

public class NullCheckTest : MonoBehaviour
{
    GameObject go;
    void Start()
    {
        StartCoroutine(CheckNullTest());
    }

    IEnumerator CheckNullTest()
    {
        GameObject go = new GameObject();
        Destroy(go);

        WaitForEndOfFrame wfs = new WaitForEndOfFrame();
        yield return wfs;

        int? hashCode = go?.GetHashCode();
        Debug.Log(hashCode);
    }
}

go 가 null이기 때문에 Log의 값이 null이 나와야 할 것 같지만 hashCode의 값이 나오게 된다. Unity Object가 ==, != 은 오버로딩했지만 ?, ?? 는 오버로딩하지 않았기 때문에 System.Object의 null 체크로 null이 아닌 값으로 체크되었기 때문이다.

2. public이나 serializeField로 정의된 변수의 경우 값을 할당하지 않았다면 fake null 상태가 된다.

public class NullCheckTest : MonoBehaviour
{
    public GameObject go = null;
    void Start()
    {
        if( go == null )
        {
            Debug.Log("Game Object Is Null");
        }

        if( (object)go != null)
        {
            Debug.Log("System Object Is Not Null");
        }
    }
 }

3. Unity Object의 ==, !=의 연산은 시간이 소요된다.

public class NullCheckTest : MonoBehaviour
{
	void Start()
    {
        StartCoroutine(CheckNullTest());
    }

    IEnumerator CheckNullTest()
    {
        GameObject go = new GameObject();
        Destroy(go);

        WaitForEndOfFrame wfs = new WaitForEndOfFrame();

        int testNum = 1000;

        System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
        sw.Stop();

        yield return wfs;

        sw.Start();
        for (int i = 0; i < testNum; i++)
        {
            if (go == null) {
                ;
            }
        }
        sw.Stop();
        Debug.Log(" Unity Null Check : " + sw.ElapsedTicks);

        sw.Restart();
        for (int i = 0; i < testNum; i++)
        {
            if (object.ReferenceEquals(go, null))
            {
                ;
            }
        }
        sw.Stop();

        Debug.Log(" System Null Check : " + sw.ElapsedTicks);

        go = null;
        sw.Restart();
        for (int i = 0; i < testNum; i++)
        {
            if (go == null)
            {
                ;
            }
        }
        sw.Stop();
        Debug.Log(" GameObject Null Check : " + sw.ElapsedTicks);

    }
}

1000번의 테스트 수행 시, 단순 Unity Object의 시간이 object의 null 체크보다 오래 걸리는 것을 확인할 수 있다. go를 null로 할당할 경우 해당 시간을 줄일 수 있다.

내부적으로 구현된 네이티브 코드를 확인 시 Unity Object의 ==, != 가 코드 유연성을 위해 비교해야될 대상이 많기 때문이다.

정리

1. Unity Object가 사용되지 않는 시점에 명시적으로 NULL을 대입하는게 좋음. Fake null 이슈 외에도 의도치 않은 참조를 막을 수 있어 메모리 누수를 방지할 수 있음

2. !=, ==, bool 형변환 이 외의 ?, ?? 등 다른 연산자를 사용할 경우에는 Fake null을 이해하고 사용해야 함

: ?, ??를 오버로딩해서 사용하여 실수를 줄이는 것도 방법이 될 수 있음 

* 출처 및 참고 블로그

- https://ansohxxn.github.io/unitydocs/fakenull/

 

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

🚀 Destroy

ansohxxn.github.io

- https://overworks.github.io/unity/2019/07/22/null-of-unity-object-part-2.html

 

유니티 오브젝트의 null 비교 시 유의사항 2

이전글: 유니티 오브젝트의 null 비교 시 유의사항

overworks.github.io

- https://overworks.github.io/unity/2019/07/16/null-of-unity-object.html#fn:1

 

유니티 오브젝트의 null 비교 시 유의사항

다음글: 유니티 오브젝트의 null 비교 시 유의사항 2

overworks.github.io

- https://www.csharpstudy.com/CS6/CSharp-null-conditional-operator.aspx

 

C# 6.0 널 조건 연산자 - C# 프로그래밍 배우기 (Learn C# Programming)

널 조건 연산자 (Null-conditional operator) C# 프로그래밍에서 NULL 체크만큼 많은 시간을 할애하는 곳도 아마 드물 것이다. 즉, 객체의 메서드나 속성을 사용하기 전에 객체가 NULL인지 항상 체크해 줘야

www.csharpstudy.com