Математически рассмотрим для этого вопроса рациональное число
8725724278030350 / 2**48
где **
в знаменателе обозначает возведение в степень, т.е. знаменатель 2
до 48
-й степени. (Дробь не находится в младших членах, сводится к 2.) Это число точно представляется как System.Double
. Его десятичное разложение есть
31.0000000000000'49'73799150320701301097869873046875 (exact)
где апострофы не представляют отсутствующие цифры, а просто отмечают суки, где округление до 15 соответственно. 17.
Обратите внимание на следующее: если это число округлено до 15 цифр, результат будет 31
(за ним следует тринадцать 0
s), потому что следующие цифры (49...
) начинаются с 4
(что означает round вниз). Но если число сначала округлено до 17 цифр, а затем округлено до 15 цифр, результат может быть 31.0000000000001
. Это связано с тем, что первое округление округляется, увеличивая цифры 49...
до 50 (terminates)
(следующие цифры были 73...
), а второе округление затем снова округлялось (когда правило округления средней точки гласит: "округлить от нуля" ).
(Разумеется, есть много других чисел с вышеуказанными характеристиками.)
Теперь, оказывается, стандартное строковое представление .NET этого числа "31.0000000000001"
. Вопрос: не является ли это ошибкой?. По стандартным строковым представлениям мы подразумеваем String
, созданный методом экземпляров параметров Double.ToString()
, который, конечно, идентичен тому, что создается ToString("G")
.
Интересно отметить, что если вы набросаете вышеприведенный номер на System.Decimal
, тогда вы получите точно decimal
31
! См. этот вопрос о переполнении стека для обсуждения удивительного факта, что отбрасывание Double
в decimal
включает в себя первое округление до 15 цифр. Это означает, что кастинг на decimal
делает правильный раунд до 15 цифр, тогда как вызов ToSting()
делает неправильный.
Подводя итог, мы имеем число с плавающей запятой, которое при выводе на пользователя 31.0000000000001
, но при преобразовании в decimal
(где 29 цифры доступны), становится 31
точно. Это печально.
Вот несколько С# -кодов для проверки проблемы:
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-адрес), просто удалите строки кода, зависящие от exactString
. Вы все еще можете увидеть, как рассматриваемый 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
также во многих случаях неверно округляется, и ошибки в этих случаях не могут быть описаны как ошибки "round-two" - В моих случаях
ToString()
, кажется, дает большее число, чемdecimal
, когда они не согласны (независимо от того, какой из двух раундов правильно)
Я только экспериментировал с такими случаями, как выше. Я не проверял, есть ли ошибки округления с числами других "форм".