Skip to content

Instantly share code, notes, and snippets.

@20chan
Last active February 4, 2021 02:03
Show Gist options
  • Save 20chan/d8def3eb18ae98235decc722ed931ad1 to your computer and use it in GitHub Desktop.
Save 20chan/d8def3eb18ae98235decc722ed931ad1 to your computer and use it in GitHub Desktop.
유니티 직렬화 방식과 null 사용의 개같음과 버그

유니티에서 Object 클래스는 (System.Object가 아님) == 오퍼레이터와 implicit bool 캐스팅 오퍼레이터를 오버라이드한다. 이게 진자 말도 안된다

2018.3.0f2 버젼이고 코드는 Jetbrain Rider 로 디컴파일한 코드를 사용

정의된 코드는 다음과 같다:

public static bool operator ==(Object x, Object y)
{
  return Object.CompareBaseObjects(x, y);
}

public override bool Equals(object other)
{
  Object rhs = other as Object;
  if (rhs == (Object) null && other != null && (object) (other as Object) == null)
    return false;
  return Object.CompareBaseObjects(this, rhs);
}

public static implicit operator bool(Object exists)
{
  return !Object.CompareBaseObjects(exists, (Object) null);
}

private static bool CompareBaseObjects(Object lhs, Object rhs)
{
  bool flag1 = (object) lhs == null;
  bool flag2 = (object) rhs == null;
  if (flag2 && flag1)
    return true;
  if (flag2)
    return !Object.IsNativeObjectAlive(lhs);
  if (flag1)
    return !Object.IsNativeObjectAlive(rhs);
  return lhs.m_InstanceID == rhs.m_InstanceID;
}

private static bool IsNativeObjectAlive(Object o)
{
  if (o.GetCachedPtr() != IntPtr.Zero)
    return true;
  if (o is MonoBehaviour || o is ScriptableObject)
    return false;
  return Object.DoesObjectWithInstanceIDExist(o.GetInstanceID());
}

조낸 끔직하다. 유니티 포럼같은데서 보면 오브젝트가 Destroy 되었는지 보기 위해서 obj == null 을 사용하라고 하고, 또 if (obj) 같은 코드도 비슷하게 많이 사용한다. == 오퍼레이터가 실제로 null 인지 비교한 뒤 오브젝트가 살아있는지 비교한다?

이 개같은 behaviour 때문에 정말 참조가 null인지 확인하기 위해 난 (object)obj == null 같은 말도안되는 코드를 작성해야 했다. 이게 또 어이가 없는게 뭐냐면, ?? 오퍼레이터는 이루틴을 거치지 않고 순수 null인지 확인을 한다. resharper의 설명 워낙 예전부터 구닥다리 랭버젼으로 돌아가서 굳이 지원도 안하고 레거시때문에 지금와선 늦었지 싶어서 그냥 포기했다 보다.

그렇다고 이 개같은 문제가 그냥 해결될 수 있을까?

private static bool CompareBaseObjects(Object lhs, Object rhs)
{
  bool flag1 = (object) lhs == null;
  bool flag2 = (object) rhs == null;
  if (flag2 && flag1)
    return true;
  if (flag2 || flag1)
    return false;
  // if (flag2)
  //   return !Object.IsNativeObjectAlive(lhs);
  // if (flag1)
  //   return !Object.IsNativeObjectAlive(rhs);
  return lhs.m_InstanceID == rhs.m_InstanceID;
}

위처럼 내가 졸라 싫어하는 부분을 단순히 저렇게 해버린다면 아마 상식적으로는 돌아가겠지 여기에다가 그냥 bool Object.Destroyed 하나만 넣어주면 덧나냐고

위처럼 null을 쓰레기처럼 사용하는건 아마 유니티의 노답 직렬화 시스템때문일것

유니티의 serialization 시스템은 기본적으로 모든 오브젝트를 순수 null로 두지 않는다. (아닌 경우도 있긴 함 아래) 다음과 같은 테스트 스크립트를 만들고, serialize 되는 필드를 만들어서 실행하고 디버그로 찍어보면 알 수 있다.

public class Test : MonoBehaviour {
    public GameObject a;
    [NonSerialized]
    public GameObject b;

    void Start() {
        Debug.Log(((object)a) == null); // false
        Debug.Log(((object)b) == null); // true
    }
}

얘네는 일단 씬에 들어가서 컴포넌트를 넣으면 다음처럼 빈 오브젝트를 할당한다

MonoBehaviour:
  ...
  a: {fileID: 0}

그렇다면 수동으로 씬에서 이 부분을 지워서 실행하면?? 그래도 똑같이 작동한다. Object가 아닌 배열, 리스트도 똑같다. 유니티는 serialize에서 null을 지원하지 않는다.

그래서 serialize 되지 않는 필드가 초기화되지 않아 null인 경우는 정말 따로 생각해주어야 한다. 그때마다 비상식적인 null 확인이 족같음은 물론이고 거슬러 올라가 10억 달러짜리 실수를 탓하게 된다.

유니티 존나 싫다. 그래도 이거만큼 잘 굴러가는게 없다. 좆같다.

추가로 찾음

SerializedField 인 배열의 값이 null이 들어가는 경우가 있다

씬 A, B와 스크립트 M이 들어간 게임오브젝트 G를 생각하자 M에 배열을 넣지 않은 상태에서 G를 B씬에 추가한다 그리고 M에 빼열을 넣고 씬 A에서 시작해 B씬을 로딩하면 짠 G에 들어간 M의 인스턴스에는 빼열의 값이 null이 들어가있다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment