跳到正文
Qiita · Unity 开发社区· 来源· Omochaya·· 4 小时前AI 评分52

【C# / Unity】无类型约束下零分配调用 Dispose 的方法

【C# / Unity】型制約なしゼロアロケでDisposeする方法

正文 · 原文

1. DisposeHelperの必要性

汎用コレクションやオブジェクトプールなどの汎用コンテナを実装する際、「中身の型 T が IDisposable を実装していれば破棄したい」というシチュエーションがよくあります。

しかし、通常の実装では以下のジレンマに直面します。

  • where T : IDisposable の型制約をつけると、IDisposable を実装していない一般的な型(通常の構造体やクラス)をコンテナに格納できなくなる。
  • 型制約を外して (IDisposable)item や item as IDisposable を呼ぶと、T が構造体(値型)だった場合に毎回ボクシング(GC Alloc)が発生し、Unity ではフレームレート低下や GC スパイクの原因になる。

「型制約をつけずに任意の型を受け入れつつ、構造体のボクシングを発生させずに Dispose を呼ぶ」ための仕組みが必要になります。

2. Unsafe.Unbox による解決アプローチ

通常のアンボックスキャスト (T)boxed は、ヒープにあるデータからスタック上に値のコピーを取り出します。

一方、System.Runtime.CompilerServices.Unsafe.Unbox<T> は、ヒープ上のボックス化領域への直接の参照(ref T) を取得できます。

この特性を利用し、以下の手順でボクシングを初回のみに抑えます。

  1. 型 T のボックス化インスタンスをヒープ上に 1 つだけ生成して保持する。
  2. 破棄対象の構造体データを、Unsafe.Unbox<T> を介してヒープ内の領域へ直接上書きコピーする。
  3. そのインスタンスに対して IDisposable.Dispose() を呼び出す。
  4. 破棄処理による変更内容をスタック上の元の変数へ書き戻す。

これにより、2 回目以降の呼び出しでは新たな GC アロケーションが一切発生しなくなります。

3. 実装コード

再帰呼び出し(ツリー構造の破棄)への対策としてボックスを Stack で管理し、参照型フィールドの GC リーク防止や、クラスと構造体のオーバーロード解決も含めた完成コードです。

Unity の Allow 'unsafe' Code は無効のままで動作します。
(検証環境: Unity 6.3)

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

public static class DisposeHelper
{
    private static class DisposeStructHelper<T> where T : struct
    {
        private static readonly Stack<IDisposable> Stack = new(1);

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void Dispose(ref T item)
        {
            if (!Stack.TryPop(out var disposer))
            {
                T dummy = default;
                disposer = dummy as IDisposable; // ここでボクシング
            }

            // キャッシュしたボックスを再利用して、
            // 対象(item)のDisposeを処理して反映する。
            Unsafe.Unbox<T>(disposer) = item;
            try
            {
                disposer.Dispose();
                item = Unsafe.Unbox<T>(disposer);
            }
            finally
            {
                // itemが参照型フィールドを保持していた場合のメモリリークを防止
                Unsafe.Unbox<T>(disposer) = default;
                Stack.Push(disposer);
            }
        }
    }

    // クラスと構造体で同一シグネチャのオーバーロードを作るためのマーカー
    public readonly struct ClassMarker { }

    // 型判定のキャッシュ(未実装型での不要な Stack 生成とリフレクション負荷を防止)
    private static class TypeTraits<T> where T : struct
    {
        public static readonly bool IsDisposable = typeof(IDisposable).IsAssignableFrom(typeof(T));
    }

    #region Dispose (破棄のみ)

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void Dispose<T>(ref T item) where T : struct
    {
        if (TypeTraits<T>.IsDisposable)
        {
            DisposeStructHelper<T>.Dispose(ref item);
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void Dispose<T>(ref T item, ClassMarker _ = default) where T : class
    {
        if (item is IDisposable disposable) { disposable.Dispose(); }
    }

    // プロパティ等の破棄用(値渡し)
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void Dispose<T>(T item) where T : class
    {
        if (item is IDisposable disposable) { disposable.Dispose(); }
    }

    #endregion

    #region SafeDispose (破棄 + 初期化)

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void SafeDispose<T>(ref T item) where T : struct
    {
        Dispose(ref item);
        item = default;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void SafeDispose<T>(ref T item, ClassMarker _ = default) where T : class
    {
        Dispose(ref item);
        item = null;
    }

    #endregion
}

実装の要点

  • 再帰呼び出し対策: ボックスインスタンスを単一の static フィールドではなく Stack<IDisposable> で保持することで、ツリー構造のオブジェクト破棄などで処理が再帰してもデータ破壊が起きません。
  • メモリリーク対策: finally 句でボックス領域を default で上書きし、T がクラス参照を持っていた場合に GC 対象から外れるのを防いでいます。
  • オーバーロード解決: where T : struct と where T : class は本来同一シグネチャで多重定義できませんが、クラス側にデフォルト引数 ClassMarker _ = default を付与することで、呼び出し側は引数 1 つのままコンパイル時に解決させています。
  • スレッドセーフについて: スタックを静的領域に確保しているため Unity のメインスレッド等シングルスレッド前提です。マルチスレッド環境で共用する場合は [ThreadStatic] やスレッドごとの管理が必要です。

4. まとめ

  • Unsafe.Unbox<T> でヒープ上のボックス領域を直接書き換えることで、構造体のボックス化を初回の 1 回に抑えられます。
  • 型制約なし・ゼロアロケーション・再帰安全を同時に達成できるため、低レイヤなコンテナやメモリ管理機構の実装に有用です。
  • Unsafe.Unbox はポインタ構文を使わないため、Unity プロジェクトで Allow 'unsafe' Code を有効にする必要はありません。
  • IDisposable に限らず、例えば自前のインターフェイス向けに同様のヘルパーを用意しても役に立つ場面はありそうです。

来源:Qiita · Unity 开发社区 · qiita.com