Математически рассмотрим для этого вопроса рациональное число
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
.
Мы заключаем следующее:
ToString()
и округление до 15 цифр с помощью преобразования в Decimal
во многих случаях не совпадаетDecimal
также неправильно округляется во многих случаях, и ошибки в этих случаях не могут быть описаны как ошибки "двойного округления"ToString()
, кажется, дает большее число, чем Десятичное
преобразование, когда они не совпадают (независимо от того, какой из двух раундов правильный)Я экспериментировал только со случаями, подобными приведенному выше.Я не проверял, есть ли ошибки округления с номерами других «форм».