Math.Round y MathF.Round devuelven resultados redondeados correctamente

Math.Round(Double, Int32) y MathF.Round(Single, Int32), y sus MidpointRounding sobrecargas, ahora devuelven el valor que se redondea correctamente al número solicitado de dígitos fraccionarios en función del valor exacto de la entrada. Algunas entradas ahora redondean a un resultado diferente (y correcto) que en versiones anteriores. Además, el digits argumento ya no tiene un límite superior.

Versión introducida

.NET 11 Preview 7

Comportamiento anterior

Anteriormente, Math.Round(value, digits, mode) calculaba Round(value * 10^digits, mode) / 10^digits. Dado que value * 10^digits generalmente no se puede representar exactamente, los valores ligeramente por debajo (o superior) de un punto medio decimal podrían escalarse en un punto medio exacto o a través de un límite de redondeo, lo que produjo un resultado redondeado incorrectamente. Los valores con grandes magnituds también podrían perder sus bits fraccionarios por completo durante el paso de escalado.

El digits argumento también se limitó a 0-15 para double y 0-6 para float; los valores fuera de ese intervalo lanzaron 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 ejemplo, 655.925 se almacena como 655.924999999999954525…, que está por debajo del 655.925 punto medio, por lo que el resultado correcto es 655.92.

Nuevo comportamiento

A partir de .NET 11, el resultado se calcula a partir del valor exacto de la entrada mediante la aritmética de precisión arbitraria y el valor devuelto es el valor representable más cercano al resultado decimal redondeado correctamente.

Además, se acepta cualquier valor no negativo digits ; solo se inician ArgumentOutOfRangeExceptionvalores negativos. Los recuentos de dígitos en o más allá de la precisión necesaria para recorrer de ida y vuelta el tipo (17 para double, 9 para float) dejan el valor sin cambios, que es el resultado correcto.

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 cambio disruptivo

Este es un cambio de comportamiento.

Motivo del cambio

Los resultados anteriores eran incorrectos para una gran fracción de entradas: aproximadamente 5% de valores aleatorios en el intervalo admitido digits difieren del resultado redondeado correctamente. El comportamiento anterior también rechazó o controló incorrectamente entradas finitas grandes. La nueva implementación es coherente con IEEE: redondea el valor exacto de la entrada y devuelve el resultado representable más cercano, que coincide con el valor que value.ToString("F{digits}") ya se ha producido.

Los límites 0-15 y 0-6 digits eran una limitación artificial vinculada al enfoque de escalado a10^digits escala antiguo. Dado que la implementación exacta es correcta para cualquier recuento de dígitos, el límite se levantó al mismo tiempo para evitar una segunda interrupción del comportamiento más adelante.

Para obtener más información, vea dotnet/runtime#130574.

La mayoría del código no necesita cambios y ventajas de los resultados corregidos.

Si depende de la salida exacta anterior (incorrecta), redondee mediante el enfoque anterior explícitamente, por ejemplo, Math.Round(value * pow10, mode) / pow10. Como alternativa, realice el redondeo utilizando decimal cuando los valores representan cantidades base-10 como moneda.

double y float son tipos de punto flotante binarios y no pueden representar exactamente la mayoría de las fracciones decimales. Para el redondeo decimal exacto de cantidades decimales, prefiera Decimal. <System.Numerics.Decimal32>, <System.Numerics.Decimal64> y <System.Numerics.Decimal128> son tipos basados en decimales IEEE 754 con rangos expandidos y funcionalidad, y también son adecuados para este tipo de trabajo.

Las APIs afectadas

El mismo comportamiento corregido y el flujo de intervalo elevado digits a través de los puntos de entrada de la interfaz numérica que se delegan a estos métodos, por ejemplo, double.Round, float.Round, Half.Roundy NFloat.Round. Math.Round(Decimal, Int32) y los argumentos Math.Round(Double) únicos y MathF.Round(Single) las sobrecargas no se ven afectados.