Qiita · Unity 开发社区· 来源· Omochaya·· 3 小时前AI 评分52
【C# / Unity】无类型约束下零分配调用 Dispose 的方法
【C# / Unity】型制約なしゼロアロケでDisposeする方法
正文 · AI 翻译
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的装箱实例。 - 通过
Unsafe.Unbox<T>将待丢弃的结构体数据直接覆盖复制到堆内的区域中。 - 对该实例调用
IDisposable.Dispose()。 - 将销毁处理所做的更改写回栈上的原变量。
由此,从第二次及之后的调用开始,将完全不再产生新的 GC 分配。
3. 实现代码
作为对递归调用(树结构的销毁)的对策,用 Stack 管理 box,并包含防止引用类型字段的 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>来保持 box 实例,即使在销毁树形结构对象等过程中处理发生递归,也不会造成数据破坏。 -
内存泄漏对策:在
finally语句中用default覆盖 box 区域,防止在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