Dwukrotny błąd w metodzie Double.ToString .NET
Matematycznie rozważmy dla tego pytania liczbę wymierną
8725724278030350 / 2**48
gdzie**
w mianowniku oznacza potęgowanie, tj. mianownik jest2
do48
potęga. (Ułamek nie jest najniższy, można go zmniejszyć o 2.) Ta liczba wynosidokładnie reprezentowalny jakoSystem.Double
. Jego dziesiętna ekspansja jest
31.0000000000000'49'73799150320701301097869873046875 (exact)
gdzie apostrofy nie reprezentują brakujących cyfr, a jedynie zaznaczają krzyżyki, do których są zaokrąglone15 wzgl.17 cyfry należy wykonać.
Zwróć uwagę na następujące kwestie: Jeśli liczba ta zostanie zaokrąglona do 15 cyfr, wynikiem będzie31
(następnie trzynaście0
s) ponieważ następne cyfry (49...
) zacznij od a4
(co oznacza rundana dół). Ale jeśli liczba jestpierwszy zaokrąglone do 17 cyfr inastępnie zaokrąglone do 15 cyfr, wynik może być31.0000000000001
. To dlatego, że pierwsze zaokrąglenie zaokrągla się w górę, zwiększając49...
cyfry do50 (terminates)
(następne cyfry były73...
), a drugie zaokrąglenie może zaokrąglić się ponownie (gdy reguła zaokrąglania punktu środkowego mówi „zaokrąglić od zera”).
(Oczywiście jest wiele więcej liczb o powyższych cechach).
Teraz okazuje się, że standardowa ciąg znaków reprezentujący tę liczbę .NET"31.0000000000001"
. Pytanie: czy to nie błąd? Przez standardową reprezentację ciągu rozumiemyString
produkowane przez parametryDouble.ToString()
metoda instancji, która jest oczywiście identyczna z produkowaną przezToString("G")
.
Interesującą rzeczą do odnotowania jest to, że jeśli rzucisz powyższy numer naSystem.Decimal
wtedy dostanieszdecimal
to jest31
dokładnie! Widziećto pytanie o przepełnienie stosu do dyskusji na temat zaskakującego faktu, że rzucanieDouble
doDecimal
obejmuje pierwsze zaokrąglenie do 15 cyfr. Oznacza to, że casting doDecimal
tworzy poprawną rundę na 15 cyfr, podczas gdy wywołanieToSting()
robi niepoprawny.
Podsumowując, mamy liczbę zmiennoprzecinkową, która, gdy dane wyjściowe dla użytkownika, to31.0000000000001
, ale po konwersji naDecimal
(gdzie29 cyfry są dostępne), staje się31
dokładnie. To niefortunne.
Oto kod C #, aby zweryfikować problem:
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
}
Powyższy kod używa SkeetaToExactString
metoda. Jeśli nie chcesz używać jego rzeczy (można znaleźć za pośrednictwem adresu URL), po prostu usuń wiersze kodu powyżej zależneexactString
. Nadal możesz zobaczyć, jakDouble
w pytaniu (evil
) jest zaokrąglony i odlany.
DODANIE:
OK, przetestowałem więcej liczb i oto tabela:
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
Pierwsza kolumna podaje dokładną (choć obciętą) wartość, którąDouble
przedstawiać. Druga kolumna podaje reprezentację ciągu z"R"
formatuj ciąg. Trzecia kolumna podaje zwykłą reprezentację ciągu. I wreszcie czwarta kolumna podajeSystem.Decimal
to wynika z konwersji tegoDouble
.
Podsumowujemy:
Zaokrąglij do 15 cyfr poToString()
i zaokrąglaj do 15 cyfr po konwersji naDecimal
w wielu przypadkach nie zgadzam sięKonwersja doDecimal
w wielu przypadkach również zaokrągla błędnie, a błędów w tych przypadkach nie można opisać jako błędów „dwa razy”W moich przypadkachToString()
wydaje się dawać większą liczbę niżDecimal
konwersja, gdy nie zgadzają się (bez względu na to, która z dwóch rund poprawnie)Eksperymentowałem tylko z takimi przypadkami jak powyżej. Nie sprawdziłem, czy występują błędy zaokrąglania z liczbą innych „formularzy”.