45 Stimmen

Die Umwandlung einer Dezimalzahl in eine Gleitkommazahl in C# führt zu einem Unterschied

Zusammenfassung des Problems:

Für einige Dezimalwerte wird bei der Umwandlung des Typs von decimal in double eine kleine Bruchzahl zum Ergebnis hinzugefügt.

Noch schlimmer ist, dass es zwei "gleiche" Dezimalwerte geben kann, die zu unterschiedlichen Double-Werten führen, wenn sie umgewandelt werden.

Codebeispiel:

decimal dcm = 8224055000.0000000000m;  // dcm = 8224055000
double dbl = Convert.ToDouble(dcm);    // dbl = 8224055000.000001

decimal dcm2 = Convert.ToDecimal(dbl); // dcm2 = 8224055000
double dbl2 = Convert.ToDouble(dcm2);  // dbl2 = 8224055000.0

decimal deltaDcm = dcm2 - dcm;         // deltaDcm = 0
double deltaDbl = dbl2 - dbl;          // deltaDbl = -0.00000095367431640625

Schauen Sie sich die Ergebnisse in den Kommentaren an. Die Ergebnisse wurden aus der Beobachtung des Debuggers kopiert. Die Zahlen, die diesen Effekt erzeugen, haben weit weniger Dezimalstellen als das Limit der Datentypen, daher kann es kein Überlauf sein (vermute ich!).

Was noch viel interessanter ist, ist, dass es zwei gleiche Dezimalwerte geben kann (im obigen Codebeispiel sehen Sie "dcm" und "dcm2" mit "deltaDcm" gleich null), die zu unterschiedlichen Double-Werten führen, wenn sie umgewandelt werden. (Im Code "dbl" und "dbl2", die einen Nicht-Null-"deltaDbl" haben)

Ich vermute, es sollte etwas mit dem Unterschied in der bitweisen Darstellung der Zahlen in den beiden Datentypen zu tun haben, aber ich kann nicht herausfinden, was! Und ich muss wissen, was zu tun ist, um die Umwandlung so zu machen, wie ich es brauche. (wie z.B. dcm2 -> dbl2)

1 Stimmen

Ich habe dieses Problem auf MS Connect gemeldet. Hier ist der Link: connect.microsoft.com/VisualStudio/feedback/…

0 Stimmen

Ich bin mir nicht sicher, was der Grund ist, aber es scheint, dass das Problem in den (6) großen Dezimalstellen liegt. Ich habe mit 5 Dezimalstellen getestet und es funktioniert einwandfrei. Ich habe ein ähnliches Szenario, bei dem ich von Dezimal zu Double und zurück umwandle und da meine Genauigkeit nur 2 Dezimalstellen beträgt, ist mein Code sicher konvertiert.

50voto

Jon Skeet Punkte 1325502

Interessant - obwohl ich im Allgemeinen den normalen Weg des Schreibens von Gleitkommazahlen nicht vertraue, wenn man an den genauen Ergebnissen interessiert ist.

Hier ist eine etwas einfachere Demonstration, die DoubleConverter.cs verwendet, die ich schon einige Male zuvor verwendet habe.

using System;

class Test
{
    static void Main()
    {
        decimal dcm1 = 8224055000.0000000000m;
        decimal dcm2 = 8224055000m;
        double dbl1 = (double) dcm1;
        double dbl2 = (double) dcm2;

        Console.WriteLine(DoubleConverter.ToExactString(dbl1));
        Console.WriteLine(DoubleConverter.ToExactString(dbl2));
    }
}

Ergebnisse:

8224055000.00000095367431640625
8224055000

Jetzt stellt sich die Frage, warum der ursprüngliche Wert (8224055000.0000000000), der eine Ganzzahl ist - und genau als double darstellbar ist - zusätzliche Daten enthält. Ich vermute stark, dass dies auf Eigenheiten im Algorithmus zurückzuführen ist, der von decimal in double konvertiert, aber das ist bedauerlich.

Es verstößt auch gegen Abschnitt 6.2.1 der C#-Spezifikation:

