C# 使用 GCHandle-Member 实现 IDisposable

C# implementing IDisposable with GCHandle-Member

鉴于实施 class 包含 GCHandle-成员,实施 IDisposable-模式的正确方法是什么?

我想到了这个,但它导致我的应用程序内存泄漏:

public class foo : IDisposable
{
     private bool disposed = false;
     private readonly byte[] bytes;
     private readonly GCHandle hBytes;
     private readonly IDisposable someWrapperForUnmanagedData;

     public foo(byte[] bytes)
     {
          this.bytes = bytes;
          hBytes = GCHandle.Alloc(bytes, GCHandleType.Pinned);

          someWrapperForUnmanagedData = new bar(..., bytes, ...);
     }

     protected virtual void Dispose(bool disposing)
     {
          if (disposed) return;
          if (disposing)
          {
               hBytes.Free();
          }
          someWrapperForUnmanagedData.Dispose();
          disposed = true;
     }

     public void Dispose()
     {
          Dispose(true);
          GC.SupressFinalize(this);
     }

     ~foo() => Dispose(false);
}

Microsoft 文档没有提到 GCHandles 应该如何与 IDisposable 一起使用,而且我似乎无法在网上找到任何内容。

foo 的终结器是否应该调用 hBytes.Free()? 按照文档,终结器只应该为非托管资源调用清理例程,在这种情况下仅为 someWrapperForUnmanagedData.

您的 Dispose 方法倒退了。

  1. 当你的类型的终结器被调用时,你应该释放你拥有的托管资源。
  2. 当您的类型的 IDisposable.Dispose() 方法被调用时,您应该对您拥有的事物调用 Dispose,并释放您拥有的托管资源。

原因很简单。令人高兴的情况是您的类型已被处置(有人对其调用 .Dispose())。这使您有机会释放您拥有的非托管资源,并且您还需要将此 Dispose() 调用向下传播到您的 children,以便他们也可以释放其非托管资源。

如果有人忘记对你调用 .Dispose(),那么 GC 可能会通过调用你的 finalize 方法来挽救你。在这种情况下,您应该释放您拥有的任何非托管资源,但您不应该调用任何 children。如果您的任何 children 有自己的终结器,那么:

  1. GC会单独调用它们的finalizer,所以你不应该也调用它。
  2. 无法保证类型最终确定的顺序,因此您的 children 可能已经完成。

注意术语所有权的使用。一个非托管资源应该只有一个拥有它的东西,那就是负责释放它的东西。

请注意,您的 someWrapperForUnmanagedData 而非 非托管类型。这是一个 C# class,非常易于管理。非托管类型类似于原始指针。如果 someWrapperForUnmanagedData 本身拥有一些其他非托管类型,它应该实现 IDisposable 并定义一个终结器以确保它们被释放。

GCHandle 没有自己的终结器,因此它无法被 GC Free() 编辑。因此,它在这里算作非托管资源:您需要手动对其调用 Free()

所以:

     protected virtual void Dispose(bool disposing)
     {
          if (disposed) return;

          if (disposing)
          {
               someWrapperForUnmanagedData.Dispose();
          }
          hBytes.Free();
          
          disposed = true;
     }

请注意,正如 this doc 明确指出的那样,如果可能,您应该使用 SafeHandle(或其子 class 之一)。 SafeHandle 实现了自己的终结器,因此您不必这样做。

运行时也知道 SafeHandle,这让它避免了一些非常讨厌的竞争条件,例如您的类型可以在进行非托管调用时同时完成,从而导致严重的崩溃。如果您不了解这场比赛,这意味着您绝对应该使用 SafeHandle.