Mathématiques génériques

.NET 7 introduit de nouvelles interfaces génériques liées aux mathématiques dans la bibliothèque de classes de base. La disponibilité de ces interfaces signifie que vous pouvez contraindre un paramètre de type d’un type ou d’une méthode générique à être « semblable à un nombre ». En outre, C# 11 et versions ultérieures vous permet de définir des static virtualmembres d’interface. Étant donné que les opérateurs doivent être déclarés en tant que static, cette nouvelle fonctionnalité C# permet aux opérateurs d’être déclarés dans les nouvelles interfaces pour les types semblables à des nombres.

Ensemble, ces innovations vous permettent d’effectuer des opérations mathématiques de manière générique, c’est-à-dire sans avoir besoin de connaître le type exact avec lequel vous travaillez. Par exemple, si vous souhaitiez écrire une méthode qui ajoute deux nombres, vous deviez précédemment ajouter une surcharge de la méthode pour chaque type (par exemple, static int Add(int first, int second) et static float Add(float first, float second)). Désormais, vous pouvez écrire une méthode générique unique, où le paramètre de type est contraint d’être un type numérique. Par exemple :

static T Add<T>(T left, T right)
    where T : INumber<T>
{
    return left + right;
}

Dans cette méthode, le paramètre de type T est contraint d’être un type qui implémente la nouvelle interface INumber<TSelf>. INumber<TSelf> implémente l’interface IAdditionOperators<TSelf,TOther,TResult> , qui contient l’opérateur +. Cela permet à la méthode d’ajouter génériquement les deux nombres. La méthode peut être utilisée avec l’un des types numériques intégrés de NET, car ils ont tous été mis à jour pour implémenter INumber<TSelf> dans .NET 7.

Les auteurs de bibliothèque bénéficieront le plus des interfaces mathématiques génériques, car ils peuvent simplifier leur base de code en supprimant les surcharges « redondantes ». D’autres développeurs en bénéficieront indirectement, car les API qu’ils consomment peuvent commencer à prendre en charge davantage de types.

Les interfaces

Les interfaces ont été conçues pour être suffisamment affinées pour permettre aux utilisateurs de définir leurs propres interfaces en haut, tout en étant suffisamment granulaires pour être faciles à consommer. Dans cette mesure, il existe quelques interfaces numériques principales avec lesquelles la plupart des utilisateurs interagiront, par exemple INumber<TSelf> et IBinaryInteger<TSelf>. Les interfaces plus affinées, comme IAdditionOperators<TSelf,TOther,TResult> et ITrigonometricFunctions<TSelf>, prennent en charge ces types et sont disponibles pour les développeurs qui définissent leurs propres interfaces numériques spécifiques au domaine.

Interfaces numériques

Cette section décrit les interfaces dans System.Numerics qui décrivent les types similaires à des nombres et les fonctionnalités disponibles pour ceux-ci.

Nom de l’interface Description
IBinaryFloatingPointIeee754<TSelf> Expose les API communes aux types à virgule flottante binaire1 qui implémentent la norme IEEE 754.
IBinaryInteger<TSelf> Expose les API communes aux entiers binaires2.
IBinaryNumber<TSelf> Expose les API communes aux nombres binaires.
IFloatingPoint<TSelf> Expose les API communes aux types à virgule flottante.
IFloatingPointIeee754<TSelf> Expose les API communes aux types à virgule flottante qui implémentent la norme IEEE 754.
INumber<TSelf> Expose les API communes aux types de nombres comparables (effectivement le domaine des nombres « réels »).
INumberBase<TSelf> Expose les API communes à tous les types de nombres (effectivement le domaine des nombres « complexe »).
ISignedNumber<TSelf> Expose les API communes à tous les types de nombres signés (comme le concept de NegativeOne).
IUnsignedNumber<TSelf> Expose les API communes à tous les types de nombres non signés.
IAdditiveIdentity<TSelf,TResult> Expose le concept de (x + T.AdditiveIdentity) == x.
IMinMaxValue<TSelf> Expose le concept de T.MinValue et T.MaxValue.
IMultiplicativeIdentity<TSelf,TResult> Expose le concept de (x * T.MultiplicativeIdentity) == x.

1Les types à virgule flottante binaire sont Double (double), Halfet Single (float).

2Les types entiers binaires sont Byte (byte), Int16 (short), Int32 (int), Int64 (long), Int128, IntPtr (nint), SByte (sbyte), UInt16 (ushort), UInt32 (uint), UInt64 (ulong), UInt128, et UIntPtr (nuint).

