跳到正文
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)。

利用这一特性,可以通过以下步骤将装箱限制为只在第一次发生。

  1. 在堆上只生成并保留一个类型为 T 的装箱实例。
  2. 通过 Unsafe.Unbox<T> 将待丢弃的结构体数据直接覆盖复制到堆内的区域中。
  3. 对该实例调用 IDisposable.Dispose()。
  4. 将销毁处理所做的更改写回栈上的原变量。

由此,从第二次及之后的调用开始,将完全不再产生新的 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