Преобразования Decimal и BigInteger в числа с плавающей запятой округляются корректно

Преобразования между 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 в Half или BFloat16 также выполняются с правильным округлением и могут приводить к иному битовому шаблону назначения, чем в предыдущих версиях.

Для примера BigInteger преобразованное значение double теперь такое:

4.6116860184273879E+18

Преобразования из Half в float, BigInteger или BFloat16 также округляются корректно.

Если преобразование вычисляется как константа времени компиляции, компилятор из .NET 11 Preview 7 SDK или более поздней версии SDK может встроить новый результат в выходную сборку при повторной сборке проекта независимо от того, на какую целевую платформу ориентирован проект.

Тип разрушающего изменения

Это изменение поведения.

Причина изменения

Предыдущие алгоритмы преобразования потеряли информацию о том, что тип назначения может представлять и иногда выбрал значение, отличное от ближайшего представляющего результата. Это привело к ошибкам точности при преобразовании decimal в обоих направлениях и при преобразованиях из BigInteger. Новые алгоритмы соответствуют ожидаемому правилу вычисления с плавающей запятой при выполнении преобразования: как если бы промежуточные вычисления выполнялись с точной промежуточной точностью, после чего выполняется одно округление до целевого типа.

Дополнительные сведения см. в разделе dotnet/runtime#130565 и dotnet/runtime#130566.

Не предполагайте, что двоичный литерал с плавающей запятой и десятичный литерал с тем же исходным текстом представляет то же значение. Если значение должно быть десятичным, используйте десятичный литерал:

decimal value = 123.4567m;

вместо преобразования из литерала double:

decimal value = (decimal)123.4567;

Чтобы восстановить предыдущий результат в целом при преобразовании decimal, округите преобразованное значение до 7 значимых десятичных цифр для float источника или 15 значимых десятичных цифр для double источника. Предыдущее преобразование выполняло округление до ближайшего значения, а в случае равенства — к чётному; оно не усекало. Положительные и отрицательные значения обрабатываются симметрично.

Например, форматирование исходного значения с использованием строки стандартного числового формата G7 или G15 и последующий разбор результата обеспечивают соответствующее округление до значащих цифр для произвольных конечных значений:

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

Реализация на основе Span не выполняет выделений памяти. Значения за пределами decimal диапазона продолжают вызывать исключение во время синтаксического анализа, как и во время преобразования.

Обновите тесты и сериализованные ожидаемые значения, в которых был закодирован прежний неправильно округлённый результат. Сюда входит код, который зависит от BigInteger усечения преобразования к нулю. Если приложению требуется определённый устаревший битовый шаблон float, double, Half или BFloat16 для протокола или формата файла, явно закодируйте этот битовый шаблон, а не воспроизводите его посредством числового преобразования.

Нет переключения совместимости для восстановления предыдущих алгоритмов преобразования.

Затронутые API