Компилятор C # не ограничивает количество цифр дробной части литерала с плавающей запятой

#c#

#c#

Вопрос:

Это просто для академических целей.

Я заметил, что для целых литералов мы можем объявить, до 18446744073709551615 которого равно 2^64-1 или ulong.MaxValue . Определение большего, чем это значение, приводит к ошибке времени компиляции.

А для литералов с плавающей запятой мы можем объявлять их с целой частью до 999...999 ( 9 повторяется 308 раз). Объявление целочисленной части с большим количеством цифр снова приводит к ошибке времени компиляции. Меня интересует то, что компилятор, похоже, позволяет нам указывать в дробной части неограниченное количество цифр. Практически неограниченное количество цифр для дробной части не имеет смысла.

Вопросы:

  1. Существует ли константа, представляющая максимальное количество цифр, внутренне определенных компилятором C # для дробной части числа с плавающей запятой?

  2. Если такая константа существует, почему компилятор C # не выдает ошибку времени компиляции, когда пользователи указывают дробные части за ее пределами?

Минимальный рабочий пример 1

 namespace FloatingPoint
{
    class Program
    {
        static void Main(string[] args)
        {
            const ulong @ulong = 18446744073709551615;
            const double @double = 99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999.9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999;

        }
    }
}
  

Минимальный рабочий пример 2

 using System;

namespace FloatingPoint
{
    class Program
    {
        static void Main(string[] args)
        {

            const double x01 = 0.9;
            const double x02 = 0.99;
            const double x03 = 0.999;
            const double x04 = 0.9999;

            const double x05 = 0.99999;
            const double x06 = 0.999999;
            const double x07 = 0.9999999;
            const double x08 = 0.99999999;

            const double x09 = 0.999999999;
            const double x10 = 0.9999999999;
            const double x11 = 0.99999999999;
            const double x12 = 0.999999999999;

            const double x13 = 0.9999999999999;
            const double x14 = 0.99999999999999;
            const double x15 = 0.999999999999999;
            const double x16 = 0.9999999999999999;

            const double x17 = 0.99999999999999999;
            const double x18 = 0.999999999999999999;
            const double x19 = 0.9999999999999999999;
            const double x20 = 0.99999999999999999999;

            Console.WriteLine(x01);
            Console.WriteLine(x02);
            Console.WriteLine(x03);
            Console.WriteLine(x04);
            Console.WriteLine(x05);
            Console.WriteLine(x06);
            Console.WriteLine(x07);
            Console.WriteLine(x08);
            Console.WriteLine(x09);
            Console.WriteLine(x10);
            Console.WriteLine(x11);
            Console.WriteLine(x12);
            Console.WriteLine(x13);
            Console.WriteLine(x14);
            Console.WriteLine(x15);
            Console.WriteLine(x16);
            Console.WriteLine(x17);
            Console.WriteLine(x18);
            Console.WriteLine(x19);
            Console.WriteLine(x20);

        }
    }
}

/* output:

0.9
0.99
0.999
0.9999
0.99999
0.999999
0.9999999
0.99999999
0.999999999
0.9999999999
0.99999999999
0.999999999999
0.9999999999999
0.99999999999999
0.999999999999999
1
1
1
1
1
*/
  

