게임오브젝트 접근할때 null 비교를 통해
null이면 debug찍고 return 하는 과정이 번거로워서
일종의 extension 함수를 작성해봣다.
public static bool CheckNull(this object obj,
string objName="",
[CallerMemberName] string methodName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
if (obj == null)
{
if (Debug.isDebugBuild)
{
// 파일 경로에서 클래스 이름(파일명이 클래스와 동일한 경우)을 추출
string className = System.IO.Path.GetFileNameWithoutExtension(filePath);
Debug.LogError($"[{className}] {methodName} - Object {objName} is null! (Line: {lineNumber})");
}
return true;
}
return false;
}
문제는 object 비교를 하게되자 디버깅에서는 오브젝트가 null로 찍히지만
CheckNull 함수 내에서는 오브젝트가 null이 아니어서 계속 return false가 되었다.
https://discussions.unity.com/t/is-null-returns-false-on-null-objects/821933
검색해보던 중 위 유니티 포럼을 찾았다.
핵심 내용은 다음과 같다.
Unity는 == 연산자를 오버로드하여 특정 객체가 삭제되었을 때도 null처럼 동작하도록 만든다.
하지만 is null은 C#의 기본 연산자로, Unity의 오버로드된 == 연산자의 영향을 받지 않는다.
따라서 obj == null은 true를 반환할 수 있지만, obj is null은 false를 반환할 수 있다.
object.ReferenceEquals(obj, null)을 사용하면 더 정확한 null 체크가 가능할 수 있다.
핵심 원인은 함수 인자의 자료형으로 받은 object가
UnityEngine.Object
자료형이 아니고, C#의 object
이므로 발생하는 현상이다.
UnityEngine.Object 타입의 객체에서는 == null이나 is null 같은 연산자들이 오버로드되어,
객체가 Destroy() 되었을 경우나 기본값 null로 초기화되었을 때 null처럼 동작한다.
하지만 C#의 object 타입은 기본 연산자를 사용하기 때문에 Unity의 == null 오버로드 기능을 무시하고 일반적인 null 체크로 수행된다.
즉, obj == null을 검사할 때, obj가 UnityEngine.Object 타입이라면
Unity의 특수한 null 검사 방식이 적용되지만,
그냥 object 타입이라면 Unity의 null 오버로드를 우회하고
일반적인 C#의 null 비교 규칙을 따르게 된다.
따라서 유니티 엔진의 == null 를 오버로드 하고싶다면
UnityEngine.Object obj
타입을 사용해야한다.
위 내용을 검색하면서 알아본 null 비교 방식들을 정리해보려고 한다.
대표적으로 세가지가 있다.
UnityEngine.Object 타입에서는 ==
연산자가 오버로드되어 있으며,
C++ 네이티브 객체가 삭제되었는지까지 감안하여 null을 반환한다.
하지만 일반적인 C# 클래스(System.Object를 상속받은 클래스)에서는
== null
이 일반적인 참조 비교만 수행하며, Unity의 오버로드된 ==
의 영향을 받지 않는다.
is null
연산자는 C#의 기본 연산자로,
일반적인 == null
비교보다 빠른 성능을 낼 수 있다.
하지만 is
연산자는 UnityEngine.Object에 대한 오버로드된 ==
연산자의 영향을 받지 않으므로, Destroy된 오브젝트에 대해 정확한 null 비교에 문제가 생길 수 있다.
ReferenceEquals(a, b)
는 메모리 주소를 직접 비교하는 방식으로,
일반적인 ==
연산자보다 빠른 성능을 낼 수 있다.
하지만 UnityEngine.Object에 대해 오버로드된 ==
연산자를 우회하므로,
마찬가지로 Destroy된 오브젝트에 대해 정확한 null 비교에 문제가 생길 수 있다.
Destroy()를 호출하거나 GetComponent()를 호출했는데 적절한 컴포넌트를 찾지 못한 경우,
Unity는 반환된 객체를 null
로 처리한다.
하지만 이 객체를 최상단 자료형인 System.Object로 캐스팅한 후
== null
로 검사하면, 서로 다른 결과가 나올 수 있다.
예제 코드:
(Object) obj == null // UnityEngine.Object는 null로 평가됨
obj is null // false 반환 (System.Object로 캐스팅했기 때문)
이 문제는 유니티는 객체에 C++(네이티브)상태 객체와 C#(유니티)상태 객체로 나뉘어 있기 때문에 발생한다.
유니티는 내부적으로 C++ 코드가 있어 실제 객체는 C++로, 클래스 내에서 보이는건 C#으로 래핑 되어 있다.
Destroy()를 하면 C++의 실제 객체는 삭제
C#으로 래핑된 부분은 아직 GC의 동작을 기다리고 있다.
UnityEngine.Object == 는 C# 래핑 부분뿐 아니라 실제 객체도 존재하는지에 대한 부분도 보고 있다.
위 경우 실제 C++ 객체는 Destroy에 의해 삭제되었으니 이를 알고 null이라 한다.
System.object의 경우 아직 C#의 래핑된 부분이 남아 있으므로 null이 아니라고 한다.
이 문제는 Unity가 객체를 C++ 네이티브 객체와 C#에서 관리되는 객체로 나누어 관리하기 때문에 발생한다.
Unity의 객체 구조
Destroy()
를 호출하면 C++ 네이티브 객체가 제거되지만,UnityEngine.Object
의 ==
오버로드 동작
==
연산자를 오버로드하여 C++ 네이티브 객체가 삭제되었는지도 확인하여 null인지 판단한다.UnityEngine.Object == null
을 검사하면 실제 C++ 객체가 삭제된 경우에도 true
를 반환한다.System.Object를 통한 비교
System.Object
로 캐스팅하면 Unity의 ==
연산자가 적용되지 않으며,null
로 평가되지 않을 수 있다.