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) を取得できます。
この特性を利用し、以下の手順でボクシングを初回のみに抑えます。
- 型
Tのボックス化インスタンスをヒープ上に 1 つだけ生成して保持する。 - 破棄対象の構造体データを、
Unsafe.Unbox<T>を介してヒープ内の領域へ直接上書きコピーする。 - そのインスタンスに対して
IDisposable.Dispose()を呼び出す。 - 破棄処理による変更内容をスタック上の元の変数へ書き戻す。
これにより、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