在阅读了这两个内容后,包括本网站上的高投票答案,我仍然觉得有点不清楚。
由于我对此事的理解可能有误,我将首先发布我所知道的概要,以便我可以纠正,如果我错了,然后发布我的具体问题:
有时在编写托管代码时,我们必须将地址传递给非托管代码。这就是IntPtr的用途。但是,我们尝试确定两个相反的事情:a)从GC保持指针(到一个地址)。 b)在不需要时释放它(即使我们忘记明确地这样做)。
HandleRef执行第一个操作,SafeHandle执行第二个操作。 (我实际上在这里指的是SafeHandle列出here)的推导。
我的问题:
答案 0 :(得分:6)
我认为你把指针(IntPtr
或void*
)与句柄(对Windows对象的引用)混淆了。不幸的是,句柄可以用IntPtr
类型表示,这可能令人困惑。
SafeHandle
专门用于处理句柄。句柄不是指针,而是系统提供的表中的索引(有点 - 它意味着不透明)。例如,CreateFile
函数返回HANDLE
,适合与SafeFileHandle
一起使用。 SafeHandle
类本身就是Windows句柄的包装器,它将在SafeHandle
完成后释放Windows句柄。因此,只要您想使用句柄,就必须确保保留对SafeHandle
对象的引用。
指针只是一个值。它是内存中对象的地址。 IntPtr
是struct
,struct
语义将使其按值传递(也就是说,每次将IntPtr
传递给函数时,您实际制作副本IntPtr
)。除非装箱,否则GC甚至不会知道您的IntPtr
。
HandleRef
文档的重要部分是:
HandleRef
构造函数有两个参数:表示包装器的Object
和表示非托管句柄的IntPtr
。 interop编组程序仅将句柄传递给非托管代码,并保证包装器(作为第一个参数传递给HandleRef的构造函数)在调用期间保持活动状态。
我们来看MSDN example:
FileStream fs = new FileStream("HandleRef.txt", FileMode.Open);
HandleRef hr = new HandleRef(fs, fs.SafeFileHandle.DangerousGetHandle());
StringBuilder buffer = new StringBuilder(5);
int read = 0;
// platform invoke will hold reference to HandleRef until call ends
LibWrap.ReadFile(hr, buffer, 5, out read, 0);
Console.WriteLine("Read with struct parameter: {0}", buffer);
LibWrap.ReadFile2(hr, buffer, 5, out read, null);
Console.WriteLine("Read with class parameter: {0}", buffer);
这相当于:
FileStream fs = new FileStream("HandleRef.txt", FileMode.Open);
var hf = fs.SafeFileHandle.DangerousGetHandle();
StringBuilder buffer = new StringBuilder(5);
int read = 0;
LibWrap.ReadFile(hf, buffer, 5, out read, 0);
Console.WriteLine("Read with struct parameter: {0}", buffer);
LibWrap.ReadFile2(hf, buffer, 5, out read, null);
Console.WriteLine("Read with class parameter: {0}", buffer);
// Since we no more have a HandleRef, the following line is needed:
GC.KeepAlive(fs);
但在这种特殊情况下,更好的解决方案是:
using(FileStream fs = new FileStream("HandleRef.txt", FileMode.Open))
{
StringBuilder buffer = new StringBuilder(5);
int read = 0;
LibWrap.ReadFile(fs.SafeFileHandle, buffer, 5, out read, 0);
Console.WriteLine("Read with struct parameter: {0}", buffer);
LibWrap.ReadFile2(fs.SafeFileHandle, buffer, 5, out read, null);
Console.WriteLine("Read with class parameter: {0}", buffer);
}
总结一下:
对于句柄,请使用SafeHandle
并确保它不再可用,直到您不再需要它为止,此时您要么让GC收集它,要么显式处理它(通过调用{{ 1}}方法)。
对于指针,确保指向内存在本机代码可以访问它的整个时间内固定。您可以使用Dispose()
关键字或固定fixed
来实现此目标。
GCHandle
是IntPtr
,因此GC不会收集它。
这不是收集的struct
,而是IntPtr
对象暴露它,此时不再可以访问,并且可由GC收集。完成后,它会处理句柄。
referenced answer的代码是:
HWnd
对于对象可达性规则,只要对象没有更多可达参考,就认为对象不再使用。在前面的示例中,在HWnd a = new HWnd();
IntPtr h = a.Handle;
// The GC can kick in at this point and collect HWnd,
// because it's not referenced after this line.
// If it does, HWnd's finalizer could run.
// If it runs, HWnd will dispose the handle.
// If the handle is disposed, h will hold a freed handle value,
// which is invalid. It still has the same numerical value, but
// Windows will already have freed the underlying object.
// Being a value type, h itself has nothing to do with the GC.
// It's not collectable. Think of it like it were an int.
B.SendMessage(h, ...);
// Adding GC.KeepAlive(a) here solves this issue.
行之后,IntPtr h = a.Handle;
变量之后没有其他用法,因此假设此对象不再使用,可以随时释放。 a
创建了这样的用法,因此对象保持活动状态(由于JIT完成了使用情况跟踪,因此实际情况会更复杂一些,但这对于此解释来说已经足够了。)
SafeHandle 不包含像HandleRef这样的安全措施。正确的吗?
好问题。我想P / Invoke封送程序会在调用期间保持句柄处于活动状态,但是如果它已经完成,它的拥有对象(如GC.KeepAlive(a)
)仍然可以在调用期间显式处理它。这是HWnd
提供的安全措施,您无法单独使用HandleRef
。您需要确保句柄所有者(前一个示例中的SafeHandle
)自己保持活动状态。
但HWnd
的主要目标是包装HandleRef
,这是存储句柄值的旧方法。现在,无论如何,IntPtr
首选SafeHandle
来处理句柄存储。您只需确保句柄所有者在P / Invoke调用期间不会显式处理句柄。