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
,你仍然需要等待一个集合真正回收内存
如果我有两个实现 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
,你仍然需要等待一个集合真正回收内存