IDisposable、非托管字段、引用类型和赋值

IDisposable, unmanaged fields, reference types and assignments

如果我有两个实现 IDisposable 的 class 实例,第一个 class 中的非托管字段会发生什么情况,如果它被分配给第二个。

例如,考虑以下简化的 class:

public unsafe class Image : IDisposable
{
    private float* pixelsBase;

    private GCHandle pixelsHandle;

    public Image(int width, int height)
    {
        this.Width = width;
        this.Height = height;

        // Assign the pointer and pixels.
        this.Pixels = new float[width * height * 4];
        this.pixelsHandle = GCHandle.Alloc(this.Pixels, GCHandleType.Pinned);
        this.pixelsBase = (float*)this.pixelsHandle.AddrOfPinnedObject().ToPointer();
    }

    public float[] Pixels {get; private set;}

    ~ImageBase()
    {
        this.Dispose(false);
    }

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

    protected virtual void Dispose(bool disposing)
    {

        if (disposing)
        {
            // Dispose of any managed resources here.
        }

        if (this.pixelsHandle.IsAllocated)
        {
            this.pixelsHandle.Free();
            this.pixelsBase = null;
        }
    }
}

Image class 实例中的 pixelsBase 字段会发生什么变化?

例如

var firstImage = new Image(100, 200);
var secondImage; new Image(300, 300);

firstImage = secondImage;

IDisposable 与您的问题无关,因为您从不调用它。唯一重要的是终结器——虽然 IDisposable 和终结器是相关的,但 运行time 根本不关心 IDisposable

那么,使用终结器会得到什么?当您的对象不再被引用时,它的终结器将被放入终结器队列,并且除非发生进程终止,否则会在一段时间后执行。在您的情况下,这将释放 GCHandle 并允许收集 Pixels 字节数组。

不要试图用 C# 编写 C++,或者用 C++ 来理解 C#。它们看起来很相似,但非常不同——尤其是在内存管理方面。 C# 终结器与 C++ 析构函数几乎没有共同之处。

作为旁注,您希望避免长时间固定托管对象 - 它会阻止堆压缩正常工作,这意味着除非您的 Pixels 数组在 LOH 上,否则您不会去在收集发生时回收任何固定句柄下方的任何内存(免责声明:这是当前MS.NET 运行时间的实现细节;根据合同,.NET甚至 根本不需要 垃圾收集器,并且不能保证终结器永远 运行,更不用说完成了。

如果您已经在处理指针,那么为您的数组分配非托管内存可能是一个更好的主意。如果您真的想为后备存储使用托管数组,在需要非托管指针的范围内使用 fixed 可能比在整个对象的生命周期内固定整个对象更好。事实上,您的解决方案需要 至少 两个集合来释放对象的内存 - 第一个最终调用 GCHandle.Free,第二个实际回收现在没有的内存固定时间更长。即使你手动调用Dispose,你仍然需要等待一个集合真正回收内存