什么时候在C#/。NET中使用指针?


76

我知道C#使程序员能够在不安全的上下文中访问和使用指针。但是什么时候需要?

在什么情况下使用指针变得不可避免?

是否仅出于性能原因?

同样,为什么C#通过不安全的上下文公开此功能,并从中删除所有托管的优势?从理论上讲,可以使用指针而不损失托管环境的任何优势吗?


感谢理查德(Richard),只是想通过询问(更多)问题来学习更多:O
Joan Venge

3
这个问题很可能是你的兴趣:stackoverflow.com/questions/584134/...
汉斯·奥尔森

Answers:


91

什么时候需要?在什么情况下使用指针变得不可避免?

如果接受管理的安全解决方案的净成本是不可接受的,但是不安全解决方案的净成本是可以接受的。您可以通过从总成本中减去总收益来确定净成本或净收益。不安全的解决方案的好处是,“不要浪费时间进行不必要的运行时检查以确保正确性”;成本是(1)即使在关闭安全管理系统的情况下也必须编写安全的代码,以及(2)必须处理可能降低垃圾收集器效率的问题,因为它无法在具有非托管指针的内存周围移动。它。

或者,如果您是编写编组层的人。

是否仅出于性能原因?

出于性能以外的原因,使用托管语言中的指针似乎是不正确的。

在大多数情况下,您可以使用Marshal类中的方法来处理非托管代码的互操作。(在某些情况下,很难或不可能使用编组工具来解决互操作性问题,但我不知道。)

当然,正如我说的那样,如果您是编写Marshal班的人,那么显然您不会使用编组层来解决您的问题。在这种情况下,您需要使用指针来实现它。

为什么C#通过不安全的上下文公开此功能,并从中删除所有托管的优势?

这些管理优势带来了性能成本。例如,每次您向数组请求第十个元素时,运行时都需要进行检查以查看是否存在第十个元素,如果没有第十个元素,则抛出异常。使用指针可以消除运行时成本。

相应的开发人员成本是,如果您做错了,那么您将要处理内存损坏错误,这些错误会格式化硬盘并在一小时后使您的进程崩溃,而不是在错误时处理一个干净的异常。

从理论上讲,可以使用指针而不会失去托管环境的任何优势吗?

我认为“优势”是指诸如垃圾收集,类型安全和引用完整性之类的优势。因此,您的问题本质上是“理论上是否可以关闭安全系统,但仍然可以获得打开安全系统的好处?” 不,显然不是。如果您因为不喜欢该安全系统而关闭该安全系统,那么您将无法获得启用它的好处!


感谢Eric的回复。您能告诉我“参照完整性”是什么意思吗?使用引用而不是指针吗?
Joan Venge

9
@Joan:每个引用实际上都指向有效的东西或为null。指针不具有该属性。指针可能指向毫无用处的内存。但是托管引用具有该属性。如果您有对字符串的引用,则该值始终为null或有效字符串;您可以保证不会出现对非有效字符串的非null引用的情况。
埃里克·利珀特

谢谢Eric,我现在明白了。
Joan Venge

2
@masoudkeshavarz:不。使用托管指针,不可能伪造指向任意内存的指针。对于不安全代码中的非托管指针,好吧,我们只能说它们由于某种原因被称为“非托管”和“不安全” 。您可以使用不安全代码中的非托管指针来执行任何您想做的事情,包括破坏.NET运行时数据结构。
埃里克·利珀特

1
天哪,一个小时以来一直在寻找一个明确的no-bs答案,这真是太神奇了。谢谢!
krivar

17

指针是与托管的垃圾收集环境固有的矛盾。
一旦您开始处理原始指针,GC将不知道发生了什么。

具体来说,它不知道对象是否可达,因为它不知道指针在哪里。
它也不能在内存中移动对象,因为那样会破坏指针。

所有这些都可以通过GC跟踪的指针解决。那就是引用。

您仅应在凌乱的高级互操作方案中或高度复杂的优化中使用指针。
如果必须要问,您可能不应该问。


7
为+1,如果您必须提出要求,则可能不应该这样做。极好的建议:-)
Darin Dimitrov'3

1
您的结论是正确的,但您的大多数解释都是错误的。从垃圾收集器的角度来看,指针和引用没有什么不同。当指针或引用存储在无类型的存储区中时,导致GC崩溃的原因是,GC不再知道它只是数字值还是托管对象的地址。
Ben Voigt

1
@SLaks:我不是说引用和指针没有什么不同,我说从垃圾收集器的角度来看它们没有什么不同。GC不在乎是采用数组元素的地址,还是从指向另一个元素的指针开始并进行算术运算以找到您现在要指向的那个元素。
Ben Voigt

1
@SLaks:即使在本机C和C ++中,指针算法也仅允许在单个对象/分配(例如数组)的范围内。垃圾收集器无论如何将整个对象移动到一起,指针不会中断。
Ben Voigt

3
@SLaks:很好。顺便说一句,您假设的GC跟踪指针确实存在于其他.NET语言中(尽管有一些限制-它只能是一个自动变量),并且它支持算术运算法则:interior_ptr
Ben Voigt

5

GC可以移动参考。使用不安全会将对象保留在GC的控制范围之外,并避免这种情况。“固定”固定对象,但让GC管理内存。

根据定义,如果您有一个指向对象地址的指针,并且GC对其进行了移动,则该指针不再有效。

关于为什么需要指针:主要原因是要使用非托管DLL,例如用C ++编写的DLL。

还要注意,当您固定变量并使用指针时,您更容易受到堆碎片的影响。


编辑

您已经谈到了托管代码与非托管代码的核心问题...内存如何释放?