Für eine Konvertierung von decimal in float oder double wird der Dezimalwert auf den nächsten double- oder float-Wert gerundet. Auch wenn diese Konvertierung Präzision verlieren kann, führt sie niemals dazu, dass eine Ausnahme ausgelöst wird.

Der "nächste double-Wert" ist offensichtlich einfach 8224055000... also ist dies meiner Meinung nach ein Bug. Ich würde jedoch nicht erwarten, dass er bald behoben wird. (Übrigens gibt es dieselben Ergebnisse auch in .NET 4.0b1.)

Um den Bug zu vermeiden, möchten Sie wahrscheinlich den Dezimalwert zuerst normalisieren, indem Sie effektiv die zusätzlichen Nullen nach dem Dezimalpunkt "entfernen". Dies ist etwas knifflig, da es 96-Bit-Integer-Arithmetik erfordert - die .NET 4.0 BigInteger-Klasse kann dies möglicherweise erleichtern, aber das ist möglicherweise keine Option für Sie.

0 Stimmen

Dies ist meiner Meinung nach auch ein Fehler. Haben Sie/Irgendjemand dies an Microsoft gemeldet? Ich durchsuche MS Connect und sehe nichts dazu. Deshalb poste ich es. Möchte nur wissen, ob sie bestätigen, dass dies ein Fehler ist oder nicht.

0 Stimmen

96-Bit-Arithmetik ist in diesem speziellen Fall nicht notwendig, weil man decimal dafür verwenden kann, um die schweren Aufgaben zu erledigen :)

1 Stimmen

Faszinierender Fehler! Wie Anton Tykhyy bemerkt, liegt dies höchstwahrscheinlich daran, dass die Darstellung von Dezimalzahlen mit viel zusätzlicher Genauigkeit nicht mehr "nativ" in Ganzzahlen ist, die in ein Double ohne Darstellungsfehler passen. Ich wäre bereit, bis zu einem Dollar zu wetten, dass dieser Fehler seit fünfzehn Jahren in der OLE-Automation besteht - wir verwenden die OA-Bibliotheken für die Dezimalcodierung. Ich habe zufällig ein Archiv von OA-Quellen von vor zehn Jahren auf meinem Computer; wenn ich morgen etwas freie Zeit habe, werde ich einen Blick darauf werfen.

26voto

Anton Tykhyy Punkte 18869

Die Antwort liegt darin, dass decimal versucht, die Anzahl der signifikanten Stellen zu erhalten. Somit hat 8224055000.0000000000m 20 signifikante Stellen und wird als 82240550000000000000E-10 gespeichert, während 8224055000m nur 10 hat und als 8224055000E+0 gespeichert wird. Die Mantisse von double besteht (logisch) aus 53 Bits, d.h. maximal 16 Dezimalstellen. Dies ist genau die Genauigkeit, die Sie erhalten, wenn Sie in double konvertieren, und tatsächlich ist die verirrte 1 in Ihrem Beispiel an der 16. Dezimalstelle. Die Konvertierung ist nicht 1-zu-1, weil double die Basis 2 verwendet.

Hier sind die binären Darstellungen Ihrer Zahlen:

dcm:
00000000000010100000000000000000 00000000000000000000000000000100
01110101010100010010000001111110 11110010110000000110000000000000
dbl:
0.10000011111.1110101000110001000111101101100000000000000000000001
dcm2:
00000000000000000000000000000000 00000000000000000000000000000000
00000000000000000000000000000001 11101010001100010001111011011000
dbl2 (8224055000.0):
0.10000011111.1110101000110001000111101101100000000000000000000000

Für double habe ich Punkte verwendet, um die Vorzeichen-, Exponenten- und Mantissenfelder zu trennen; für decimal siehe MSDN zu decimal.GetBits, aber im Wesentlichen sind die letzten 96 Bits die Mantisse. Beachten Sie, wie die Mantissenbits von dcm2 und die signifikantesten Bits von dbl2 genau übereinstimmen (vergessen Sie nicht das implizite 1-Bit in der Mantisse von double) und tatsächlich stellen diese Bits 8224055000 dar. Die Mantissenbits von dbl sind die gleichen wie bei dcm2 und dbl2, jedoch mit der lästigen 1 im am wenigsten signifikanten Bit. Der Exponent von dcm ist 10 und die Mantisse ist 82240550000000000000.

