Double.MaxValue для целого является отрицательным?

Почему Double.MaxValue, приведённый к интегральному типу, приводит к отрицательному значению, наименьшему значению этого типа?

double maxDouble = double.MaxValue;       // 1.7976931348623157E+308
long maxDoubleLong = (long) maxDouble;    // -9223372036854775808

Я бы понял ошибку компилятора, если он слишком большой или OverflowException во время выполнения, или если я буду использовать unchecked, что преобразование может не вызывать исключение, но результат становится undefined и неверным (отрицательный).

Также странно, что значение long.MinValue:

bool sameAsLongMin = maxDoubleLong == long.MinValue; // true

Кстати, то же самое происходит, если я отбрасываю его на int:

int maxDoubleInt = (int)maxDouble;                   // -2147483648
bool sameAsIntMin = maxDoubleInt == int.MinValue;    // true

Если он пытается передать его в decimal, я получаю OverflowException во время выполнения

decimal maxDoubleDec = (decimal)maxDouble;  // nope

Обновить: кажется, что ответы Майкла и Барра ударяют ноготь по голове, если я использую checked явно, я получаю OverflowException:

checked
{
    double maxDouble = double.MaxValue;     // 1.7976931348623157E+308
    long maxDoubleLong = (long) maxDouble;  // nope
}

Ответ 1

Спецификация языка С# (версия 5.0) гласит следующее в 6.2.1 "Явные числовые преобразования" (выделено мной):

  • Для преобразования из float или double в интегральный тип обработка зависит от контекста проверки переполнения (§7.6.12), в котором происходит преобразование:

    • В проверенном контексте преобразование происходит следующим образом:

      • Если значение операнда равно NaN или бесконечно, генерируется исключение System.OverflowException.
      • В противном случае исходный операнд округляется до нуля до ближайшего целочисленного значения. Если это целочисленное значение находится в пределах тип назначения, то это значение является результатом преобразования.
      • В противном случае выдается исключение System.OverflowException.
    • В неконтролируемом контексте преобразование всегда выполняется успешно и выполняется следующим образом.

      • Если значение операнда равно NaN или бесконечно, результатом преобразования является неопределенное значение типа назначения.
      • В противном случае исходный операнд округляется до нуля до ближайшего целочисленного значения. Если это целочисленное значение находится в пределах тип назначения, то это значение является результатом преобразования.
      • В противном случае результатом преобразования является неопределенное значение типа назначения.

И в 7.6.12 "Проверенные и непроверенные операторы"

Для не константных выражений (выражения, которые оцениваются в время выполнения), которые не включены никакими проверенными или непроверенными операторами или операторов, контекст проверки переполнения по умолчанию не установленесли внешние факторы (такие как коммутаторы и выполнение компилятора конфигурация окружения). Вызовите проверяемую оценку.

Для конверсий от double до decimal: "Если исходное значение NaN, бесконечность или слишком большое, чтобы представлять в виде десятичного числа, генерируется исключение System.OverflowException". checked vs unchecked не вступает в игру (только для интегральных операций).

Ответ 2

Возможно, не полный ответ, но спецификация языка С# (§6.2.1) говорит следующее:

В неконтролируемом контексте преобразование всегда выполняется успешно, и поступает следующим образом.

• Если значение операнда NaN или бесконечно, результатом преобразования является неуказанное значение тип назначения.

• В противном случае исходный операнд округляется в направлении от нуля до ближайшего целочисленного значения. Если это целочисленное значение находится внутри диапазон типа назначения, то это значение является результатом преобразование.

• В противном случае результат преобразования неопределенное значение для типа адресата.

(основное внимание).

(Майкл Берр ответил в то же время, что и я, и он также включил информацию о стандартном контексте checked/unchecked в С#, см. комментарии ниже, поэтому этот ответ в значительной степени избыточен.)

Изменить 1: Обратите внимание, что если преобразование выполнено время компиляции (преобразование постоянного выражения), правила немного разные. Попробуйте изменить переменную maxDouble с помощью модификатора const. Затем компилятор С# сможет видеть значения, и для этого потребуется явно указать unchecked.

Изменить 2: В моей версии среды выполнения (.NET 4.5 для Windows 8.1) следующий код:

double d1 = double.PositiveInfinity;
double d2 = double.MaxValue;
double d3 = 2.3e23;
double d4 = double.NaN;
double d5 = -2.3e23;
double d6 = double.MinValue;
double d7 = double.NegativeInfinity;
Console.WriteLine((long)d1);
Console.WriteLine((long)d2);
Console.WriteLine((long)d3);
Console.WriteLine((long)d4);
Console.WriteLine((long)d5);
Console.WriteLine((long)d6);
Console.WriteLine((long)d7);

дает:

-9223372036854775808
-9223372036854775808
-9223372036854775808
-9223372036854775808
-9223372036854775808
-9223372036854775808
-9223372036854775808

поэтому оказывается, что "неуказанное значение" на самом деле "всегда" MinValue для типа назначения в этой реализации.

Ответ 3

Кажется, что поведение по умолчанию здесь unchecked, а именно: если вы явно не указали checked, переполнение не будет обнаружено:

 double maxDouble = double.MaxValue;       // 1.7976931348623157E+308
 long uncheckedMaxDoubleLong = (long)maxDouble;    // -9223372036854775808
 long checkedMaxDoubleLong = checked((long)maxDouble); // ** Overflow Exception

В ретроспективе попытка прямого преобразования от double до long без проверки или ограничения ввода сначала не рекомендуется из-за двух аспектов:

  • числовые диапазоны несоответствий/потенциал для переполнения
  • соображения округления

Итак, лучше было бы здесь использовать Convert.ToInt64:

 var convertedDouble = Convert.ToInt64(maxDouble);    // ** OverflowException

Поскольку это внутренне проверяет вам checked и высказывает мнение о округлении, а именно:

 return checked((long)Math.Round(value));