==연산자
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");
}
}
}
![](https://blog.kakaocdn.net/dn/bmtxqc/btrXiOoHM5s/JJ67RVepBx6pFqJvXz5i0K/img.png)
디버깅을 통해 확인해본 결과 유니티 객체(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) { ... }
암시적 부울 연산자를 사용할 때 주의점은 다음과 같습니다.
- 암시적 부울 연산자를 사용할 때 내부적으로 추가 검사가 수행됩니다.
- 매우 자주 실행되는 코드의 중요 섹션의 경우 일반적으로 null을 확인하기 위해 ReferenceEquals 메서드를 사용하는 것이 더 성능이 좋습니다.
- ?? 또는 ?.와 같은 연산자를 사용하는 것은 "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상태가 되기 때문에 반드시 주의하셔야 합니다.
![](https://blog.kakaocdn.net/dn/cyNVM4/btrXmlGeqVF/YDgR3uaHEalTepFaSOygl1/img.png)
![](https://blog.kakaocdn.net/dn/l80AR/btrXiPBygRX/biouzZ09A4h0VJ4bASmhkk/img.png)
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이 아니기 때문입니다.