メモリリークを防ぐテンプレ＆検出するエディター拡張

<sup>更新履歴： 名前やタグに依存した Unity の一部機能で起きる LMS & エラーの回避策を追加。IDE エディター上でのエラーチェックをより厳密に。</sup><br/>&nbsp;



`Leaked Managed Shell` って名前が付いたことで問題の認識がしやすくなったのは良いことですね。


👉 **Leaked Managed Shell** とは

- [【Unity】Unityでメモリリーク発生？原因と調査方法について](https://logicalbeat.jp/blog/15426/)
- 似たような事例？ [Unity 特有の不必要なヒープ拡張とその対処法](https://qiita.com/sator_imaging/items/bb4120aa3f3830eab279)


全てのケースでヌル代入が必要なわけじゃないと思うけど、統一してしまった方が楽だろうってことでテンプレートなヘルパーを使う試み。

- [`ManagedShell` クラス](#managedshell-クラス)
    - [テンプレートコード](#テンプレートコード)
- [リークの可能性のあるクラスを自動検出](#リークの可能性のあるクラスを自動検出)
- [付録](#付録)



# `ManagedShell` クラス

このヘルパーを使うと無駄に確保され続けるメモリの解放の他、エディター上でのテストを考慮して `Destroy()` と `DestroyImmediate()` を使い分ける面倒と `DestroyImmediate` 直後の「削除されたオブジェクトにアクセスしようとしてるよ」エラーからも解放される。

<sup>※ [オブジェクトを破棄したフレーム内限定で起きる問題](https://qiita.com/sator_imaging/items/1251fa3a0b51db87a82f)を回避するための処理も行っている</sup>


<!--
<sup>`DestroyImmediate` 直後のエラーは起きたり起きなかったりする。処理速度次第？？ それとも単なる凡ミス？？</sup>
-->


<!--
ヘルパーには１～７つの引数を受ける `Dispose()` と、

```csharp
ManagedShell.Dispose(ref unityObj);
..
ManagedShell.Dispose(ref unityObj1, ...ref unityObj7);
```
-->


ヘルパーには単体でオブジェクトを渡す以外に `T[]` `List<T>` `Dictionary<TKey, TValue>` `ICollection<T>` を渡せるオーバーロードがある。

```csharp
// オブジェクト単体の破棄
ManagedShell.Dispose(ref unityObj);
// 書き込み可能なコレクションは ref で渡せばコレクション自体も破棄する（ヌル代入する
ManagedShell.Dispose(ref Array_or_Collection);
// readonly なコレクションは無印で渡せば全要素を破棄した後に Clear() する（コレクション自体は残る
ManagedShell.Dispose(ReadOnly_Array_or_Collection);
```

<sup>`Dictionary<TKey, TValue>` はどんな型でも突っ込めて `IDisposable` を実装している非 Unity オブジェクトだった場合は `Dispose()` する</sup>

プレハブのルートを GameObject じゃなくて Transform で握ってるような時に使いやすい `DisposeGameObject` メソッドも用意。


```csharp
// Object.Destroy(component.gameObject); して component = null; するイメージ
void DisposeGameObject<T>(ref T component) where T : UnityEngine.Component
// 使い方は Dispose() と同じ
ManagedShell.DisposeGameObject(ref component);
ManagedShell.DisposeGameObject(ref componentArrayOrCollection);
ManagedShell.DisposeGameObject(readonlyComponentCollection);
```

※ `Destroy(Transform)` はエラーは出ずに何も起きないだけだったような？ → [💬](https://github.com/microsoft/Microsoft.Unity.Analyzers/blob/main/doc/UNT0030.md)


## テンプレートコード

プロジェクト間で移植した時の競合を回避するには～～とか、複数のアセンブリから使う場合の切り分けと参照は～～とか、やろうとしてる事の割に面倒が増えないようにテンプレートを使って各アセンブリ（`.dll`）それぞれがヘルパークラスを持つ、って運用にするのが楽。

<!--
> オブジェクトの破棄に時間がかかる、速くしたい！ってことはない（破棄回数を減らすロジックにした方がイイ）ので
-->

> **Note**
> [オブジェクトを破棄したフレーム内限定で起きる問題](https://qiita.com/sator_imaging/items/1251fa3a0b51db87a82f)への対処を行う処理はプリプロセッサーシンボル `MANAGED_SHELL_DISABLE_DEEP_DESTROY` を設定すると無効になる。

> **Note**
> `IDisposable` を実装してるかのチェックもしてる（`OnDestroy` があるからあり得ないか？  
> プリプロセッサーシンボル `MANAGED_SHELL_DISABLE_DISPOSABLE_CHECKS` で無効になる。


<p><details lang="ja" --open><summary><b>ManagedShell</b> テンプレート</summary>

テンプレートファイルを元に新規スクリプトを作るメソッドは

👉 `UnityEditor.ProjectWindowUtil.CreateScriptAssetFromTemplateFile(...)`

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

#ROOTNAMESPACEBEGIN#
    internal static class ManagedShell
    {
        const int LAYER_DEFAULT = 0;
        const string TAG_DEFAULT = "Untagged";
        const string NAME_PREFIX = "__MS_DESTROYED__";

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        static void Dispose_Internal<T>(T obj, float t = 0f)
            where T : UnityEngine.Object
        {
            if (obj == null || obj is Transform)
            {
                //UnityEngine.Debug.LogWarning("[ManagedShell] destroying null or transform doesn't make sense: " + obj);
                return;
            }

#if false == MANAGED_SHELL_DISABLE_DEEP_DESTROY
            // NOTE: as of actual object deletion is happened at the end of frame, GameObject.Find(),
            //       GameObject.FindWithTag() or other Unity native functions unexpectedly retrieve
            //       destroyed objects. thus need to "hide" destroyed object from those functions.
            //       * error could occur if IDisposable.Dispose() is depending on modified properties.
            //         however it's edge case and here is only chance to hide object before destroy.
            if (t <= 0)
            {
                // component.name will change gameObject.name. ignore it.
                if (obj is not Component)
                    // 1) must be unique due to obj.name may be used as dictionary key
                    // 2) empty or short string causes problem when slicing or something w/o bounds check
                    obj.name = NAME_PREFIX + obj.GetHashCode();

                if (obj is GameObject go)
                {
                    go.SetActive(false);
                    // to hide from GetComponentsInChildren<T>(true)
                    go.transform.SetParent(null, false);
                    go.tag = TAG_DEFAULT;
                    go.layer = LAYER_DEFAULT;
                    //go.hideFlags = ;
                }
            }
#endif

#if false == MANAGED_SHELL_DISABLE_DISPOSABLE_CHECKS
            // mono behaviour might implement IDisposable. super ultra rare case.
            if (obj is IDisposable disposable)
            {
                disposable.Dispose();
                if (obj == null)
                    return;
            }
#endif

#if UNITY_EDITOR
            if (Application.IsPlaying(obj))
                UnityEngine.Object.Destroy(obj, t);
            else
                UnityEngine.Object.DestroyImmediate(obj);
#else
            UnityEngine.Object.Destroy(obj, t);
#endif
        }


        ///<summary>NOTE: Do nothing when obj is null or Transform component.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T>(ref T obj, float t = 0f)
            where T : UnityEngine.Object
        {
            Dispose_Internal(obj, t);
            obj = null;
        }

        ///<summary>
        /// This method ensures variable doesn't have reference to deleted component.
        /// Especially useful when destroying GameObject by Transform reference.
        ///</summary>
        ///<remarks>Shorthand for `Destroy(component.gameObject); component = null;`</remarks>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<T>(ref T component, float t = 0f)
            where T : Component
        {
            Dispose_Internal(component.gameObject, t);
            component = null;
        }


        /*  Transform overloads  ================================================================ */

        // NOTE: these overloads are to show error when trying to dispose Transform instance
        const string ERROR_TRANSFORM = "Disposing Transform does nothing. Use DisposeGameObject() instead.";

        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(ref Transform transform, float t = 0f)
        {
            // TODO: [Obsolete("message", true)] sometimes doesn't show error on Visual Studio.
            //       don't throw for workaround.
            //throw new NotSupportedException(ERROR_TRANSFORM);
        }

        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(Transform[] array, float t = 0f) { }
        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(ref Transform[] array, float t = 0f) { }

        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(List<Transform> list, float t = 0f) { }
        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(ref List<Transform> list, float t = 0f) { }

        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(ICollection<Transform> collection, float t = 0f) { }
        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(ref ICollection<Transform> collection, float t = 0f) { }

        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(Dictionary<Transform, Transform> dict, float t = 0f) { }
        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(ref Dictionary<Transform, Transform> dict, float t = 0f) { }

        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose<T>(Dictionary<T, Transform> dict, float t = 0f) { }
        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose<T>(ref Dictionary<T, Transform> dict, float t = 0f) { }

        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose<T>(Dictionary<Transform, T> dict, float t = 0f) { }
        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose<T>(ref Dictionary<Transform, T> dict, float t = 0f) { }


        /*  Dispose collection overloads  ================================================================ */

        ///<summary>For readonly collection to dispose only elements.<br/>Use `ref` overload instead to dispose elements and collection together.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T>(T[] array, float t = 0f)
            where T : UnityEngine.Object
        {
            if (array == null)
                return;

            for (int i = 0; i < array.Length; i++)
            {
                Dispose_Internal(array[i], t);
                array[i] = null;
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T>(ref T[] array, float t = 0f)
            where T : UnityEngine.Object
        {
            Dispose(array, t);
            array = null;
        }

        ///<summary>For readonly collection to dispose only elements.<br/>Use `ref` overload instead to dispose elements and collection together.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T>(List<T> list, float t = 0f)
            where T : UnityEngine.Object
        {
            if (list == null)
                return;

            for (int i = 0; i < list.Count; i++)
            {
                Dispose_Internal(list[i], t);
                //list[i] = null;
            }
            list.Clear();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T>(ref List<T> list, float t = 0f)
            where T : UnityEngine.Object
        {
            Dispose(list, t);
            list = null;
        }

        ///<summary>For readonly collection to dispose only elements.<br/>Use `ref` overload instead to dispose elements and collection together.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T>(ICollection<T> collection, float t = 0f)
            where T : UnityEngine.Object
        {
            if (collection == null)
                return;

            foreach (var item in collection)
            {
                Dispose_Internal(item, t);
            }
            collection.Clear();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T>(ref ICollection<T> collection, float t = 0f)
            where T : UnityEngine.Object
        {
            Dispose(collection, t);
            collection = null;
        }

        ///<summary>For readonly collection to dispose only elements.<br/>Use `ref` overload instead to dispose elements and collection together.</summary>
        ///<remarks>Dispose() will be called if TKey and/or TValue implements IDisposable.</remarks>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<TKey, TValue>(Dictionary<TKey, TValue> dict, float t = 0f)
        {
            if (dict == null)
                return;

            foreach (var key in dict.Keys)
            {
                //value
                if (dict[key] is UnityEngine.Object valOb)
                {
                    Dispose_Internal(valOb, t);
                    //dict[key] = default;
                }
                else if (dict[key] is IDisposable disposable)
                {
                    disposable.Dispose();
                }

                //key
                if (key is UnityEngine.Object keyOb)
                {
                    //dict.Remove(key);
                    Dispose_Internal(keyOb, t);
                }
                else if (key is IDisposable disposable)
                {
                    disposable.Dispose();
                }
            }
            dict.Clear();
        }

        ///<remarks>Dispose() will be called if TKey and/or TValue implements IDisposable.</remarks>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<TKey, TValue>(ref Dictionary<TKey, TValue> dict, float t = 0f)
        {
            Dispose(dict, t);
            dict = null;
        }


        /*  DisposeGameObject collection overloads  ================================================================ */

        ///<summary>For readonly collection to dispose only elements.<br/>Use `ref` overload instead to dispose elements and collection together.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<T>(T[] array, float t = 0f)
            where T : Component
        {
            if (array == null)
                return;

            for (int i = 0; i < array.Length; i++)
            {
                Dispose_Internal(array[i].gameObject, t);
                array[i] = null;
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<T>(ref T[] array, float t = 0f)
            where T : Component
        {
            DisposeGameObject(array, t);
            array = null;
        }

        ///<summary>For readonly collection to dispose only elements.<br/>Use `ref` overload instead to dispose elements and collection together.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<T>(List<T> list, float t = 0f)
            where T : Component
        {
            if (list == null)
                return;

            for (int i = 0; i < list.Count; i++)
            {
                Dispose_Internal(list[i].gameObject, t);
                //list[i] = null;
            }
            list.Clear();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<T>(ref List<T> list, float t = 0f)
            where T : Component
        {
            DisposeGameObject(list, t);
            list = null;
        }

        ///<summary>For readonly collection to dispose only elements.<br/>Use `ref` overload instead to dispose elements and collection together.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<T>(ICollection<T> collection, float t = 0f)
            where T : Component
        {
            if (collection == null)
                return;

            foreach (var item in collection)
            {
                Dispose_Internal(item.gameObject, t);
            }
            collection.Clear();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<T>(ref ICollection<T> collection, float t = 0f)
            where T : Component
        {
            DisposeGameObject(collection, t);
            collection = null;
        }

        ///<summary>For readonly collection to dispose only elements.<br/>Use `ref` overload instead to dispose elements and collection together.</summary>
        ///<remarks>Dispose() will be called if TKey and/or TValue implements IDisposable.</remarks>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<TKey, TValue>(Dictionary<TKey, TValue> dict, float t = 0f)
        {
            if (dict == null)
                return;

            foreach (var key in dict.Keys)
            {
                //value
                if (dict[key] is Component valOb)
                {
                    Dispose_Internal(valOb.gameObject, t);
                    //dict[key] = default;
                }
                else if (dict[key] is IDisposable disposable)
                {
                    disposable.Dispose();
                }

                //key
                if (key is Component keyOb)
                {
                    //dict.Remove(key);
                    Dispose_Internal(keyOb.gameObject, t);
                }
                else if (key is IDisposable disposable)
                {
                    disposable.Dispose();
                }
            }
            dict.Clear();
        }

        ///<remarks>Dispose() will be called if TKey and/or TValue implements IDisposable.</remarks>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<TKey, TValue>(ref Dictionary<TKey, TValue> dict, float t = 0f)
        {
            DisposeGameObject(dict, t);
            dict = null;
        }


        /*  overloads  ================================================================ */

#if false

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T1, T2>(
            ref T1 o1, ref T2 o2, float t = 0f)
            where T1 : UnityEngine.Object
            where T2 : UnityEngine.Object
        {
            Dispose(ref o1, t);
            Dispose(ref o2, t);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T1, T2, T3>(
            ref T1 o1, ref T2 o2, ref T3 o3, float t = 0f)
            where T1 : UnityEngine.Object
            where T2 : UnityEngine.Object
            where T3 : UnityEngine.Object
        {
            Dispose(ref o1, t);
            Dispose(ref o2, t);
            Dispose(ref o3, t);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T1, T2, T3, T4>(
            ref T1 o1, ref T2 o2, ref T3 o3, ref T4 o4, float t = 0f)
            where T1 : UnityEngine.Object
            where T2 : UnityEngine.Object
            where T3 : UnityEngine.Object
            where T4 : UnityEngine.Object
        {
            Dispose(ref o1, t);
            Dispose(ref o2, t);
            Dispose(ref o3, t);
            Dispose(ref o4, t);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T1, T2, T3, T4, T5>(
            ref T1 o1, ref T2 o2, ref T3 o3, ref T4 o4, ref T5 o5, float t = 0f)
            where T1 : UnityEngine.Object
            where T2 : UnityEngine.Object
            where T3 : UnityEngine.Object
            where T4 : UnityEngine.Object
            where T5 : UnityEngine.Object
        {
            Dispose(ref o1, t);
            Dispose(ref o2, t);
            Dispose(ref o3, t);
            Dispose(ref o4, t);
            Dispose(ref o5, t);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T1, T2, T3, T4, T5, T6>(
            ref T1 o1, ref T2 o2, ref T3 o3, ref T4 o4, ref T5 o5, ref T6 o6, float t = 0f)
            where T1 : UnityEngine.Object
            where T2 : UnityEngine.Object
            where T3 : UnityEngine.Object
            where T4 : UnityEngine.Object
            where T5 : UnityEngine.Object
            where T6 : UnityEngine.Object
        {
            Dispose(ref o1, t);
            Dispose(ref o2, t);
            Dispose(ref o3, t);
            Dispose(ref o4, t);
            Dispose(ref o5, t);
            Dispose(ref o6, t);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T1, T2, T3, T4, T5, T6, T7>(
            ref T1 o1, ref T2 o2, ref T3 o3, ref T4 o4, ref T5 o5, ref T6 o6, ref T7 o7, float t = 0f)
            where T1 : UnityEngine.Object
            where T2 : UnityEngine.Object
            where T3 : UnityEngine.Object
            where T4 : UnityEngine.Object
            where T5 : UnityEngine.Object
            where T6 : UnityEngine.Object
            where T7 : UnityEngine.Object
        {
            Dispose(ref o1, t);
            Dispose(ref o2, t);
            Dispose(ref o3, t);
            Dispose(ref o4, t);
            Dispose(ref o5, t);
            Dispose(ref o6, t);
            Dispose(ref o7, t);
        }

#endif

    }

#ROOTNAMESPACEEND#
```

<!------- End of Details JA Tag -------></details></p>



# リークの可能性のあるクラスを自動検出

`UnityEngine.Object` を継承した型をクラスのメンバーとして持っていて、`OnDestroy` もしくは `Dispose` メソッドが無いモノを探し出すエディター拡張を使って検出する。

<!--
大量のクラスが見つかるから `Texture2D` をメンバーとして持っている型だけを探す、とかのが良いのかもしれない。
-->

LMS それ自体はさほどメモリを消費しないが、クラスメンバーとして大きなサイズのバイト列や JSON 文字列などを持っている場合、LMS の発生に伴ってそれらのメモリも解放されないことになるので特に注意したい。

<p><details lang="ja" --open><summary>LMS (Leaked Managed Shell) 発生条件と対処方法</summary>

エディター拡張を使って一覧してみると、基底クラス（`Component`）が持っている `transform` や `rigidbody` その他のフィールドを除いてもかなりの数が該当してしまう。


<!--
GameObject は `transform` のほか様々な `UnityEngine.Object` を継承した型をメンバーとして持っているが問題は起こさない。
-->

LMS はプレハブの階層に含まれるスクリプトの参照を握りっぱなしにしたままプレハブを破棄する、そんなロジックの場合に発生しやすいようなので、普段使いでは全く問題は起きないスクリプトが「そういう使われ方」をしたときにはじめて問題を起こす「かもしれない」という非常に厄介なモノ。

機械的に抽出しようとすると文脈を見て判別しなければならないので一筋縄ではいかないし、差し当たっては「そういう使われ方」をするかもしれないからとりあえず `OnDestroy` `Dispose` を実装しておく、という方針で対処するしかなさそう。

<!------- End of Details JA Tag -------></details></p>


<p><details lang="ja" --open><summary><b>LeakedManagedShellDetector</b> エディター拡張</summary>

- 根こそぎ探す： `メインメニュー > Tools > Detect Leaked Managed Shell Potentials`
- Project パネルで選んだものだけ： `右クリック > Leaked Managed Shell Detector`

<sup>※ コピペして使えるように、自動生成された `Dispose()` メソッドのソースコードをログとして出力する</sup>


```csharp
#if UNITY_EDITOR && UNITY_2021_3_OR_NEWER

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;

internal static class LeakedManagedShellDetector
{
    const string MENU_MAIN = "Tools/Detect Leaked Managed Shell Potentials";
    const string MENU_ASSETS = "Assets/Leaked Managed Shell Detector";

    readonly static BindingFlags BIND_FLAGS
        = BindingFlags.Public
        | BindingFlags.NonPublic
        | BindingFlags.Static
        | BindingFlags.Instance
        | BindingFlags.GetField | BindingFlags.SetField
        | BindingFlags.GetProperty | BindingFlags.SetProperty
        ;

    // both StartsWith and EndsWith are tested on namespaces and assembly (.dll) names
    readonly static string[] IGNORED_NAMESPACES = new string[]
    {
        "UnityEngine.",
        "UnityEditor.",
        "UnityEditorInternal.",
        "Unity.",
        "TMPro.",
        ".Editor",
        "-Editor",
    };
    readonly static string[] DISPOSE_METHODS = new string[]
    {
        "OnDestroy",
        "Dispose",
    };
    readonly static Type[] IGNORED_TYPES = new Type[]
    {
        //typeof(GameObject),
        //typeof(Transform),
    };

    const string INDENT = "    ";
    const string READONLY = "// readonly member is not supported: ";
    const string METHOD_IMPL = "MethodImpl"; //"global::System.Runtime.CompilerServices.MethodImpl";
    const string METHOD_SIGNATURE = "\n[" + METHOD_IMPL + "(" + METHOD_IMPL + @"Options.AggressiveInlining)]
public void Dispose()
{
" + INDENT + @"//<auto-generated>" + nameof(LeakedManagedShellDetector) + @"</auto-generated>
";
    const string LOG_PREFIX = "// LMS Disposer Generator";
    const string MS_DISPOSE = "ManagedShell.Dispose";


    static bool IsUnityObject(Type t)
    {
        if (t.IsSubclassOf(typeof(UnityEngine.Object)))
        {
            return !IGNORED_TYPES.Contains(t);
        }
        else if (t.IsSubclassOf(typeof(Delegate)))
        {
            return false;
        }
        else if (t.IsArray)
        {
            return IsUnityObject(t.GetElementType());
        }
        else if (t.IsGenericType)
        {
            foreach (var generic in t.GetGenericArguments())
            {
                if (IsUnityObject(generic))
                    return true;
            }
        }
        return false;
    }


    static bool Detect(Type cls, out string generatedDisposerMethod)
    {
        bool isCandidate = false;
        var sb = new StringBuilder(METHOD_SIGNATURE);
        foreach (var member in cls.GetMembers(BIND_FLAGS))
        {
            if (member.DeclaringType == typeof(Component) ||
                member.DeclaringType == typeof(ScriptableObject))
                continue;

            if (member is FieldInfo f && IsUnityObject(f.FieldType))
            {
                isCandidate = true;
                if (f.FieldType.IsSubclassOf(typeof(UnityEngine.Object)))
                {
                    sb.AppendLine($"{INDENT}{(f.IsInitOnly ? READONLY : string.Empty)}{MS_DISPOSE}(ref {f.Name});");
                }
                else
                {
                    sb.AppendLine($"{INDENT}{MS_DISPOSE}({(f.IsInitOnly ? string.Empty : "ref ")}{f.Name});");
                }
                //break;
            }
            else if (member is PropertyInfo p && IsUnityObject(p.PropertyType))
            {
                isCandidate = true;
                if (p.PropertyType.IsSubclassOf(typeof(UnityEngine.Object)))
                {
                    sb.AppendLine($"{INDENT}{(!p.CanWrite ? READONLY : string.Empty)}{MS_DISPOSE}(ref {p.Name});");
                }
                else
                {
                    sb.AppendLine($"{INDENT}{MS_DISPOSE}({(!p.CanWrite ? string.Empty : "ref ")}{p.Name});");
                }
                //break;
            }
        }
        sb.AppendLine("}");
        sb.AppendLine("// End of Disposer for " + cls.FullName);
        sb.AppendLine();

        generatedDisposerMethod = sb.ToString();
        return isCandidate;
    }


    [MenuItem(MENU_ASSETS)]
    static void GenerateDisposerMethodForSelectedAssets(MenuCommand cmd)
    {
        if (Selection.objects == null)
            return;

        foreach (var sel in Selection.objects)
        {
            Type cls = null;
            if (sel is MonoScript mono)
            {
                cls = mono.GetClass();
            }

            if (cls == null)
            {
                UnityEngine.Debug.Log("unsupported type: " + sel.name);
                continue;
            }

            if (Detect(cls, out var disposerMethod))
            {
                UnityEngine.Debug.LogWarning($"{LOG_PREFIX}{disposerMethod}");
            }
            else
            {
                UnityEngine.Debug.Log("No leaks: " + cls.FullName);
            }
        }
    }

    static bool IsIgnored(string name)
    {
        foreach (var ignore in IGNORED_NAMESPACES)
        {
            if (name.StartsWith(ignore, StringComparison.Ordinal)
                || name.EndsWith(ignore, StringComparison.Ordinal))
            {
                return true;
            }
        }
        return false;
    }


    [MenuItem(MENU_MAIN)]
    static void DetectLMSPotentials()
    {
        bool detectOnlyUnityOb = EditorUtility.DisplayDialog(nameof(DetectLMSPotentials),
            "Detect only UnityEngine.Object Inheritances?\n\n * Types defined in Packages or DLL are always ignored.",
            "Yes", "No");

        foreach (var assem in AppDomain.CurrentDomain.GetAssemblies())
        {
            if (IsIgnored(assem.FullName[..assem.FullName.IndexOf(',')]))
                continue;

            foreach (var cls in assem.GetTypes())
            {
                if (detectOnlyUnityOb && !cls.IsSubclassOf(typeof(UnityEngine.Object)))
                    continue;

                if (IsIgnored(Path.GetFileName(cls.FullName)))
                    continue;

                if (!Detect(cls, out var disposerMethod))
                    continue;

                //OnDestroy/Dispose?
                bool isDisposed = false;
                foreach (var method in cls.GetMethods(BIND_FLAGS))
                {
                    if (DISPOSE_METHODS.Contains(method.Name))
                    {
                        isDisposed = true;
                        break;
                    }
                }
                if (isDisposed)
                    continue;

                // dirty code!!
                UnityEngine.Object asset = null;
                GameObject tmpGO = null;
                try
                {
                    if (cls.IsSubclassOf(typeof(MonoBehaviour)))
                    {
                        tmpGO = new GameObject();
                        asset = tmpGO.AddComponent(cls);
                        bool isFound = AssetDatabase.TryGetGUIDAndLocalFileIdentifier<Object>(
                            MonoScript.FromMonoBehaviour(asset as MonoBehaviour), out var GUID, out var localId);
                        var path = AssetDatabase.GUIDToAssetPath(GUID);

                        Object.DestroyImmediate(asset);
                        asset = null;
                        if (isFound
                            && !path.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase)
                            && !path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
                        {
                            asset = AssetDatabase.LoadMainAssetAtPath(path);
                        }
                        Object.DestroyImmediate(tmpGO);
                        tmpGO = null;

                        if (path.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase)
                            || path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
                        {
                            //UnityEngine.Debug.LogError("PACKAGE/DLL IGNORED: " + path);
                            continue;
                        }
                    }
                }
                catch
                {
                }
                finally
                {
                    if (tmpGO != null)
                    {
                        Object.DestroyImmediate(tmpGO);
                        tmpGO = null;
                    }
                }

                UnityEngine.Debug.LogWarning(LOG_PREFIX
                    + $" \t {assem.FullName[..assem.FullName.IndexOf(',')]}"
                    + $" \t {cls.FullName}\n"
                    //+ $"//{new string('=', 50)}"
                    + $"{disposerMethod}\n"
                    , asset);
            }
        }
    }

}

#endif
```

<!------- End of Details EN Tag -------></details></p>



# 付録

プロジェクト全体から `.Destroy` `.DestroyImmediate` を検索するための正規表現は 👇

- `[\.\s]+(Destroy|DestroyImmediate)\s*\(`

--

[この問題の発端](http://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/)は Unity で C# と JavaScript が使えた時代に JavaScript 的な `if (obj)` ヌルチェックが出来るようにしたかったからだろうね。

ユーザー目線だと今となっては `?.` `??` `??=` は使えるけど意図した動作をしたりしなかったりするし、もう直しちゃっても良いと思うけどね。公式のサンプルとかアセットストアで販売されている古いアセットが動かなくなると面倒だから難しいのだろうけど。


代わりに [Unity のアナライザー](https://github.com/microsoft/Microsoft.Unity.Analyzers/blob/main/doc/index.md)に Leaked Managed Shell の検出を実装して欲しいところだけど、機械的に判別するのは面倒そう。とりあえず `Destroy` `DestroyImmediate` の全てを対象に警告やエラーを設定できるようにしてくれるだけでも助かるんだけどなー。


[BannedApiAnalyzers](https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md) なんてのもあるけど[ちょっと使うのが面倒](https://qiita.com/su10/items/1c5c70c02e8a4c8a2b9b)なんだ。

--

以上です。お疲れ様でした。