Aktualisierung II: Es ist tatsächlich sehr einfach, nachgestellte Nullen abzuschneiden.

// In dieser Konstante gibt es 28 nachgestellte Nullen -
// keine Decimalzahl darf mehr als 28 nachgestellte Nullen haben
const decimal PräziseEins = 1.000000000000000000000000000000000000000000000000m ;

// decimal.ToString() zeigt nachgestellte Nullen getreu an
Behaupten ((8224055000.000000000m).ToString () == "8224055000.000000000") ;

// Lassen Sie System.Decimal.Divide() die ganze Arbeit tun
Behaupten ((8224055000.000000000m / PräziseEins).ToString () == "8224055000") ;
Behaupten ((8224055000.000010000m / PräziseEins).ToString () == "8224055000.00001") ;

0 Stimmen

Dies ergibt Sinn, aber schauen Sie sich die Antwort von Jon Skeet an. Logisch betrachtet sollte das Angeben von mehr Nachkommastellen zu einer genaueren Umrechnung führen, nicht zu einer schlechteren! Gibt es eine Möglichkeit, die Dezimalzahl in eine umzuwandeln, die "weniger" signifikante Stellen aufweist? Dies sollte in meinem Fall zu einer besseren Umrechnung führen!

0 Stimmen

Die Umrechnung ist genauer - Sie erhalten 6 zusätzliche Stellen - aber das Ergebnis ist nicht das, was Sie erwarten, weil die Basen von Dezimal- und Double-Zahlen unterschiedlich sind. Ich füge ein Beispiel gleich hinzu.

0 Stimmen

Es ist keine genauere Umwandlung. Der genaue Wert der Dezimalzahl liegt vor und sollte zurückgegeben werden. Ich kann verstehen, warum es passiert, aber das macht es nicht richtig :)

4voto

Greg Hewgill Punkte 882617

Der Artikel Was jeder Informatiker über die Gleitkommadarstellung wissen sollte wäre ein ausgezeichneter Ausgangspunkt.

Die kurze Antwort ist, dass die Gleitpunkt-Binärarithmetik zwangsläufig eine Näherung ist, und es ist nicht immer die Näherung, die man vermuten würde. Dies geschieht, weil CPUs in Basis 2 arbeiten, während Menschen (normalerweise) in Basis 10 arbeiten. Daraus ergeben sich eine Vielzahl unerwarteter Effekte.

0 Stimmen

Vielen Dank für den Artikel-Link, er ist sehr lang, aber ich werde versuchen, ihn zu lesen. Basis-2-Arithmetik gegen Basis-10-Arithmetik ist das, woran ich Zweifel hatte, aber es gibt zwei Punkte: 1. Dezimal hat 28-29 signifikante Stellen und Double hat 15-16 signifikante Stellen. 8 signifikante Stellen reichen für meine Zahl aus. Warum sollte es so behandelt werden? Und solange eine Darstellung der ursprünglichen Zahl als Double vorhanden ist, warum sollte die Umwandlung in eine andere resultieren? 2. Was ist mit den beiden "gleichen" Dezimalwerten, die in unterschiedliche Doubles umgewandelt werden?

1 Stimmen

Die Anzahl der signifikanten Stellen ist nicht besonders relevant - "0.1" hat nur eine signifikante Stelle, lässt sich aber dennoch nicht in Float/Double darstellen. Der Punkt darüber, dass eine genaue Darstellung verfügbar ist, ist viel bedeutsamer. Was die beiden Werte betrifft, die verschiedene Doubles ergeben - sie sind zwar gleich, aber nicht dasselbe.

0 Stimmen

Gibt es eine Möglichkeit, diese „gleichen, aber nicht identischen“ Dezimalzahlen ineinander umzurechnen? Und gibt es eine Möglichkeit, das im Debugger zu sehen? (Ich vermute, ich sollte die Bitdarstellung sehen, aber es gibt in VS keine solche Option. Und die „hexad dezimal-Anzeige“ funktioniert auch nicht auf diese Weise)

