Les conversions à virgule flottante Decimal et BigInteger sont correctement arrondies

Les conversions entre Decimal et les types à virgule flottante binaire, ainsi que les conversions de BigInteger vers les types à virgule flottante binaire, produisent désormais des résultats correctement arrondis. Auparavant, ces conversions pouvaient tronquer, arrondir les valeurs intermédiaires ou ignorer des chiffres significatifs.

Version introduite

.NET 11 Preview 7

Comportement antérieur

Les conversions de float et de double vers decimal n’ont conservé que 7 et 15 chiffres décimaux significatifs, respectivement. Cela peut masquer la différence entre un littéral à virgule flottante binaire et un littéral décimal :

using System.Globalization;

double value = 1.23;
decimal converted = (decimal)value;

Console.WriteLine(converted.ToString("G29", CultureInfo.InvariantCulture));

La sortie était :

1.23

Les conversions de decimal vers double ou float peuvent être arrondies plusieurs fois. Par exemple:

using System.Globalization;

decimal value = 10000000000000.099609375m;
double converted = (double)value;

Console.WriteLine(converted.ToString("G99", CultureInfo.InvariantCulture));

La sortie était :

10000000000000.09765625

Les conversions de decimal vers Half ou BFloat16 convertissaient d’abord la valeur en float, ce qui pouvait également produire un résultat arrondi incorrect.

Les conversions de BigInteger en double tronquaient les bits supprimés au lieu d’arrondir à la valeur représentable la plus proche. Par exemple:

using System.Globalization;
using System.Numerics;

BigInteger value = long.MaxValue / 2;
double converted = (double)value;

Console.WriteLine(converted.ToString("G17", CultureInfo.InvariantCulture));

La sortie était :

4.6116860184273874E+18

Conversions de BigInteger vers float, Halfou BFloat16 d’abord converti la valeur en double, qui peut arrondir deux fois et produire un résultat d’une unité à la dernière place loin de la valeur représentée la plus proche.

Nouveau comportement

À compter de .NET 11, les conversions arrondient la valeur source exacte une fois à la valeur de destination la plus proche pouvant être représentée.

Pour le premier exemple, la double valeur n’est pas exactement 1.23. Sa valeur exacte est approximativement 1.229999999999999982236431605997..., de sorte que la valeur convertie decimal est maintenant :

1.229999999999999982236431606

Pour le deuxième exemple, la valeur convertie double est maintenant :

10000000000000.099609375

Les conversions vers decimalHalf ou BFloat16 sont également correctement arrondies et peuvent produire un modèle de bits de destination différent de celui des versions précédentes.

Pour l’exemple BigInteger , la valeur convertie double est maintenant :

4.6116860184273879E+18

Les conversions de BigInteger vers float, Halfou BFloat16 sont également correctement arrondies.

Si une conversion est évaluée comme une constante au moment de la compilation, un compilateur hébergé par le SDK .NET 11 Preview 7 ou un sdk ultérieur peut incorporer le nouveau résultat dans l'assembly de sortie lorsque le projet est reconstruit, quel que soit l'infrastructure cible du projet.

Type de changement cassant

Ce changement est un changement de comportement.

Raison du changement

Les algorithmes de conversion précédents ont perdu des informations que le type de destination peut représenter et parfois sélectionné une valeur autre que le résultat le plus proche pouvant être représenté. Cela a entraîné des erreurs de précision dans les deux sens de conversion de decimal et dans les conversions à partir de BigInteger. Les nouveaux algorithmes suivent la règle attendue de l’arithmétique en virgule flottante pour le calcul de la conversion, comme si celle-ci se faisait avec une précision intermédiaire exacte, suivie d’un seul arrondi vers le type cible.

Pour plus d’informations, consultez dotnet/runtime#130565 et dotnet/runtime#130566.

Ne supposez pas qu’un littéral à virgule flottante binaire et un littéral décimal avec le même texte source représentent la même valeur. Si une valeur est destinée à être décimale, utilisez un littéral décimal :

decimal value = 123.4567m;

au lieu d’une conversion à partir d’un double littéral :

decimal value = (decimal)123.4567;

Pour restaurer le résultat précédent en général lorsque vous effectuez une conversion decimal, arrondissez la valeur convertie à 7 chiffres décimaux significatifs pour une float source ou 15 chiffres décimaux significatifs pour une double source. La conversion précédente a arrondi à l’entier le plus proche, en cas d’égalité à l’entier pair ; elle n’a pas tronqué. Les valeurs positives et négatives ont été gérées symétriement.

Par exemple, le formatage de la valeur source avec la chaîne de format numérique standard G7 ou G15, puis l’analyse du résultat, effectue l’arrondi au nombre correspondant de chiffres significatifs pour des valeurs finies arbitraires :

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

L’implémentation basée sur les spans n’effectue pas d’allocation. Les valeurs en dehors de la plage de decimal continuent de générer une exception lors de l’analyse, comme lors de la conversion.

Mettez à jour les tests et les valeurs attendues sérialisées qui encodaient le résultat précédent, mal arrondi. Cela inclut le code qui dépendait de la BigInteger troncation de conversion vers zéro. Si une application nécessite un motif de bits spécifique existant float, Half, double ou BFloat16 pour un protocole ou un format de fichier, encodez explicitement ce motif de bits plutôt que de le reproduire au moyen d’une conversion numérique.

Il n’existe aucun commutateur de compatibilité pour restaurer les algorithmes de conversion précédents.

API affectées