Круговая ошибка в методе .NET Double.ToString

Математически рассмотрим для этого вопроса рациональное число

8725724278030350 / 2**48

, где **в знаменателе обозначает возведение в степень, т.е. знаменатель равен 2к 48-я степень.(Дробь не является младшим числом, она может быть уменьшена на 2.) Это число в точностипредставимо в виде System.Double. Его десятичное представление равно

31.0000000000000'49'73799150320701301097869873046875 (exact)

где апострофы не представляют пропущенные цифры, а просто обозначают границы, где должно быть выполнено округление до 15соответственно 17цифр

Обратите внимание на следующее: Если это число округляется до 15 цифр, результат будет 31(за которым следуют тринадцать 0с), потому что следующие цифры (49... ) начинаются с 4(означает округление вниз).Но если число сначалаокругляется до 17 цифр, а затемокругляется до 15 цифр, результатом может быть 31.0000000000001. Это связано с тем, что первое округление округляется путем увеличения 49... цифр до 50 (заканчивается)(следующие цифры были 73... ), и второе округление может затем снова округлиться (когда правило округления по средней точке говорит «округлять от нуля»).

(Конечно, существует гораздо больше чисел с указанными выше характеристиками.)

Теперь оказывается, что стандартное строковое представление этого числа в .NET — "31.0000000000001". Вопрос: не является ли это ошибкой?Под стандартным строковым представлением мы подразумеваем String, созданную параметрами Double.ToString()метода экземпляра, который курс идентичен тому, что создается ToString("G").

Интересно отметить, что если вы приведете указанное выше число к System.Decimal, то вы получите decimal, то есть 31точно! См. этот вопрос о переполнении стека для обсуждения удивительного факта, что преобразование Doubleв Decimalвключает первое округление до 15 цифр. Это означает, что приведение к Decimalделает правильное округление до 15 цифр, тогда как вызов ToSting()делает неправильное.

Подводя итог, у нас есть число с плавающей запятой, которое при выводе пользователю равно 31.0000000000001, но при преобразовании в Decimal(где доступно 29цифр) становится точно 31. Это прискорбно.

Вот некоторый код C# для проверки проблемы:

static void Main()
{
  const double evil = 31.0000000000000497;
  string exactString = DoubleConverter.ToExactString(evil); // Jon Skeet, http://csharpindepth.com/Articles/General/FloatingPoint.aspx 

  Console.WriteLine("Exact value (Jon Skeet): {0}", exactString);   // writes 31.00000000000004973799150320701301097869873046875
  Console.WriteLine("General format (G): {0}", evil);               // writes 31.0000000000001
  Console.WriteLine("Round-trip format (R): {0:R}", evil);          // writes 31.00000000000005

  Console.WriteLine();
  Console.WriteLine("Binary repr.: {0}", String.Join(", ", BitConverter.GetBytes(evil).Select(b => "0x" + b.ToString("X2"))));

  Console.WriteLine();
  decimal converted = (decimal)evil;
  Console.WriteLine("Decimal version: {0}", converted);             // writes 31
  decimal preciseDecimal = decimal.Parse(exactString, CultureInfo.InvariantCulture);
  Console.WriteLine("Better decimal: {0}", preciseDecimal);         // writes 31.000000000000049737991503207
}

В приведенном выше коде используется метод Skeet ToExactString. Если вы не хотите использовать его материалы (их можно найти по URL-адресу), просто удалите приведенные выше строки кода, зависящие от calculateString. Вы все еще можете видеть, как Doubleв вопросе ( evil) округляется и отбрасывается.

ДОПОЛНЕНИЕ:

Итак, я проверил еще несколько чисел, и вот таблица:

  exact value (truncated)       "R" format         "G" format     decimal cast
 -------------------------  ------------------  ----------------  ------------
 6.00000000000000'53'29...  6.0000000000000053  6.00000000000001  6
 9.00000000000000'53'29...  9.0000000000000053  9.00000000000001  9
 30.0000000000000'49'73...  30.00000000000005   30.0000000000001  30
 50.0000000000000'49'73...  50.00000000000005   50.0000000000001  50
 200.000000000000'51'15...  200.00000000000051  200.000000000001  200
 500.000000000000'51'15...  500.00000000000051  500.000000000001  500
 1020.00000000000'50'02...  1020.000000000005   1020.00000000001  1020
 2000.00000000000'50'02...  2000.000000000005   2000.00000000001  2000
 3000.00000000000'50'02...  3000.000000000005   3000.00000000001  3000
 9000.00000000000'54'56...  9000.0000000000055  9000.00000000001  9000
 20000.0000000000'50'93...  20000.000000000051  20000.0000000001  20000
 50000.0000000000'50'93...  50000.000000000051  50000.0000000001  50000
 500000.000000000'52'38...  500000.00000000052  500000.000000001  500000
 1020000.00000000'50'05...  1020000.000000005   1020000.00000001  1020000

Первый столбец дает точное (хотя и усеченное) значение, которое представляет Double. Второй столбец дает строковое представление из строки формата "R". Третий столбец дает обычное строковое представление. И, наконец, четвертый столбец дает System.Decimal, полученное в результате преобразования этого Double.

Мы заключаем следующее:

  • Округление до 15 цифр с помощью ToString()и округление до 15 цифр с помощью преобразования в Decimalво многих случаях не совпадает
  • Преобразование в Decimalтакже неправильно округляется во многих случаях, и ошибки в этих случаях не могут быть описаны как ошибки "двойного округления"
  • В моих случаях ToString(), кажется, дает большее число, чем Десятичноепреобразование, когда они не совпадают (независимо от того, какой из двух раундов правильный)

Я экспериментировал только со случаями, подобными приведенному выше.Я не проверял, есть ли ошибки округления с номерами других «форм».

15
задан Community 23 May 2017 в 12:12
поделиться