Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
As conversões entre Decimal e tipos binários de ponto flutuante, e as conversões de BigInteger para tipos binários de ponto flutuante, produzem agora resultados corretamente arredondados. Anteriormente, estas conversões podiam truncar, arredondar valores intermédios ou descartar dígitos significativos.
Versão introduzida
.NET 11 Preview 7
Comportamento anterior
As conversões de float e double para decimal mantiveram apenas 7 e 15 dígitos decimais significativos, respetivamente. Isto poderia ocultar a diferença entre um literal binário de ponto flutuante e um literal decimal:
using System.Globalization;
double value = 1.23;
decimal converted = (decimal)value;
Console.WriteLine(converted.ToString("G29", CultureInfo.InvariantCulture));
O resultado foi:
1.23
As conversões de decimal para float ou double podem ser arredondadas mais de uma vez. Por exemplo:
using System.Globalization;
decimal value = 10000000000000.099609375m;
double converted = (double)value;
Console.WriteLine(converted.ToString("G99", CultureInfo.InvariantCulture));
O resultado foi:
10000000000000.09765625
As conversões de decimal para Half ou BFloat16 primeiro converteram o valor para float, o que também pode produzir um resultado arredondado incorretamente.
As conversões de BigInteger para double truncavam os bits descartados em vez de arredondar para o valor representável mais próximo. Por exemplo:
using System.Globalization;
using System.Numerics;
BigInteger value = long.MaxValue / 2;
double converted = (double)value;
Console.WriteLine(converted.ToString("G17", CultureInfo.InvariantCulture));
O resultado foi:
4.6116860184273874E+18
As conversões de BigInteger para float, Half ou BFloat16 convertiam primeiro o valor em double, o que podia provocar um arredondamento duplo e produzir um resultado a uma unidade na última posição do valor representável mais próximo.
Novo comportamento
A partir do .NET 11, as conversões arredondam o valor de origem exato uma vez para o valor de destino representável mais próximo.
Para o primeiro exemplo, o double valor não é exatamente 1.23. O seu valor exato é aproximadamente 1.229999999999999982236431605997..., pelo que o valor convertido decimal é agora:
1.229999999999999982236431606
Para o segundo exemplo, o valor convertido double é agora:
10000000000000.099609375
As conversões de decimal para Half ou BFloat16 também são corretamente arredondadas e podem produzir um padrão de bits de destino diferente do das versões anteriores.
Para o BigInteger exemplo, o valor convertido double é agora:
4.6116860184273879E+18
As conversões de BigInteger para float, Half, ou BFloat16 também estão corretamente arredondadas.
Se uma conversão for avaliada como uma constante de tempo de compilação, um compilador hospedado pelo SDK .NET 11 Preview 7 ou por um SDK posterior pode incorporar o novo resultado na assembly de saída quando o projeto for reconstruído, independentemente da estrutura alvo do projeto.
Tipo de mudança disruptiva
Esta mudança é uma mudança comportamental.
Motivo da mudança
Os algoritmos de conversão anteriores perdiam informação que o tipo de destino podia representar e, por vezes, selecionavam um valor diferente do resultado representável mais próximo. Isto causou erros de precisão em ambas as direções de conversão de decimal e nas conversões a partir de BigInteger. Os novos algoritmos seguem a regra esperada da aritmética de vírgula flutuante para o cálculo da conversão, como se este fosse efetuado com precisão intermédia exata, seguido de um único arredondamento para o tipo de destino.
Para mais informações, veja dotnet/runtime#130565 e dotnet/runtime#130566.
Ação recomendada
Não presuma que um literal binário em ponto flutuante e um literal decimal com o mesmo texto de origem representam o mesmo valor. Se um valor pretende ser decimal, use um literal decimal:
decimal value = 123.4567m;
em vez de uma conversão a partir de um double literal:
decimal value = (decimal)123.4567;
Para restaurar o resultado anterior em geral, ao converter para decimal, arredonda o valor convertido para 7 dígitos decimais significativos para uma float fonte ou 15 dígitos decimais significativos para uma double fonte. A conversão anterior arredondava para o valor mais próximo, com desempate para o par; não truncava. Os valores positivos e negativos foram tratados simetricamente.
Por exemplo, formatar o valor de origem com a cadeia de formato numérico padrão G7 ou G15 e analisar o resultado efetua o arredondamento correspondente aos algarismos significativos para quaisquer valores finitos:
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
A implementação baseada em span não efetua alocações. Valores fora do intervalo de decimal continuam a lançar uma exceção durante a análise sintática, tal como aconteceu durante a conversão.
Atualizar testes e serializar valores esperados que codificavam o resultado anterior, incorretamente arredondado. Isto inclui código que dependia do BigInteger truncamento de conversão em direção a zero. Se uma aplicação exigir um padrão de bits legado específico float, double, Half ou BFloat16 para um protocolo ou formato de ficheiro, codifique explicitamente esse padrão de bits em vez de o reproduzir por meio de uma conversão numérica.
Não existe um interruptor de compatibilidade para restaurar os algoritmos de conversão anteriores.
APIs afetadas
- Decimal.Decimal(Single)
- Decimal.Decimal(Double)
- Conversões explícitas de Single e Double para Decimal
- Conversões explícitas de Decimal para Single e 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) e Decimal.CreateTruncating<TOther>(TOther) quando convertem para ou a partir de Single ou Double
- Conversão explícita de Decimal para Half
- Conversão explícita de Decimal para BFloat16
- Conversões explícitas de BigInteger para Double, Single, Half, e BFloat16
- Conversões equivalentes realizadas através de IConvertible ou através das interfaces matemáticas genéricas