【Unity】Destroyしたらnull代入を同時にする

以下の Unity 公式の Tweet を見て思ったことです。

Twitterから:

Unity でオブジェクトを Destroy したら null を代入するのが望ましいです。既に null が入っているように見えますが、実はこれ、本当の null ではないんです。この状況 (Leaked Managed Shell) とメモリプロファイラを使った対策について動画で解説してみました。 https://youtu.be/UIwQmpQTtA4

メモリプロファイラー の Leaked Managed Shell の使い方 (Memory Profiler パッケージを別途追加 > Window > Analysis > Memory Profiler > All Of Memory > 検索ボックスに leaked で破棄忘れが検索できる)

これはさておき、ゲームオブジェクトを Destroy したフレーム内で null 判定すると null でないと言われるので null 代入は基本ですが、気をつけましょうで済ますと忘れてしまうので確実に null 代入できる仕組みを考えてみました。

// UnityObjectExtensions.cs


using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;

/// <summary>
/// <see cref="Object"/> の機能を拡張します。
/// </summary>
public static class ObjectUtil
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void SafeDestroy(ref Object target)
    {
        Object.Destroy(target);
        target = null;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void SafeDestroy(ref Object target, float t)
    {
        Object.Destroy(target, t);
        target = null;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void SafeDestroy<T>(T[] array) where T : UnityEngine.Object
    {
        if (array == null) return;
        for (int i = 0; i < array.Length; i++)
        {
            Object.Destroy(array[i]);
            array[i] = null;
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void SafeDestroy<T>(ICollection<T> collection) where T : UnityEngine.Object
    {
        if (collection == null) return;
        foreach (Object obj in collection)
        {
            Object.Destroy(obj);
        }
        collection.Clear();
    }
}

使い方は以下の通り。

// Text.cs

MyScript _script;
public void Foo()
{
    ObjectUtil.SafeDestroy(ref _script);
    // ここで _script は自動的に null になってる
}

readonly List<MyScript> _list = new();
public void Bar()
{
    ObjectUtil.SafeDestroy(_list);
    // ここで _list の内容は全部開放 & リストも空になってる
}

とはいえ、以下のように別の変数に代入されていたら同一フレーム内で _obj2 は手動で null にする以外で、通常の方法ではどうやっても判定不可能のため注意してください。

[SerializeField] GameObject _obj1;
GameObject _obj2;

private void Awake()
{
    _obj2 = _obj1;
}

private void Delete()
{
    ObjectUtil.SafeDestroy(ref _obj1);
    if (_obj1)
    {
        // こっちは入らない
    }
    if (_obj2)
    {
        // こっちは入る(自分でnull代入しない限り判断できない
    }
}

もしこうなるときは Destroy する前に _obj.SetActive(false); でオブジェクトを無効化して、if (_obj.activeSelf) などで判定するか、そのフレームで判定しなくても良いように設計を見直すことになると思います。