L’interface que vous êtes le plus susceptible d’utiliser directement est INumber<TSelf>, ce qui correspond approximativement à un nombre réel. Si un type implémente cette interface, cela signifie qu’une valeur a un signe (ce qui inclut les types unsigned, considérés comme positifs) et peut être comparée à d’autres valeurs du même type. INumberBase<TSelf> confère des concepts plus avancés, comme des nombres complexes et imaginaires, par exemple la racine carrée d’un nombre négatif. D’autres interfaces, comme IFloatingPointIeee754<TSelf>, ont été créées, car toutes les opérations n’ont pas de sens pour tous les types de nombres, par exemple, le calcul de la valeur plancher d’un nombre n’a de sens que pour les types à virgule flottante. Dans la bibliothèque de classes de base .NET, le type à virgule flottante Double implémente IFloatingPointIeee754<TSelf>, mais Int32 ne l’implémente pas.

Plusieurs interfaces sont également implémentées par différents autres types, notamment Char, DateOnly, DateTime, DateTimeOffset, Decimal, Guid, TimeOnly et TimeSpan.

Le tableau suivant présente quelques-unes des API principales exposées par chaque interface.

Interface Nom de l’API Description
IBinaryInteger<TSelf> DivRem Calcule simultanément le quotient et le reste.
LeadingZeroCount Compte le nombre de bits zéros non significatifs dans la représentation binaire.
PopCount Compte le nombre de bits définis dans la représentation binaire.
RotateLeft Fait pivoter les bits vers la gauche, parfois également appelé décalage circulaire vers la gauche.
RotateRight Fait pivoter les bits vers la droite, parfois également appelé décalage circulaire vers la droite.
TrailingZeroCount Compte le nombre de bits zéros de fin dans la représentation binaire.
IFloatingPoint<TSelf> Ceiling Arrondit la valeur vers l’infini positif. +4,5 devient +5, et -4,5 devient -4.
Floor Arrondit la valeur vers l’infini négatif. +4,5 devient +4, et -4,5 devient -5.
Round Arrondit la valeur en utilisant le mode d’arrondi spécifié.
Truncate Arrondit la valeur vers zéro. +4,5 devient +4, et -4,5 devient -4.
IFloatingPointIeee754<TSelf> E Obtient une valeur représentant le nombre d’Euler pour le type.
Epsilon Obtient la valeur la plus petite pouvant être représentée supérieure à zéro pour le type.
NaN Obtient une valeur représentant NaN pour le type.
NegativeInfinity Obtient une valeur représentant -Infinity pour le type.
NegativeZero Obtient une valeur représentant -Zero pour le type.
Pi Obtient une valeur représentant Pi pour le type.
PositiveInfinity Obtient une valeur représentant +Infinity pour le type.
Tau Obtient une valeur représentant Tau (2 * Pi) pour le type.
(Autre) (Implémente l’ensemble complet d’interfaces répertoriées sous Interfaces de fonction.)
INumber<TSelf> Clamp Limite une valeur à pas plus et pas moins que la valeur minimale et maximale spécifiée.
CopySign Définit le signe d’une valeur spécifiée sur la même valeur qu’une autre valeur spécifiée.
Max Renvoie la valeur la plus élevée de deux valeurs, en renvoyant NaN si l’une des entrées est NaN.
MaxNumber Renvoie la valeur la plus basse de deux valeurs, en renvoyant le nombre si une entrée est NaN.
Min Renvoie la valeur la plus basse de deux valeurs, en renvoyant NaN si l’une des entrées est NaN.
MinNumber Renvoie la valeur la plus basse de deux valeurs, en renvoyant le nombre si une entrée est NaN.
Sign Renvoie -1 pour les valeurs négatives, 0 pour zéro et +1 pour les valeurs positives.
INumberBase<TSelf> One Obtient la valeur 1 pour le type.
Radix Obtient la base pour le type. Int32 renvoie 2. Decimal renvoie 10.
Zero Obtient la valeur 0 pour le type.
CreateChecked Crée une valeur, en levant une OverflowException si l’entrée ne peut pas être ajustée.1
CreateSaturating Crée une valeur, en fixant à T.MinValue ou T.MaxValue si l’entrée ne peut pas être ajustée.1
CreateTruncating Crée une valeur à partir d’une autre valeur, en enveloppant si l’entrée ne peut pas être ajustée.1
IsComplexNumber Renvoie true si la valeur a une partie réelle non nulle et une partie imaginaire non nulle.
IsEvenInteger Renvoie true si la valeur est un entier pair. 2,0 renvoie true et 2,2 renvoie false.
IsFinite Renvoie true si la valeur n’est pas infinie et n’est pas NaN.
IsImaginaryNumber Renvoie true si la valeur a une partie réelle nulle. Cela signifie que 0 est imaginaire et 1 + 1i ne l’est pas.
IsInfinity Renvoie true si la valeur représente l’infini.
IsInteger Renvoie true si la valeur est un entier. 2,0 et 3,0 renvoient true et 2,2 et 3,1 renvoient false.
IsNaN Renvoie true si la valeur représente NaN.
IsNegative Renvoie true si la valeur est négative. Cela inclut -0,0.
IsPositive Renvoie true si la valeur est positive. Cela inclut 0 et +0,0.
IsRealNumber Renvoie true si la valeur a une partie imaginaire nulle. Cela signifie que 0 est réel, de même que tous les types INumber<T>.
IsZero Renvoie true si la valeur représente zéro. Cela inclut 0, +0,0 et -0,0.
MaxMagnitude Renvoie la valeur avec la valeur absolue la plus élevée, en renvoyant NaN si l’une des entrées est NaN.
MaxMagnitudeNumber Renvoie la valeur avec la valeur absolue la plus élevée, en renvoyant le nombre si une entrée est NaN.
MinMagnitude Renvoie la valeur avec la valeur absolue la plus basse, en renvoyant NaN si l’une des entrées est NaN.
MinMagnitudeNumber Renvoie la valeur avec la valeur absolue la plus basse, en renvoyant le nombre si une entrée est NaN.
ISignedNumber<TSelf> NegativeOne Obtient la valeur -1 pour le type.

