IntPtr与ref C#的区别

11

我需要将非托管的dll导入到我的C#应用程序中,我想知道IntPtr和ref之间的区别,你推荐我使用哪种,并解释原因?请注意,这两种方法对我都有效。

[DllImport("mydll.dll", CallingConvention=CallingConvention.Cdecl)]
static extern Result Init(IntPtr versionInfo);

 [DllImport("mydll.dll", CallingConvention=CallingConvention.Cdecl)]
 public static extern Result Init(ref Version versionInfo);
4个回答

5
如果Version是与外部Init函数所需的结构体兼容的结构体,则两者之间没有显着差异,除了ref版本将更容易从C#中使用,因为运行时将管理所有的封送和固定。除非你真的想要做所有这些工作,否则我建议你选择ref选项。
当然,如果没有看到C函数原型、C#中的Version结构体以及用于该参数的C中的结构体,我只能猜测。

2
有一个可能很重要的区别:使用IntPtr允许将IntPtr.Zero作为值传递,而ref Version则不允许。因此,根据本机代码所需的内容,需要IntPtr重载并不罕见。 - jonp
3
在这种情况下,你需要同时拥有两个版本(一个重载函数),这样当你有一个Version时就可以通过引用传递它,而当你想要传递null时就可以传递IntPtr.Zero。或者将Version改为类而不是结构体,这样就可以传递null了(不再需要使用ref关键字,但在类上需要使用P/Invoke注解属性)。 - Ben Voigt

3

编辑:基于一些批评,我不得不重新思考并进一步研究这个问题。我自由地承认IntPtr容易出错,对于“更难”的含义可能存在争议,但如果框架能够自动驱动它并防止你不得不固定事物等,那么如果你不使用IntPtr,则我认为我同意IntPtr会更加“困难”。在进一步研究和思考之后,我认为在强制执行P/Invoke且无法创建C++/CLI包装器时,在托管代码中声明您的结构体(在您的情况下是Version),然后通过ref参数传递它会更容易。

[StructLayout(LayoutKind.Sequential)]
struct Version
{
    // Data members
}

[DllImport("mydll.dll", CallingConvention=CallingConvention.Cdecl)]
public static extern Result Init(ref Version versionInfo);

我猜想您需要使用IntPtr版本。区别在于,IntPtr是管理指向内存的指针的类,而在参数列表中声明参数为引用(ref关键字)意味着您正在通过引用传递。一般情况下,当通过P/Invoke调用从非托管dll传递数据时,除了传递的数据是基本类型(int、double、string-带有适当的Marshal修饰)之外,我都将数据传递为IntPtr。然后,您可以将IntPtr进行封送处理到非托管结构的托管声明中。
更多有关P/Invoke调用的信息,请查看以下链接: http://msdn.microsoft.com/en-us/magazine/cc164123.aspx

很不幸,您正在用困难而容易出错的方式进行事情。更不幸的是,您还在教导其他人这样做。 - Ben Voigt
@Ben,我很高兴学习一种更简单/更少出错的方法来做这件事。你有链接或建议的阅读材料吗?我不花太多时间P/Invoking东西,大部分时间我会写一个C++/CLI包装器,但当我必须使用P/Invoke时,我只使用ref参数来处理int、string(在某些情况下是StringBuilder),然后对于其他的东西使用IntPtr。 - pstrjds
1
C++/CLI为胜利而生 :) 对于那些无法使用它的场合(如Windows CE),或者无法确定是否安装了Visual C++运行时库的情况,可以参考http://pinvoke.net上提供的大量示例。通常情况下,不仅包括像“int”和“double”这样的基本类型可以在p/invoke签名中安全使用,还包括由基本类型组成的用户定义类型。例如,“POINT”、“POINTF”、“RECT”、“FONTMETRIC”、“BITMAPFILEHEADER”等。 - Ben Voigt
@Ben 看起来我在写我的新抱怨时你正在帮忙,我的错!谢谢你的信息,当我需要进行 API 级别的操作并且必须 P/Invoke 时,我实际上会参考 pinvoke.net。我想我明白你的意思了,我读到另一篇文章指出了在托管环境中声明结构体(使用 SequentialLayout 属性),然后让环境使用 "ref" 关键字进行封送的能力。感谢你让我进一步研究。 - pstrjds

2

IntPtr可以让您将所有的Externs移到一个类中,而不依赖于在调用中使用的结构体,在其他地方可能定义。我喜欢将api适配器与数据结构/类定义分开,这似乎更自然地放在下一层。


-1
最好使用 IntPtr,因为这样可以使您的代码在编译到 x64 时更加灵活。 IntPtr 会自动扩展其大小。
IntPtr = 4 bytes on x86
IntPtr = 8 bytes on x64

在C语言中,有些类型在编译为x64时会将其大小加倍,但是对应的C#类型(声明为PInvoke)不会这样做。这时函数调用就会失败。如果Version类包含这样的字段,那么最好使用IntPtr


ref将传递一个指向指针大小的参数。你回答的其余部分似乎是在谈论“版本”结构的内部,而不是参数本身。虽然“版本”的某些字段可能应该具有类型“IntPtr”,但这并不是不在p/invoke声明中使用“ref Version”的理由。 - Ben Voigt
@Ben,你没有理解我的想法,尽管我认为我表达得很清楚。我并没有说使用ref不起作用,但是在我看来,我会使用IntPtr。但是当你不知道答案时,谢谢你的投票。 - Liviu Mandras
@Liviu:我已经投了反对票(显然不只是我一个人),因为你的答案是错误的。你说“IntPtr会让你的代码更灵活”,涉及指针大小,这是完全错误的。在每个平台上,refIntPtr的大小都是相同的。 - Ben Voigt
@Ben:你完全错了。看看这个链接,自己去验证一下:http://msdn.microsoft.com/en-us/library/system.intptr.size.aspx/ 所有对我进行负面评价的人都应该查看这个链接。我已经厌倦了这个问题。 - Liviu Mandras
@Liviu:我从未说过IntPtr的大小不会改变。我说过,一个ref参数也会被转换为正确大小的指针。因此,使用IntPtr没有任何优势。可以这样说:“IntPtr将使您的代码在指针大小方面更加灵活”,但是你说IntPtr相对于ref会使您的代码更加灵活,这是错误的。 - Ben Voigt
你的回答的后半部分也是错误的。在函数参数中使用 IntPtr 不会改变 Version 内任何字段的大小。只有在 Version 内部使用 IntPtr 才会这样做,而这在 p/invoke 声明中根本不会显示出来。 - Ben Voigt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接