为什么不能使用 < 和 > 操作符比较两个 IntPtr?

10

我目前遇到了一个与不安全指针有关的问题,似乎是编译器的bug。

注意:问题并不在于我使用指针和不安全代码;代码运行得很好。问题与已确认的编译器错误有关,在某些情况下拒绝编译合法代码。如果您对该问题感兴趣,请访问我的另一个问题

由于我的问题出在指针变量的声明上,所以我决定绕过这个问题,改用IntPtr,需要时再转换为实际指针。

然而,我注意到我不能像这样做:

IntPtr a = something;
IntPtr b = somethingElse;
if (a > b) // ERROR. Can't do this
{
}
IntPtr类型似乎没有定义><运算符。请注意,我确实可以比较两个实际指针。 IntPtr有一个.ToInt64()方法。然而,这返回一个有符号值,在涉及正负值比较时可能返回不正确的值。
老实说,我真的不理解为什么要有一个返回有符号值的.ToInt64()方法,考虑到指针比较是无符号执行的,但这不是我的问题。
有人可能会认为IntPtr是不透明的句柄,因此使用><进行比较是没有意义的。然而,我想指出IntPtr具有加法和减法方法,这意味着IntPtr实际上有一个顺序概念,因此><确实有意义。
我想我可以将 ToInt64() 的结果转换为 ulong 然后进行比较,或者将 IntPtr 转换为指针然后进行比较,但是这让我想到为什么 >< 没有定义在 IntPtr 中。

为什么我不能直接比较两个 IntPtr


@EdS:是这样吗?AllocHGlobal既不返回对象也不返回数组,但我可以在我得到的地址空间内有意义地进行指针比较。 - Panda Pajama
@Ed,这意味着每次我在API函数(如malloc)提供的地址空间内比较指针时,我都依赖于未定义的行为? - Panda Pajama
@Ed:这正是我所说的。但是,从学术角度来看,语言不知道malloc返回的是什么,因此就语言而言,返回的指针既不是同一对象,也不是(语言)数组。这只是一次超级追究的对话;我们都知道自己在说什么,而且我们表达的完全是同样的意思。 - Panda Pajama
@PandaPajama:malloc的行为是由标准定义的,因此它确实如此。语言实现者必须实现标准。我认为你关注的是实现特定的行为,但是实现定义和未定义的行为之间存在差异。我不认为这很迂腐;这些差异很重要。 - Ed S.
@Ed:不完全是。在对象之间比较指针是没有任何意义的。基于标准字面含义讨论无意义操作的语义充分符合我对学究气质的定义。我从未这样做过,也确实不是我正在做的事情。这并不意味着IntPtr不能进行排序比较。一个完美的C# IntPtr带有序和顺序概念,使我可以用<>进行比较,并在跨对象比较时抛出异常。 - Panda Pajama
显示剩余8条评论
3个回答

7

IntPtr一直以来都有点被忽视。直到.NET 4.0才增加了Add/operator+Subtract/operator-

现在...如果你想比较两个指针,如果它们是IntPtr就将它们转换为long,如果它们是UIntPtr,则转换为ulong。请注意,在Windows上,只有当您使用带有/3GB选项的32位程序时,才需要使用UIntPtr,否则32位程序只能使用低2gb地址空间,而对于64位程序,远少于64位的地址空间被使用(目前为48位)。

显然,如果您正在使用.NET进行内核编程,情况会有所改变 :-)(我在这里开玩笑,希望不要当真 :-) )

关于为什么首选IntPtr而不是UIntPtr的原因:https://msdn.microsoft.com/en-us/library/system.intptr%28v=vs.110%29.aspx

IntPtr类型符合CLS,而UIntPtr类型不符合。只有IntPtr类型在公共语言运行时中使用。提供UIntPtr类型主要是为了与IntPtr类型保持架构对称。

有些语言没有有符号和无符号类型之分。.NET希望支持它们。

通过使用

editbin /LARGEADDRESSAWARE myprogram.exe

(我甚至能够崩溃我的图形适配器 :-))
static void Main(string[] args)
{
    Console.WriteLine("Is 64 bits", Environment.Is64BitProcess);

    const int memory = 128 * 1024;

    var lst = new List<IntPtr>(16384); // More than necessary

    while (true)
    {
        Console.Write("{0} ", lst.Count);

        IntPtr ptr = Marshal.AllocCoTaskMem(memory);
        //IntPtr ptr = Marshal.AllocHGlobal(memory);
        lst.Add(ptr);

        if ((long)ptr < 0)
        {
            Console.WriteLine("\nptr #{0} ({1}, {2}) is < 0", lst.Count, ptr, IntPtrToUintPtr(ptr));
        }
    }
}

我能够在64位操作系统上使用32位程序分配近4GB的内存(所以我的IntPtr是负数)

这里是从IntPtrUIntPtr的转换

public static UIntPtr IntPtrToUintPtr(IntPtr ptr)
{
    if (IntPtr.Size == 4)
    {
        return unchecked((UIntPtr)(uint)(int)ptr);
    }

    return unchecked((UIntPtr)(ulong)(long)ptr);
}