IL:

 .method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       302 (0x12e)
  .maxstack  1
  IL_0000:  nop
  IL_0001:  ldc.r8     0.90000000000000002
  IL_000a:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_000f:  nop
  IL_0010:  ldc.r8     0.98999999999999999
  IL_0019:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_001e:  nop
  IL_001f:  ldc.r8     0.999
  IL_0028:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_002d:  nop
  IL_002e:  ldc.r8     0.99990000000000001
  IL_0037:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_003c:  nop
  IL_003d:  ldc.r8     0.99999000000000005
  IL_0046:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_004b:  nop
  IL_004c:  ldc.r8     0.99999899999999997
  IL_0055:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_005a:  nop
  IL_005b:  ldc.r8     0.99999990000000005
  IL_0064:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_0069:  nop
  IL_006a:  ldc.r8     0.99999998999999995
  IL_0073:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_0078:  nop
  IL_0079:  ldc.r8     0.99999999900000003
  IL_0082:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_0087:  nop
  IL_0088:  ldc.r8     0.99999999989999999
  IL_0091:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_0096:  nop
  IL_0097:  ldc.r8     0.99999999999
  IL_00a0:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_00a5:  nop
  IL_00a6:  ldc.r8     0.99999999999900002
  IL_00af:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_00b4:  nop
  IL_00b5:  ldc.r8     0.99999999999989997
  IL_00be:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_00c3:  nop
  IL_00c4:  ldc.r8     0.99999999999999001
  IL_00cd:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_00d2:  nop
  IL_00d3:  ldc.r8     0.999999999999999
  IL_00dc:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_00e1:  nop
  IL_00e2:  ldc.r8     0.99999999999999989
  IL_00eb:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_00f0:  nop
  IL_00f1:  ldc.r8     1.
  IL_00fa:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_00ff:  nop
  IL_0100:  ldc.r8     1.
  IL_0109:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_010e:  nop
  IL_010f:  ldc.r8     1.
  IL_0118:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_011d:  nop
  IL_011e:  ldc.r8     1.
  IL_0127:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_012c:  nop
  IL_012d:  ret
} // end of method Program::Main
  

Комментарии:

1. Я не могу сказать наверняка, но если бы мне пришлось догадываться, это могло бы быть в этом пикантном кусочке из спецификации : «Если указанный литерал не может быть представлен в указанном типе, тогда возникает ошибка времени компиляции. Значение реального литерала типа float or double определяется с помощью режима IEEE «округление до ближайшего». » Так что, возможно, в этом случае это сумасшедшее длинное значение может быть «представлено» как число с плавающей запятой (поскольку почти все они являются приближением), или это может быть «IEEE round to nearest mode», который может позволить это.

2. Вам не нужно так много цифр, чтобы увидеть поведение, 0.9999999999999995 представлено как 0.999999999999999 , где 0.99999999999999951 представлено как 1 .

3. Для десятичных разрядов оно, по-видимому, округляется до ближайшего представимого значения (если оно находится между Double.MinValue и Double.MaxValue ). Таким const double d = 0.999... образом (повторяется до 2000 цифр) компилируется в IL-коде как 1 . То есть const double @d = 1; компилируется в точно такой же IL-код , поскольку const double @double = 0.999...; это может «не иметь смысла», но поскольку вполне вероятно, что любая введенная вами фракция не будет существовать как точное значение, она, вероятно, использует те же правила аппроксимации и является представимой , тогда как вне диапазонов Min / Max не представимы (таким образом, ошибка).

Ответ №1:

  1. Да, но это не десятичные цифры
  2. Спецификация дробных частей за пределами возможности их точного представления проста, когда спецификация десятичная, а представление двоичное. 0.3 уже требует аппроксимации.

Ответ №2:

В большинстве случаев число с плавающей запятой в любом случае будет приближением к желаемому реальному значению (если только оно не является одним из значений, которые могут быть представлены точно). Кроме того, приближение четко определено: просто округлите до ближайшего представимого значения. С другой стороны, не существует полезного способа округления целого числа (или целой части действительного числа) до ближайшего представимого значения. Что значит, например, округлить 2 ^ 100 до 2 ^ 64-1?

Ответ №3:

Я не знаю о каких-либо ограничениях на количество десятичных знаков, разрешенных в литерале с плавающей запятой, хотя проверить, действительно ли существует такой предел, должно быть относительно просто, хотя, если это так, это, вероятно, больше зависит от внутренних компонентов компилятора, чем от чего-либо конкретного для самих значений с плавающей запятой. Однако я думаю, что стоит подумать о том, имеет ли смысл вообще ограничивать количество десятичных знаков в литерале. Я думаю, что ключевым моментом здесь является разница между числами, которые не могут быть представлены, поскольку они находятся за пределами поддерживаемого диапазона двойного типа данных (которые подбираются компилятором), и числами, которые не могут быть представлены точно в пределах типа данных.

Действительно, существует много десятичных чисел, которые не могут быть представлены точно как двойные (например, 0,1), и все же компилятор молча принимает их, преобразуя их в ближайшее представимое значение, и было бы значительным неудобством, если бы это было не так. Почему поэтому литерал с избытком десятичных знаков должен обрабатываться по-другому?