LoginSignup
1
3

More than 1 year has passed since last update.

Unityにおけるnullチェック(2) - Destroyが何をしているか

Last updated at Posted at 2022-12-15

昨日の続きの記事になります。

前回のおさらい

  • UnityEngine.Objectでは等値演算子==(及び非等値演算子!=)がオーバーロードされている
  • オーバーロードされた等値演算子ではObject.CompareBaseObjectsメソッドに判定処理を委譲している
public static bool operator ==(Object x, Object y) => Object.CompareBaseObjects(x, y);

public static bool operator !=(Object x, Object y) => !Object.CompareBaseObjects(x, y);
  • Object.CompareBaseObjectsメソッド内では純粋なnullチェックやInstanceIDの等値判定に加えて、渡されたオブジェクトが「破棄」されていないか(Unityフレームワーク上で生存しているか)をチェックするObject.IsNativeObjectAliveメソッドが実行されている
private static bool CompareBaseObjects(Object lhs, Object rhs)
{
    // objectにアップキャストしてnullか判定
    bool flag1 = (object)lhs == null;
    bool flag2 = (object)rhs == null;
    
    // 第1引数と第2引数がどちらもnullなら等価と判定
    if (flag2 & flag1)
        return true;

    // Object.IsNativeObjectAlive: ObjectがUnityフレームワーク上で存在するものとして扱われているかを判定するメソッド(推定)
    // 具体的にはDestroyされているかなどを返すと考えられる(他にもチェック項目がある可能性あり)
    
    // 第1引数のみnullの時、第2引数がDestroyされているならtrue、されていないならfalse
    if (flag1) return !Object.IsNativeObjectAlive(rhs);

    // 第2引数のみnullの時、第1引数がDestroyされているならtrue、されていないならfalse
    if (flag2) return !Object.IsNativeObjectAlive(lhs);
    
    // 両方nullでなくかつDestroyされていないなら、IDが一致するかどうかで等価を判定
    return lhs.m_InstanceID == rhs.m_InstanceID;
}
  • 逆に言うと、Object.IsNativeObjectAliveメソッドがfalseを返すようにすることがオブジェクトを「破棄(Destroy)」するということであると考えられる

なお、この記事では前回に引き続いてDestroyに破棄という訳を当てていますが、非公式なものです。

Destroyの挙動検証

前回のDestroyによる破棄というセクションでは

IsNativeObjectAliveメソッドの機能を書き下し版メソッドの中で「ObjectがUnityフレームワーク上で存在するものとして扱われているかを判定するメソッド(推定)」と記載しましたが、IsNativeObjectAliveメソッドがfalseを返す場合に、そのオブジェクトはUnityフレームワーク上で「破棄」されていると考えられます。
逆に言うと、Destroyメソッドなどが行う「破棄」の処理は、このメソッドがfalseを返すようにするということだと考えられます。

のようにメソッドの実装に基づく推論で話を勧めましたが、実際にDestroy前後でどのように値が変化しているかを検証してみます。
以下のようなテストコードを書きました。

SampleTest.cs
using System;
using System.Collections;
using System.Reflection;
using UnityEngine;
using Object = UnityEngine.Object;

public class SampleTest : MonoBehaviour
{
    private const string IsNativeObjectAlive = "IsNativeObjectAlive";
    private const string DoesObjectWithInstanceIDExist = "DoesObjectWithInstanceIDExist";
    private const string GetCachedPtr = "GetCachedPtr";

    public GameObject FooGameObject;
    public MonoBehaviour BarMonoBehaviour;
    public ScriptableObject BazScriptableObjectPrefab;
    public Material QuxMaterialPrefab;
    public Texture QuuxTexturePrefab;

    private ScriptableObject bazScriptableObject;
    private Material quxMaterial;
    private Texture quuxTexture;

