Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Преобразования между 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
- Decimal.Decimal(Single)
- Decimal.Decimal(Double)
- Явные преобразования от Double и Single к Decimal
- Явные преобразования из Decimal в Single и Double
- Decimal.ToSingle(Decimal)
- Decimal.ToDouble(Decimal)
- Convert.ToDecimal(Single)
- Convert.ToDecimal(Double)
- Convert.ToSingle(Decimal)
- Convert.ToDouble(Decimal)
- Decimal.CreateChecked<TOther>(TOther), Decimal.CreateSaturating<TOther>(TOther)и Decimal.CreateTruncating<TOther>(TOther) когда они преобразуются в или из Single или Double
- Явное преобразование из Decimal в Half
- Явное преобразование из Decimal в BFloat16
- Явные преобразования из BigInteger в Double, Half, Single и BFloat16
- Эквивалентные преобразования, выполняемые через IConvertible или через обобщённые математические интерфейсы