Почему компилятор оценивает остаток MinValue% -1, отличный от времени выполнения?
Я думаю, что это похоже на ошибку в компиляторе С#.
Рассмотрим этот код (внутри метода):
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
выбрасывается.
Спецификация языка С# упоминает этот случай специально и говорит, что должен быть выброшен System.OverflowException
. Он не зависит от контекста checked
или unchecked
(похоже, ошибка с константами постоянной времени компиляции оператору остатка совпадает с checked
и unchecked
).
Такая ошибка возникает с int
(System.Int32
), а не только long
(System.Int64
).
Для сравнения, компилятор обрабатывает dividend / divisor
с операндами const
намного лучше, чем dividend % divisor
.
Мои вопросы:
Я прав, это ошибка? Если да, это известная ошибка, которую они не хотят исправлять (из-за обратной совместимости, даже если глупо использовать % -1
с постоянной времени компиляции -1
)? Или мы должны сообщать об этом, чтобы они могли исправить его в следующих версиях компилятора С#?
Ответы
Ответ 1
Этот угловой регистр очень конкретно рассматривается в компиляторе. Наиболее релевантные комментарии и код в Roslyn source:
// 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;
Также поведение устаревшей версии компилятора С++, возвращаясь к версии 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;
Итак, сделайте вывод, что это не было забыто или забыто, по крайней мере, у программистов, которые работали над компилятором, его можно было бы квалифицировать как недостаточно точный язык в спецификации языка С#. Подробнее о проблемах во время выполнения, вызванных этим убийцей, ткнуть в этот пост.
Ответ 2
Я думаю, что это не ошибка; это скорее, как компилятор С# вычисляет %
(это предположение). Кажется, что компилятор С# сначала вычисляет %
для положительных чисел, затем применяет знак. Имея 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
? (Опять угадай) Кажется, что компилятор С# фактически вычисляет выражение во время компиляции и не считает тип константы и действует на него как BigInteger
или что-то (ошибка?). Потому что, если мы объявим такую функцию, как:
static long Compute(long l1, long l2)
{
return l1 % l2;
}
И вызовите Console.WriteLine(Compute(dividend, divisor));
, мы получим одно и то же исключение. И снова, если мы объявим константу следующим образом:
const long dividend = long.MinValue + 1;
Мы не получили бы исключения.