    private IEnumerator Start()
    {
        bazScriptableObject = Instantiate(BazScriptableObjectPrefab);
        quxMaterial = Instantiate(QuxMaterialPrefab);
        quuxTexture = Instantiate(QuuxTexturePrefab);

        Debug.Log("Destroy前:");
        InvokeAllObjects();

        // GameObjectを破棄
        Destroy(FooGameObject);
        // MonoBehaviourを破棄
        Destroy(BarMonoBehaviour);
        // ScriptableObjectを破棄
        Destroy(bazScriptableObject);
        // Materialを破棄
        Destroy(quxMaterial);
        // Textureを破棄
        Destroy(quuxTexture);

        Debug.Log("Destroy直後:");
        InvokeAllObjects();

        // 1フレーム待機
        yield return null;

        Debug.Log("Destroy後1フレーム待機:");
        InvokeAllObjects();
    }

    private void InvokeAllObjects()
    {
        InvokeAllMethods(FooGameObject, nameof(FooGameObject));
        InvokeAllMethods(BarMonoBehaviour, nameof(BarMonoBehaviour));
        InvokeAllMethods(bazScriptableObject, nameof(bazScriptableObject));
        InvokeAllMethods(quxMaterial, nameof(quxMaterial));
        InvokeAllMethods(quuxTexture, nameof(quuxTexture));
    }

    private static void InvokeAllMethods(Object obj, string name)
    {
        var isNativeObjectAlive = InvokeIsNativeObjectAlive(obj);
        Debug.Log(
            $"<color={GetColor(isNativeObjectAlive)}>{name} {IsNativeObjectAlive}: {isNativeObjectAlive}</color>");
        var doesObjectWithInstanceIDExist = InvokeDoesObjectWithInstanceIDExist(obj.GetInstanceID());
        Debug.Log(
            $"<color={GetColor(doesObjectWithInstanceIDExist)}>{name} {DoesObjectWithInstanceIDExist}: {doesObjectWithInstanceIDExist}</color>");
        var cachedPtr = InvokeGetCachedPtr(obj);
        Debug.Log(
            $"<color={GetColor(cachedPtr != IntPtr.Zero)}>{name} {GetCachedPtr}: {cachedPtr}</color>");
    }

    private static string GetColor(bool value)
    {
        return value ? "green" : "red";
    }

    private static bool InvokeIsNativeObjectAlive(Object obj)
    {
        return (bool)typeof(Object)
            .InvokeMember
            (
                IsNativeObjectAlive,
                BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.InvokeMethod,
                null,
                null,
                new object[] { obj }
            );
    }

    private static bool InvokeDoesObjectWithInstanceIDExist(int instanceID)
    {
        return (bool)typeof(Object)
            .InvokeMember
            (
                DoesObjectWithInstanceIDExist,
                BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.InvokeMethod,
                null,
                null,
                new object[] { instanceID }
            );
    }

    private static IntPtr InvokeGetCachedPtr(Object obj)
    {
        var method = typeof(Object)
            .GetMethod
            (
                GetCachedPtr,
                BindingFlags.Instance | BindingFlags.NonPublic,
                null,
                new Type[0],
                null
            );

        return (IntPtr)method.Invoke(obj, null);
    }
}

これはUnityEngine.Objectクラスを継承している

  • GameObject
  • MonoBehaviour
  • ScriptableObject
  • Material
  • Texture

の代表的な5つのクラスのインスタンスに対してDestroyを実行し、実行前と実行後で

  • IsNativeObjectAlive
  • DoesObjectWithInstanceIDExist
  • GetCachedPtr
    の3つのメソッドの戻り値どうなるかを検証するテストです。

このテストを実行すると以下のように出力されました。

スクリーンショット 2022-12-15 235641.png

スクリーンショット 2022-12-15 235654.png

よって、この5つのパターンにおいてはDestroyされることで

  • IsNativeObjectAlive
  • DoesObjectWithInstanceIDExist

falseを返すようになり、GetCachedPtrは0(NULL)を返すようになるということがわかりました。

まとめ

上記の検証により、メソッドの戻り値が破棄前後で以下のように変化することがわかりました。

IsNativeObjectAlive DoesObjectWithInstanceIDExist GetCachedPtr
破棄前 true true インスタンス固有の値
破棄後 false false 0

他にもDestroy

  • アタッチされているコンポーネントに破棄された旨を通知
  • 子オブジェクトに破棄を伝播

などのような処理をしている可能性もありえますが、externなメソッドで実装を見ることは難しいため未検証です。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3