3voto

Ilan Punkte 1517

Um dieses Problem deutlicher zu sehen, probieren Sie es in LinqPad aus (oder ersetzen Sie alle .Dump()'s und ändern Sie sie in Console.WriteLine(), wenn Sie möchten).

Es scheint mir logisch inkorrekt zu sein, dass die Genauigkeit des Dezimals zu 3 verschiedenen Doubles führen könnte. Ein Lob an @AntonTykhyy für die /PreciseOne-Idee:

((double)200M).ToString("R").Dump(); // 200
((double)200.0M).ToString("R").Dump(); // 200
((double)200.00M).ToString("R").Dump(); // 200
((double)200.000M).ToString("R").Dump(); // 200
((double)200.0000M).ToString("R").Dump(); // 200
((double)200.00000M).ToString("R").Dump(); // 200
((double)200.000000M).ToString("R").Dump(); // 200
((double)200.0000000M).ToString("R").Dump(); // 200
((double)200.00000000M).ToString("R").Dump(); // 200
((double)200.000000000M).ToString("R").Dump(); // 200
((double)200.0000000000M).ToString("R").Dump(); // 200
((double)200.00000000000M).ToString("R").Dump(); // 200
((double)200.000000000000M).ToString("R").Dump(); // 200
((double)200.0000000000000M).ToString("R").Dump(); // 200
((double)200.00000000000000M).ToString("R").Dump(); // 200
((double)200.000000000000000M).ToString("R").Dump(); // 200
((double)200.0000000000000000M).ToString("R").Dump(); // 200
((double)200.00000000000000000M).ToString("R").Dump(); // 200
((double)200.000000000000000000M).ToString("R").Dump(); // 200
((double)200.0000000000000000000M).ToString("R").Dump(); // 200
((double)200.00000000000000000000M).ToString("R").Dump(); // 200
((double)200.000000000000000000000M).ToString("R").Dump(); // 199.99999999999997
((double)200.0000000000000000000000M).ToString("R").Dump(); // 200
((double)200.00000000000000000000000M).ToString("R").Dump(); // 200.00000000000003
((double)200.000000000000000000000000M).ToString("R").Dump(); // 200
((double)200.0000000000000000000000000M).ToString("R").Dump(); // 199.99999999999997
((double)200.00000000000000000000000000M).ToString("R").Dump(); // 199.99999999999997

"\nFixed\n".Dump();

const decimal PreciseOne = 1.000000000000

0 Stimmen

Ich denke, der Schlüssel zum Verständnis dessen, was passiert, besteht darin, 2E23/1E21 und 2E25/2E23 auszudrucken. Die Konvertierung von Decimal in double erfolgt durch die Division eines Ganzzahlwerts durch eine Potenz von zehn, obwohl dies einen Rundungsfehler verursachen kann.

1voto

pavium Punkte 14270

Dies ist ein altes Problem und war Gegenstand vieler ähnlicher Fragen auf StackOverflow.

Die vereinfachte Erklärung ist, dass Dezimalzahlen nicht genau im Binärsystem dargestellt werden können.

Dieser Link ist ein Artikel, der das Problem erklären könnte.

1 Stimmen

Das erklärt es tatsächlich nicht. Viele Dezimalzahlen können nicht genau in Binär umgewandelt werden - aber in diesem Fall kann die Eingabe genau in binär repräsentiert werden. Daten gehen unnötigerweise verloren.

0 Stimmen

Jon, Daten gehen nicht verloren, im Gegenteil - es ist die unnötig konservierte (aus Irchis Sicht, ohne Beleidigung) Daten, die das Problem darstellen.

0 Stimmen

Anton, sieh dir die von Jon veröffentlichte Spezifikation an. Die unnötig beibehaltene Daten sollten die Konvertierung nicht ruinieren. Nach den 16 signifikanten Ziffern gibt der Dezimalwert die Ziffern an, die alle "0" sein sollen. Warum sollte es auf "1" in der 16. Position gerundet werden?! "0" liegt näher am "genauen" Dezimalwert als "1".

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X