1Pour mieux comprendre le comportement des trois méthodes Create*, examinez les exemples suivants.

Exemple lorsqu’une valeur est trop élevée :

  • byte.CreateChecked(384) lève un OverflowException.
  • byte.CreateSaturating(384) renvoie 255, car 384 est supérieur à Byte.MaxValue (qui est 255).
  • byte.CreateTruncating(384) renvoie 128, car il prend les 8 bits les plus bas (384 a une représentation hexadécimale de 0x0180, et les 8 bits les plus bas sont 0x80, qui est 128).

Exemple lorsqu’une valeur est trop petite :

  • byte.CreateChecked(-384) lève un OverflowException.
  • byte.CreateSaturating(-384) renvoie 0, car -384 est inférieure à Byte.MinValue (qui est 0).
  • byte.CreateTruncating(-384) renvoie 128, car il prend les 8 bits les plus bas (384 a une représentation hexadécimale de 0xFE80, et les 8 bits les plus bas sont 0x80, qui est 128).

Les méthodes Create* ont également des éléments spécifiques à prendre en considération pour les types à virgule flottante IEEE 754, comme float et double, car ceux-ci ont les valeurs spéciales PositiveInfinity, NegativeInfinity et NaN. Les trois API Create* se comportent comme CreateSaturating. En outre, tandis que MinValue et MaxValue représentent le plus grand nombre négatif/positif « normal », les valeurs minimales et maximales réelles sont NegativeInfinity et PositiveInfinity, par conséquent, elles sont limitées à ces valeurs à la place.

Interfaces d’opérateur

Les interfaces d’opérateur correspondent aux différents opérateurs disponibles pour le langage C#.

  • Ils n’associent explicitement pas les opérations comme la multiplication et la division, car cela n’est pas correct pour tous les types. Par exemple, Matrix4x4 * Matrix4x4 est valide, mais Matrix4x4 / Matrix4x4 n’est pas valide.
  • Ils permettent généralement aux types d’entrée et de résultat de différer pour prendre en charge des scénarios comme la division de deux entiers pour obtenir un double, par exemple, 3 / 2 = 1.5 ou le calcul de la moyenne d’un ensemble d’entiers.
Nom de l’interface Opérateurs définis
IAdditionOperators<TSelf,TOther,TResult> x + y
IBitwiseOperators<TSelf,TOther,TResult> x & y, 'x | y', x ^ y et ~x
IComparisonOperators<TSelf,TOther,TResult> x < y, x > y, x <= y et x >= y
IDecrementOperators<TSelf> --x et x--
IDivisionOperators<TSelf,TOther,TResult> x / y
IEqualityOperators<TSelf,TOther,TResult> x == y et x != y
IIncrementOperators<TSelf> ++x et x++
IModulusOperators<TSelf,TOther,TResult> x % y
IMultiplyOperators<TSelf,TOther,TResult> x * y
IShiftOperators<TSelf,TOther,TResult> x << y et x >> y
ISubtractionOperators<TSelf,TOther,TResult> x - y
IUnaryNegationOperators<TSelf,TResult> -x
IUnaryPlusOperators<TSelf,TResult> +x

