小數與大整數浮點轉換會正確四捨五入

Decimal 與二元浮點數型別之間的轉換,以及從 BigInteger 轉換為二元浮點數型別,現在都會產生正確捨入的結果。 過去,這些轉換可能會截斷、捨棄中間值,或捨棄有效數字。

所推出的版本

.NET 11 預覽版 7

以前的行為

從 float 和 double 轉換為 decimal 的結果,分別僅保留 7 位和 15 位有效十進位數字。 這可以隱藏二進位浮點數文字與十進位文字之間的差異:

using System.Globalization;

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

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

輸出為:

1.23

從 decimal 轉換為 float 或 double 時,可能會經過多次捨入。 例如:

using System.Globalization;

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

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

輸出為:

10000000000000.09765625

從 decimal 轉換為 Half 或 BFloat16 時,會先將該值轉換為 float,這也可能產生不正確的四捨五入結果。

從 BigInteger 轉換為 double 時,會截斷被捨棄的位元,而不是四捨五入到最接近的可表示值。 例如:

using System.Globalization;
using System.Numerics;

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

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

輸出為:

4.6116860184273874E+18

從 BigInteger 轉換為 float、Half 或 BFloat16 時,會先將該值轉換為 double,這可能會發生兩次四捨五入,並產生與最接近的可表示值相差最後有效位數一個單位的結果。

新行為

從 .NET 11 開始,轉換會將精確的來源值繞回一次,直到最接近可表示的目標值。

在第一個範例中,double 的值並不完全等於 1.23。 其精確值約為 1.229999999999999982236431605997...,因此轉換後 decimal 的值為:

1.229999999999999982236431606

第二個例子的 double 轉換值現在為:

10000000000000.099609375

從 decimal 轉換為 BFloat16 或 Half 時,也會正確地捨入,而且產生的目標位元模式可能與先前版本不同。

以範例 BigInteger 來說,轉換後的 double 值現在為:

4.6116860184273879E+18

從 BigInteger 轉換為 Half、float 或 BFloat16 也都會正確四捨五入。

若轉換被評估為編譯時常數,則由 .NET 11 Preview 7 SDK 或後續 SDK 所託管的編譯器,可在重建專案時將新結果嵌入輸出組合中,無論專案的目標框架為何。

破壞性變更的類型

此變更為行為變更。

變更原因

先前的轉換演算法會遺失目的類型可能代表的資訊,有時甚至選擇非最接近可表示結果的值。 這導致在 BigInteger 轉換的兩個方向上,以及從 decimal 進行轉換時,都出現了精度誤差。 新演算法遵循預期的浮點轉換計算規則:如同先以精確的中間精度進行計算,再對目標型別執行一次捨入。

欲了解更多資訊,請參閱 dotnet/runtime#130565 及 dotnet/runtime#130566。

不要假設具有相同原始文字的二進位浮點數文字和十進位文字代表相同的值。 若值為十進位,請使用十進位字面值:

decimal value = 123.4567m;

而不是從 double 字面值進行轉換:

decimal value = (decimal)123.4567;

一般而言,當您轉換為 decimal 時,若要還原先前的結果,請將轉換後的值四捨五入至:若來源為 float,則取 7 位十進位有效數字;若來源為 double,則取 15 位十進位有效數字。 先前的轉換採用就近捨入,遇到中間值時捨入為偶數;並非截斷處理。 正負值則是對稱處理的。

例如,將來源值格式化為G7G15標準數值字串並解析結果,即可對任意有限值進行相應的有意義位數四捨五入:

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

基於跨度的實作不會分配。 超出 範圍 decimal 的值在解析過程中仍會拋出例外,就像轉換時一樣。

更新測試並序列化預期值,將先前錯誤四捨五入的結果編碼。 這包括依賴 BigInteger 在轉換時朝零截斷的程式碼。 若應用程式需要特定舊有floatdouble、 、 Half或BFloat16位元模式用於協定或檔案格式,應明確編碼該位元模式,而非透過數值轉換重現。

沒有相容開關可以恢復之前的轉換演算法。

受影響的 API