您可以按照描述的那样混合代码以提高性能,只是不能使用指针跨越托管/非托管边界(即,不能在“不安全”上下文之外使用指针)。

至于它们如何清洁……您必须管理自己的记忆;使用(希望)创建/分配了指针指向的对象(通常在C ++ DLL中)CoTaskMemAlloc(),并且您必须以相同的方式释放该内存(调用)CoTaskMemFree(),否则会发生内存泄漏。请注意,只能CoTaskMemAlloc()使用释放分配给的内存CoTaskMemFree()

另一种选择是从您的本机C ++ dll中公开一个方法,该方法使用一个指针并释放它...这使DLL可以决定如何释放内存,如果它使用其他某种方法来分配内存,则效果最佳。您使用的大多数本机dll是无法修改的第三方dll,并且它们通常没有(我已经看到)这样的函数要调用。

此处获取释放内存的示例:

string[] array = new string[2];
array[0] = "hello";
array[1] = "world";
IntPtr ptr = test(array);
string result = Marshal.PtrToStringAuto(ptr);
Marshal.FreeCoTaskMem(ptr);
System.Console.WriteLine(result);


更多阅读材料:

C#取消分配IntPtr引用的内存 第二个答案向下介绍了不同的分配/取消分配方法

如何在C#中释放IntPtr? 加强了以与分配内存相同的方式取消分配的需要

http://msdn.microsoft.com/zh-cn/library/aa366533%28VS.85%29.aspx 有关分配和取消分配内存的各种方法的MSDN官方文档。

简而言之...您需要知道如何分配内存才能释放它。


编辑 如果我正确地理解了您的问题,那么简短的答案是肯定的,您可以将数据交给非托管指针,在不安全的上下文中使用它,并在退出不安全的上下文后使数据可用。

关键是您必须将要引用的托管对象固定在一个fixed块上。这样可以防止您要引用的内存在unsafe块中时被GC移动。这里涉及到许多微妙之处,例如,您不能重新分配在固定块中初始化的指针...如果您真的打算管理自己的代码,则应该阅读不安全和固定的语句。

综上所述,以您描述的方式管理自己的对象和使用指针的好处可能无法像您想像的那样为您带来很多性能提升。不这样做的原因:

  1. C#非常优化且非常快
  2. 您的指针代码仍以IL生成,必须将其抖动(这时将进行进一步的优化)
  3. 您不是要关闭垃圾收集器……只是将要使用的对象保留在GC的权限范围之外。因此,每隔100毫秒左右,GC仍会中断您的代码,并对托管代码中的所有其他变量执行其功能。

HTH,
詹姆斯


谢谢,但是当您使用指针时,完成后如何将它们“清理”?是否有可能在性能低下的情况下使用它们,然后再切换回托管代码?
Joan Venge

感谢James提供其他信息。
Joan Venge

2
@琼:当然。但是有责任确保清理所有内容,确保周围没有可移动内存的杂散指针,等等。如果您想要关闭安全系统的好处,那么您必须承担执行安全系统通常为您所做的事情的费用。
埃里克·利珀特

谢谢Eric,这很有意义。但是,在通过指针进行性能优化的情况下,完成操作后,仍然可以将数据返回到托管世界中,对吗?就像托管数据->非托管数据->对该数据进行一些快速操作->从该非托管数据创建托管数据->清理非托管内存->返回托管世界?
Joan Venge

1
进一步说明,您可以使用GC.AddMemoryPressure和显式通知非托管内存中的内存压力的垃圾回收GC.RemoveMemoryPressure。您仍然必须自己释放内存,但是通过这种方式,垃圾回收器将在制定调度决策时考虑非托管内存。
布莱恩(Brian)

3

在C#中显式使用指针的最常见原因:

  • 做对性能非常敏感的低级工作(例如字符串操作),
  • 与非托管API的接口。

之所以从C#中删除与指针关联的语法的原因(根据我的知识和观点-乔恩·斯凯特(Jon Skeet)会回答更好的B-),是因为在大多数情况下它是多余的。

从语言设计的角度来看,一旦通过垃圾回收器管理内存,就必须对指针的作用和不作用施加严格的约束。例如,使用指针指向对象的中间可能会给GC造成严重问题。因此,一旦限制到位,您就可以省略多余的语法并以“自动”引用结尾。

同样,在C / C ++中发现的超仁慈方法是常见的错误来源。在大多数情况下,微性能完全无关紧要,最好提供更严格的规则并约束开发人员,以减少难以发现的错误。因此,对于常见的业务应用程序,.NET和Java之类的所谓“托管”环境比假定与裸机竞争的语言更适合。


1
指针不会从C#中删除。也许您在考虑Java?
Ben Voigt

我并不是说删除了指针,而是删除了多余的语法,即不必编写obj->Propertyobj.Property而是可以使用。将澄清我的答案。
Ondrej Tucny 2011年


1
本是对的;您最肯定在取消引用C#中的指针时必须使用箭头(和星号)。不要将指针引用混淆; C#都支持。
埃里克·利珀特

@Eric Lippert嘿,是的。但是,考虑将引用作为指针的子集,我选择了“指针”一词作为更通用的变体,以解释引用的演变以及“无指针”语言(其“安全”部分是正确的)-从普通的旧指针开始。
Ondrej Tucny 2011年

2

假设您要使用IPC(共享内存)在2个应用程序之间进行通信,则可以将数据封送至内存,然后通过Windows消息传递或其他方式将此数据指针传递给另一个应用程序。在接收应用程序时,您可以取回数据。

在将数据从.NET传输到旧版VB6应用程序时也很有用,在该过程中,您将数据封送至内存,使用win msging传递指向VB6应用程序的指针,使用VB6 copymemory()从托管内存空间中获取数据到VB6应用程序非托管内存空间..

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.