Notes

Certaines interfaces définissent un opérateur vérifié en plus d’un opérateur non vérifié normal. Les opérateurs vérifiés sont appelés dans les contextes vérifiés et autorisent un type défini par l’utilisateur à définir le comportement en cas de dépassement. Si vous implémentez un opérateur vérifié, par exemple, CheckedSubtraction(TSelf, TOther), vous devez également implémenter l’opérateur non vérifié, par exemple Subtraction(TSelf, TOther).

Interfaces de fonction

Les interfaces de fonction définissent des API mathématiques courantes qui s’appliquent plus largement qu’à une interface numérique spécifique. Ces interfaces sont toutes implémentées par IFloatingPointIeee754<TSelf>, et peuvent être implémentées par d’autres types pertinents à l’avenir.

Nom de l’interface Description
IExponentialFunctions<TSelf> Expose les fonctions exponentielles prenant en charge e^x, e^x - 1, 2^x, 2^x - 1, 10^x et 10^x - 1.
IHyperbolicFunctions<TSelf> Expose les fonctions hyperboliques prenant en charge acosh(x), asinh(x), atanh(x), cosh(x), sinh(x) et tanh(x).
ILogarithmicFunctions<TSelf> Expose les fonctions logarithmiques prenant en charge ln(x), ln(x + 1), log2(x), log2(x + 1), log10(x) et log10(x + 1).
IPowerFunctions<TSelf> Expose les fonctions de puissances prenant en charge x^y.
IRootFunctions<TSelf> Expose les fonctions racines prenant en charge cbrt(x) et sqrt(x).
ITrigonometricFunctions<TSelf> Expose les fonctions trigonométriques prenant en charge acos(x), asin(x), atan(x), cos(x), sin(x) et tan(x).

Analyser et mettre en forme des interfaces

L’analyse et la mise en forme sont des concepts fondamentaux en programmation. Ils sont couramment utilisés lors de la conversion de l’entrée utilisateur en un type donné ou lors de l’affichage d’un type pour l’utilisateur. Ces interfaces se trouvent dans l’espace de noms System.

Nom de l’interface Description
IParsable<TSelf> Expose la prise en charge de T.Parse(string, IFormatProvider) et T.TryParse(string, IFormatProvider, out TSelf).
ISpanParsable<TSelf> Expose la prise en charge de T.Parse(ReadOnlySpan<char>, IFormatProvider) et T.TryParse(ReadOnlySpan<char>, IFormatProvider, out TSelf).
IFormattable1 Expose la prise en charge de value.ToString(string, IFormatProvider).
ISpanFormattable1 Expose la prise en charge de value.TryFormat(Span<char>, out int, ReadOnlySpan<char>, IFormatProvider).

1Cette interface n’est pas nouvelle, ni générique. Toutefois, elle est implémentée par tous les types de nombres et représente l’opération inverse de IParsable.

Par exemple, le programme suivant prend deux nombres comme entrée, les lisant à partir de la console à l’aide d’une méthode générique où le paramètre de type est contraint d’être IParsable<TSelf>. Il calcule la moyenne à l’aide d’une méthode générique où les paramètres de type pour les valeurs d’entrée et de résultat sont contraintes d’être INumber<TSelf>, puis affiche le résultat dans la console.

using System.Globalization;
using System.Numerics;

static TResult Average<T, TResult>(T first, T second)
    where T : INumber<T>
    where TResult : INumber<TResult>
{
    return TResult.CreateChecked( (first + second) / T.CreateChecked(2) );
}

static T ParseInvariant<T>(string s)
    where T : IParsable<T>
{
    return T.Parse(s, CultureInfo.InvariantCulture);
}

Console.Write("First number: ");
var left = ParseInvariant<float>(Console.ReadLine());

Console.Write("Second number: ");
var right = ParseInvariant<float>(Console.ReadLine());

Console.WriteLine($"Result: {Average<float, float>(left, right)}");

/* This code displays output similar to:

First number: 5.0
Second number: 6
Result: 5.5
*/

Voir aussi