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

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

https://qiita.com/tanikura/items/b57f6b92371026307c0b

# 前回のおさらい

* `UnityEngine.Object`では等値演算子`==`(及び非等値演算子`!=`)がオーバーロードされている
* オーバーロードされた等値演算子では`Object.CompareBaseObjects`メソッドに判定処理を委譲している

```CS
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`メソッドが実行されている

```CS
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による破棄](https://qiita.com/tanikura/items/b57f6b92371026307c0b#destroy%E3%81%AB%E3%82%88%E3%82%8B%E7%A0%B4%E6%A3%84)というセクションでは

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

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

```CS: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987563/8d527fad-4ada-dc46-6359-7bf25e680576.png)

![スクリーンショット 2022-12-15 235654.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987563/35f216b7-07d4-2682-2d1c-e4296ff0ca1c.png)

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

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

# まとめ

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

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


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

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