为什么在未经检查的情况下,将int.MinValue除以-1会引发OverflowException?

49
int y = -2147483648;
int z = unchecked(y / -1);
第二行代码会抛出一个OverflowException异常。使用unchecked关键字不应该可以预防这种情况吗?
例如:
int y = -2147483648;
int z = unchecked(y * 2);

不会引发异常。


10
@DJBurb 这个问题并不是在问如何规避它,而是要解释这种行为。代码不应该抛出异常。 - Servy
5
执行除法时,使int溢出的唯一可行方法是使用此处列出的确切两个操作数,即将int.MinValue除以-1。每对操作数中的所有其他对都不会溢出,因此他们可能没有考虑到这种情况,并错误地假设整数除法永远不会溢出。 - Servy
6
@HansPassant - 如果他们不会这个,那他们就不是真正的开发者! - Ahmed ilyas
4
我不同意。Hans的回答可能是目前唯一回答中解释为什么会这样的原因。 - ispiro
7
你得到的赞数比踩数多,我认为你应该把你的回答重新发布。特别是因为它在某种程度上是Servy回答的第二部分/另一半。 - Victor Zakharov
显示剩余20条评论
3个回答

36
这不是C#编译器或JIT有任何控制权的异常。这仅适用于Intel / AMD处理器,当IDIV指令失败时,CPU会生成#DE陷阱(除法错误)。操作系统处理处理器陷阱,并将其反射回进程中具有STATUS_INTEGER_OVERFLOW异常。CLR忠实地将其转换为相匹配的托管异常。
英特尔处理器手册并不是关于它的信息的黄金矿山:
非整数结果被截断(切割)为0。余数始终小于除数的量级。溢出用#DE(除法错误)异常表示而不是CF标志。
简言之:有符号除法结果为+2147483648,不能用int表示,因为这是Int32.MaxValue + 1。否则,这是处理器表示负值的方式的必然副作用,他使用二补码编码。这会产生一个唯一值来表示0,留下了其他可能编码负数和正数的奇数个选项。负值多一个选项。与-Int32.MinValue相同类型的溢出,只是处理器不会在NEG指令上触发陷阱,而是生成垃圾结果。
当然,C#语言不是唯一有这个问题的语言。 C#语言规范通过提到特殊行为(第7.8.2章)使其成为实现定义行为。他们对此可能做的其他合理的事情没有任何好做法,生成处理异常的代码肯定被认为太不切实际,会产生无法诊断的慢速代码。这不是C#的方式。

C和C++语言规范通过将其定义为未定义行为而升级了赌注。这可能真的很丑陋,例如使用gcc或g ++编译器(通常是使用MinGW工具链)。该工具链对于SEH的运行时支持并不完善,它会吞下异常并允许处理器重新启动除法指令。程序挂起,使处理器不断生成#DE陷阱。将除法转换为传说中的Halt and Catch Fire指令 :)


11
但是C#语言本不应该生成会导致出现此类错误的代码(或者它应该吞咽异常并处理这种情况,而不是重新抛出),以满足unchecked的要求。事实上,当处理器抛出错误时,C#重新抛出该错误并不能真正解释任何问题。 - Servy
3
对于您的编辑,unchecked 的整个意义在于当整数操作溢出时不会抛出溢出异常,而是进行包装。 - Servy
neg 对于 -2147483648 的结果并不是垃圾值,实际上它是 +2147483648(当被解释为无符号数时)。 - harold
3
嗯,这就是用软件引爆火箭的方法。 - Hans Passant

33

C# 4规范的第7.72节(除法运算符)指出:

  

如果左操作数是最小表示的int或long值,右操作数为-1,则会发生溢出。在已检查的上下文中[...]。在未检查的上下文中,实现定义为抛出System.ArithmeticException(或其子类)或溢出未报告并将结果值作为左操作数的值

因此,在未检查的上下文中,这会引发异常实际上并不是错误,因为行为是实现定义的。


3
也许你还可以引用Hans Passant的回答,其中提供了实现细节,即为什么它对OP无效?仅仅说“因为行为未定义,我们看到一些奇怪的东西”本身并不是一个答案。 - Victor Zakharov
6
但事实就是如此,这就是重点。规范定义了您可以从C#程序中期望的行为。即使某个特定的处理器指令在使用这些操作数时抛出错误,只有在您知道该C#代码结果是作为该C#代码的完整实现而调用该确切处理器指令时,才意味着某些含义(而您并不知道这一点)。某人可能在非英特尔机器上运行相同的C#代码,并返回int.MinValue。在编写此C#代码时,您确实需要假设它可能具有任何一种行为,甚至不能假设它会抛出异常。 - Servy
12
有趣。在旧版本的.NET中,行为已经定义如果左操作数是最小可表示的intlong值,右操作数是-1,则会发生溢出。在此情况下,无论操作是在“checked”还是“unchecked”上下文中发生,都将始终抛出System.OverflowException异常。 - Dmitry
2
@Neolisk 显然,原帖的作者已经知道他的特定CPU选择了其中的哪一个选项,因为他说代码对他来说是抛出异常的。说他正在使用的CPU在这种情况下会抛出异常而不是返回第一个操作数似乎毫无意义。无论如何,当使用这个C#代码时,他需要将代码编写成行为未定义的形式,而不是依赖于任何一种行为,无论他使用的是什么CPU。 - Servy
4
@Neolisk,C#规范并没有说当你调用此函数时可能会发生任何事情,包括宇宙爆炸。它明确说明它要么返回第一个操作数,要么抛出异常。你可以依赖其中的一件事情发生,只是不知道具体哪个。如果我编写这段代码,我不认为知道哪些CPU会有哪种行为可能会有用。无论如何,你必须编写支持两种情况的代码。这不是说,你应该只编写支持一种情况的代码,然后说,“你只能在英特尔机器上运行此程序”。 - Servy
显示剩余11条评论

10

根据C# 语言规范 5.0第7.8.2节,我们有以下情况:

7.8.2 除法运算符 对于形如 x / y 的操作,应用二元运算符重载解析(§7.3.4)以选择特定的运算符实现。操作数转换为所选运算符的参数类型,并且结果类型是运算符的返回类型。下面列出了预定义的除法运算符。所有运算符都计算 x 和 y 的商。 整数除法: int operator /(int x, int y); uint operator /(uint x, uint y); long operator /(long x, long y); ulong operator /(ulong x, ulong y); 如果右操作数的值为零,则抛出 System.DivideByZeroException 异常。该除法将结果向零舍入。因此,结果的绝对值是小于或等于两个操作数商的最大可能整数。当两个操作数具有相同符号时,结果为零或正;当两个操作数具有相反符号时,结果为零或负。如果左操作数是最小可表示的 int 或 long 值,而右操作数是 -1,则会发生溢出。在已检查的上下文中,这会导致抛出 System.ArithmeticException(或其子类) 。在未经检查的上下文中,它是实现定义的,即抛出 System.ArithmeticException(或其子类) 或溢出不报告,其结果值为左操作数的值。

2
你怎么会引用规格书的不同部分,但是使用了相同的词汇? - Victor Zakharov
6
@Neolisk提到了不同版本的C#。他引用了5.0版本,而我引用的是4.0版本。显然,相关内容并没有任何不同。 - Servy

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