Dezimal- und BigInteger-Gleitkommakonvertierungen werden korrekt gerundet

Konvertierungen zwischen Decimal und binären Gleitkommatypen und Konvertierungen von BigInteger zu binären Gleitkommatypen erzeugen jetzt ordnungsgemäß gerundete Ergebnisse. Bisher konnten diese Konvertierungen Werte kürzen, über Zwischenwerte runden oder signifikante Stellen verwerfen.

Eingeführt in Version

.NET 11 Vorschau 7

Bisheriges Verhalten

Konvertierungen von float bzw double . zu decimal beibehalten nur 7 bzw. 15 signifikante Dezimalziffern. Dadurch könnte der Unterschied zwischen einem binären Gleitkommaliteral und einem Dezimalliteral ausgeblendet werden:

using System.Globalization;

double value = 1.23;
decimal converted = (decimal)value;

Console.WriteLine(converted.ToString("G29", CultureInfo.InvariantCulture));

Die Ausgabe lautete:

1.23

Konvertierungen von decimal zu float oder double könnten mehrmals gerundet werden. Beispiel:

using System.Globalization;

decimal value = 10000000000000.099609375m;
double converted = (double)value;

Console.WriteLine(converted.ToString("G99", CultureInfo.InvariantCulture));

Die Ausgabe lautete:

10000000000000.09765625

Konvertierungen von decimal zu Half oder BFloat16 konvertierten den Wert zunächst zu float, was ebenfalls zu einem fehlerhaft gerundeten Ergebnis führen konnte.

Konvertierungen von BigInteger zu double schnitten verworfene Bits ab, anstatt auf den nächstliegenden darstellbaren Wert zu runden. Beispiel:

using System.Globalization;
using System.Numerics;

BigInteger value = long.MaxValue / 2;
double converted = (double)value;

Console.WriteLine(converted.ToString("G17", CultureInfo.InvariantCulture));

Die Ausgabe lautete:

4.6116860184273874E+18

Konvertierungen von BigInteger zu float, Half oder BFloat16 konvertierten den Wert zunächst in double, was zu einer doppelten Rundung führen und ein Ergebnis liefern konnte, das um eine Einheit in der letzten Stelle vom nächstgelegenen darstellbaren Wert abwich.

Neues Verhalten

Ab .NET 11 rundet die Konvertierung den exakten Quellwert einmal auf den nächstgelegenen darstellbaren Zielwert ab.

Für das erste Beispiel ist der double Wert nicht genau 1.23. Der genaue Wert ist ungefähr 1.229999999999999982236431605997..., sodass der konvertierte decimal Wert jetzt lautet:

1.229999999999999982236431606

Für das zweite Beispiel lautet der konvertierte double Wert jetzt:

10000000000000.099609375

Konvertierungen von decimal zu Half oder BFloat16 sind ebenfalls korrekt gerundet und können ein anderes Zielbitmuster erzeugen als in früheren Versionen.

Für das BigInteger Beispiel lautet der konvertierte double Wert jetzt:

4.6116860184273879E+18

Konvertierungen von BigInteger zu float, Halfoder BFloat16 sind auch korrekt gerundet.

Wenn eine Konvertierung als Kompilierungszeitkonstante ausgewertet wird, kann ein compiler, der vom .NET 11 Preview 7 SDK oder einem höheren SDK gehostet wird, das neue Ergebnis in die Ausgabeassembly einbetten, wenn das Projekt neu erstellt wird, unabhängig vom Zielframework des Projekts.

Art der einschneidenden Änderung

Diese Änderung ist eine Verhaltensänderung.

Grund für die Änderung

Bei den bisherigen Konvertierungsalgorithmen gingen Informationen verloren, die vom Zieltyp dargestellt werden konnten, und manchmal wählten sie einen anderen Wert als den nächstliegenden darstellbaren Wert. Dies führte zu Genauigkeitsfehlern in beiden Richtungen der decimal Konvertierung und in Konvertierungen von BigInteger. Die neuen Algorithmen folgen der erwarteten Gleitkommaregel der Konvertierung so, als ob mit exakter Zwischengenauigkeit, gefolgt von einer einzelnen Rundung auf den Zieltyp.

Weitere Informationen finden Sie unter dotnet/runtime#130565 und dotnet/runtime#130566.

Gehen Sie nicht davon aus, dass ein binäres Gleitkommaliteral und ein Dezimalliteral mit demselben Quelltext denselben Wert darstellen. Wenn ein Wert dezimal sein soll, verwenden Sie ein Dezimalliteral:

decimal value = 123.4567m;

anstelle einer Konvertierung von einem double-Literal:

decimal value = (decimal)123.4567;

Um im Allgemeinen das vorherige Ergebnis bei der Konvertierung in decimal wiederherzustellen, runden Sie den konvertierten Wert bei einer float-Quelle auf 7 signifikante Dezimalstellen oder bei einer double-Quelle auf 15 signifikante Dezimalstellen. Die vorherige Konvertierung wurde zum nächstgelegenen Wert gerundet, bei Gleichstand zur geraden Zahl; sie wurde nicht abgeschnitten. Positive und negative Werte wurden symmetrisch behandelt.

Beispielsweise bewirkt das Formatieren des Ausgangswerts mit der numerischen Standardformatzeichenfolge G15 oder G7 und das anschließende Analysieren des Ergebnisses die entsprechende Rundung auf signifikante Stellen für beliebige endliche Werte:

using System.Globalization;

static decimal ConvertToDecimalLikePrevious(float value)
{
    Span<char> text = stackalloc char[32];
    value.TryFormat(text, out int length, "G7", CultureInfo.InvariantCulture);
    return decimal.Parse(text[..length], NumberStyles.Float, CultureInfo.InvariantCulture);
}

static decimal ConvertToDecimalLikePrevious(double value)
{
    Span<char> text = stackalloc char[32];
    value.TryFormat(text, out int length, "G15", CultureInfo.InvariantCulture);
    return decimal.Parse(text[..length], NumberStyles.Float, CultureInfo.InvariantCulture);
}

decimal fromFloat = ConvertToDecimalLikePrevious(14.1f);          // 14.1
decimal fromDouble = ConvertToDecimalLikePrevious(-123.4567);     // -123.4567

Die spanbasierte Implementierung allokiert keinen Speicher. Werte außerhalb des Bereichs von decimal lösen beim Parsen weiterhin eine Ausnahme aus, wie bereits bei der Konvertierung.

Aktualisieren Sie Tests und serialisierte erwartete Werte, die das vorherige, falsch gerundete Ergebnis codiert haben. Dies schließt Code ein, der von der BigInteger Konvertierungskürzung in Richtung Null abhängt. Wenn eine Anwendung für ein Protokoll oder Dateiformat ein bestimmtes Legacy-float-, double-, Half- oder BFloat16-Bitmuster erfordert, kodieren Sie dieses Bitmuster explizit, anstatt es durch eine numerische Konvertierung zu reproduzieren.

Es gibt keinen Kompatibilitätswechsel, um die vorherigen Konvertierungsalgorithmen wiederherzustellen.

Betroffene APIs