Math.Round e MathF.Round devolvem resultados arredondados corretamente

Math.Round(Double, Int32) e MathF.Round(Single, Int32), e as suas MidpointRounding sobrecargas, agora retornam o valor que está corretamente arredondado para o número solicitado de dígitos fracionários com base no valor exato da entrada. Algumas entradas agora apresentam um resultado diferente (e correto) do que nas versões anteriores. Além disso, o digits argumento já não tem um limite superior.

Versão introduzida

.NET 11 Pré-visualização 7

Comportamento anterior

Anteriormente, Math.Round(value, digits, mode) calculava Round(value * 10^digits, mode) / 10^digits. Como value * 10^digits geralmente não é exatamente representável, valores ligeiramente abaixo (ou acima) de um ponto médio decimal podem escalar para um ponto médio exato ou através de uma fronteira de arredondamento, o que produz um resultado incorretamente arredondado. Valores com grandes magnitudes também podem perder completamente os seus bits fracionários durante o passo de escala.

O digits argumento também se limitava a 0-15 para double e 0-6 para float; valores fora desse intervalo lançavam ArgumentOutOfRangeException.

Math.Round(655.925, 2, MidpointRounding.AwayFromZero);            // 655.93              (incorrect)
Math.Round(1111111111111111.5, 1, MidpointRounding.AwayFromZero); // 1111111111111111.6  (incorrect)
Math.Round(1.5, 16, MidpointRounding.ToEven);                     // throws ArgumentOutOfRangeException

Por exemplo, 655.925 é armazenado como 655.924999999999954525…, que está abaixo do 655.925 ponto médio, pelo que o resultado correto é 655.92.

Novo comportamento

A partir do .NET 11, o resultado é calculado a partir do valor exato da entrada usando aritmética de precisão arbitrária, e o valor devolvido é o valor representável mais próximo do resultado decimal corretamente arredondado.

Além disso, qualquer valor não negativo digits é aceite; apenas valores negativos lançam ArgumentOutOfRangeException. Contagens de dígitos iguais ou além da precisão necessária para contornar o tipo (17 para double, 9 para float) deixam o valor inalterado, que é o resultado correto.

Math.Round(655.925, 2, MidpointRounding.AwayFromZero);            // 655.92              (correct)
Math.Round(1111111111111111.5, 1, MidpointRounding.AwayFromZero); // 1111111111111111.5  (correct)
Math.Round(1.5, 16, MidpointRounding.ToEven);                     // 1.5                 (no longer throws)

Tipo de mudança disruptiva

Esta mudança é uma mudança comportamental.

Motivo da mudança

Os resultados anteriores estavam incorretos para uma grande fração das entradas — cerca de 5% de valores aleatórios ao longo do intervalo suportado digits diferiam do resultado corretamente arredondado. O comportamento anterior também rejeitou ou manuseou mal entradas finitas grandes. A nova implementação é consistente com IEEE: arredonda o valor exato da entrada e devolve o resultado representável mais próximo, que corresponde ao valor já value.ToString("F{digits}") produzido.

Os caps 0-15 e 0-6 digits eram uma limitação artificial ligada à antiga abordagem escala a10^digits escala. Como a implementação exata é correta para qualquer contagem de dígitos, o limite foi levantado ao mesmo tempo para evitar uma segunda quebra comportamental mais tarde.

Para mais informações, veja dotnet/runtime#130574.

A maior parte do código não precisa de alterações e beneficia dos resultados corrigidos.

Se depender do resultado anterior exato (incorreto), arredonda usando explicitamente a abordagem anterior, por exemplo, Math.Round(value * pow10, mode) / pow10. Alternativamente, realiza-se o arredondamento usando decimal quando os valores representam quantidades de base 10, como moeda.

double e float são tipos binários de ponto flutuante e não podem representar exatamente a maioria das frações decimais. Para arredondamento decimal exato das quantidades decimais, prefira Decimal. <System.Numerics.Decimal32>, <System.Numerics.Decimal64> e <System.Numerics.Decimal128> são tipos IEEE 754 baseados em decimais com intervalos e funcionalidades expandidos, sendo também adequados para este tipo de trabalho.

APIs afetadas

O mesmo comportamento corrigido e fluxo de intervalo digits elevado através dos pontos de entrada numéricos da interface que delegam a estes métodos, por exemplo, double.Round, float.Round, Half.Round, e NFloat.Round. Math.Round(Decimal, Int32) e o argumento Math.Round(Double) único e MathF.Round(Single) as sobrecargas não são afetados.