注意,由于符号扩展的工作方式,你不能简单地执行(UIntPtr)(ulong)(long)ptr,因为在32位上它将会出错。
但请注意,很少有程序在32位上支持超过2GB... http://blogs.msdn.com/b/oldnewthing/archive/2004/08/12/213468.aspx

@PandaPajama 我刚刚检查了一下,在 Windows 32 位系统上使用 Marshal.AllocCoTaskMemMarshal.AllocHGlobal,通常不能有大于 2GB 的地址,所以它们都是正数。现在...就像我说的,在 Windows /3gb(或 Linux)上会有所改变。在这种情况下,请使用 UIntPtr。但请注意,将 IntPtr 强制转换为 UIntPtr 是相当复杂的。 - xanatos
1
为什么不使用.NET进行内核编程?http://en.wikipedia.org/wiki/Singularity_%28operating_system%29 - AK_
@AK_ 这是我开的一个半玩笑... 我知道在理论上是可能的,但这不是 SO 上经常被问到的东西。 - xanatos
该信息适用于x86/64平台,主要是Windows系统。而我正在开发Xamarin.iOS应用,该应用支持64位地址。 - Panda Pajama
Singularity已经发展成熟,不再是实验性的了。来自“可靠消息来源”的传言称,微软正在内部使用它,在Azure中表现出色,因为您不需要在用户级和内核级之间进行隔离(因为您根本无法引用其他空间中的原始内存)-但这意味着您也没有真正的IntPtr。此外-它不是用C#编写的,而是用扩展方言编写的。 - Benjamin Gruenbaum

2
比较IntPtr非常危险。这是C#语言禁止此操作的核心原因,即使CLR没有问题。
IntPtr经常用于存储未管理的指针值。大问题是:指针值不是有符号值。只有UIntPtr是适当的托管类型来存储它们。UIntPtr的大问题是它不是CLS兼容类型。许多语言不支持无符号类型,例如Java、JScript和早期版本的VB.NET。因此,所有框架方法都使用IntPtr。
这特别恶劣,因为它经常可以正常工作。从32位Windows开始,地址空间的上2 GB保留给操作系统,所以程序中使用的所有指针值始终<= 0x7FFFFFFFF。在IntPtr中完全正常运行。
但并不是每种情况都如此。您可能在64位操作系统上的wow64模拟器中运行为32位应用程序。操作系统不再需要那2GB以上的地址空间,因此您可以获得4GB的地址空间,这非常好,现今32位代码经常避免OOM。指针值现在确实大于等于0x80000000。现在IntPtr比较在完全随机的情况下会失败。例如,0x80000100的值实际上比0x7FFFFE00大,但比较会认为它更小。这并不好。而且并不经常发生,指针值往往相似。而且它是相当随机的,实际指针值高度不可预测。

这是一个无法诊断的错误。

使用C或C++的程序员也很容易犯这个错误,他们的语言也不能阻止他们。微软想出了另一种方法来避免这种痛苦,这样的程序必须使用特定的链接器选项进行链接,以获得超过2GB的地址空间。


在Java 8中,您可以通过整数上的无符号算术来表示无符号值。因此,应该将“Java”更改为“Java的早期版本”。 - Benjamin Gruenbaum
嗯,Java 8并没有对.NET Framework的设计产生重大影响。 - Hans Passant
我真的希望它没有 :) 我指的是这句话: "许多语言不支持无符号类型。Java、JScript和早期版本的VB.NET就是其中的例子。" - Benjamin Gruenbaum
  1. 不,我不认为我做错了什么,请阅读我的问题。
  2. 你的回答基本上与我在问题中陈述的相同,请阅读我的问题。
  3. 根据你的回答内容,我强烈认为你没有阅读我的问题,请阅读我的问题。顺便说一句,我不是在Windows上运行。
- Panda Pajama
我看到了您的编辑,并理解了您的观点。然而,在UIntPtr上也不允许比较。据我理解IntPtr背后的概念是,它既不应该有符号也不应该没有符号。它只应该是一个不透明的容器,具有顺序和序列的概念,但不与特定表示形式相关联。显然,那并不是实际结果。 - Panda Pajama
显示剩余2条评论

0

在我看来,IntPtr并不是为像高/低比较这样的目的而开发的。它是一种存储内存地址的结构,只能用于相等性测试。您不应考虑内存中任何内容的相对位置(由CLI管理)。这就像比较以太网中哪个IP更高。


我们使用 IntPtr 来处理 -不是- 由 CLI 管理的内存。在 C# 中有许多获取非托管内存的方法,其中之一是 Marshal.AllocHGlobal()。在大多数情况下,指针和不安全代码并不是必需的,但也有一些情况需要使用它们。顺便说一句,在链路层中不存在 IP 地址;也许你指的是 MAC 地址? - Panda Pajama
@PandaPajama 嗯,我仅在进行PInvoke调用时使用IntPtr,我认为在你的情况下最好使用https://msdn.microsoft.com/en-us/library/deh4fbw8.aspx。 - FLCL
我想指向我的问题的前两段,那里我解释了为什么我不能做你所建议的事情。 - Panda Pajama

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