为什么编译器计算余数 MinValue % -1 与运行时不同

本文关键字:运行时 编译器 计算 余数 MinValue 为什么 | 更新日期: 2023-09-27 18:28:00

我认为这看起来像是C#编译器中的一个错误。

请考虑以下代码(在方法内(:

const long dividend = long.MinValue;
const long divisor = -1L;
Console.WriteLine(dividend % divisor);

它的编译没有错误(或警告(。似乎是一个错误。运行时,在控制台上打印0

那么没有const,代码:

long dividend = long.MinValue;
long divisor = -1L;
Console.WriteLine(dividend % divisor);

运行此函数时,会正确导致抛出OverflowException

C# 语言规范特别提到了这种情况,并表示应抛出System.OverflowException。它不依赖于上下文checked或看起来unchecked(编译时常量操作数对余数运算符的错误也与 checkedunchecked 相同(。

同样的错误发生在int(System.Int32(,而不仅仅是long(System.Int64(。

相比之下,编译器使用const操作数处理dividend / divisordividend % divisor 要好得多。

我的问题:

我说得对吗,这是一个错误?如果是,他们不希望修复的众所周知的错误(因为向后兼容性,即使将% -1与编译时常量一起使用-1相当愚蠢(?或者我们应该报告它,以便他们可以在即将推出的 C# 编译器版本中修复它?

为什么编译器计算余数 MinValue % -1 与运行时不同

这个极端情况在编译器中得到了非常具体的解决。 Roslyn 源代码中最相关的注释和代码:

// Although remainder and division always overflow at runtime with arguments int.MinValue/long.MinValue and -1     
// (regardless of checked context) the constant folding behavior is different.     
// Remainder never overflows at compile time while division does.    
newValue = FoldNeverOverflowBinaryOperators(kind, valueLeft, valueRight);

和:

// MinValue % -1 always overflows at runtime but never at compile time    
case BinaryOperatorKind.IntRemainder:
    return (valueRight.Int32Value != -1) ? valueLeft.Int32Value % valueRight.Int32Value : 0;
case BinaryOperatorKind.LongRemainder:
    return (valueRight.Int64Value != -1) ? valueLeft.Int64Value % valueRight.Int64Value : 0;

还有编译器的旧C++版本的行为,一直追溯到版本 1。 从 SSCLI v1.0 发行版中,clr/src/csharp/sccomp/fncbind.cpp源文件:

case EK_MOD:
    // if we don't check this, then 0x80000000 % -1 will cause an exception...
    if (d2 == -1) {
        result = 0;
    } else {
        result = d1 % d2;
    }
    break;
因此,得出

的结论是,至少在编译器上工作的程序员没有忽视或忘记这一点,它可能被定性为C#语言规范中不够精确的语言。 在这篇文章中,有关此杀手戳引起的运行时问题的更多信息。

我认为

这不是一个错误;而是C#编译器计算%的方式(这是一个猜测(。似乎 C# 编译器首先计算正数的%,然后应用符号。如果我们写Abs(long.MinValue + 1) == Abs(long.MaxValue)

static long dividend = long.MinValue + 1;
static long divisor = -1L;
Console.WriteLine(dividend % divisor);

现在我们将看到0是正确的答案,因为现在Abs(dividend) == Abs(long.MaxValue)在范围内。

那么,当我们将其声明为const值时,为什么它会起作用?(又是猜测(似乎 C# 编译器实际上在编译时计算表达式,并且不考虑常量的类型并将其作为BigInteger或其他东西(错误?因为如果我们声明一个函数,例如:

static long Compute(long l1, long l2)
{
    return l1 % l2;
}

并致电Console.WriteLine(Compute(dividend, divisor));我们将得到相同的异常。再一次,如果我们像这样声明常量:

const long dividend = long.MinValue + 1;

我们不会得到例外。