Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Les membres virtuels statiques d’interface vous permettent de définir des interfaces qui incluent des opérateurs surchargés ou d’autres membres statiques. Une fois que vous avez défini des interfaces avec des membres statiques, vous pouvez utiliser ces interfaces comme contraintes pour créer des types génériques qui utilisent des opérateurs ou d’autres méthodes statiques . Même si vous ne créez pas d’interfaces avec des opérateurs surchargés, vous bénéficiez probablement de cette fonctionnalité et des classes mathématiques génériques activées par la mise à jour du langage.
Dans ce tutoriel, vous allez apprendre à :
- Définissez des interfaces avec des membres statiques.
- Utilisez des interfaces pour définir des classes qui implémentent des interfaces avec des opérateurs définis.
- Créez des algorithmes génériques qui s’appuient sur des méthodes d’interface statique.
Prerequisites
- La dernière version du SDK .NET
- Éditeur de code Visual Studio
- Le DevKit C#
Méthodes d’interface abstraite statiques
Commençons par un exemple. La méthode suivante retourne le point moyen de deux double nombres :
public static double MidPoint(double left, double right) =>
(left + right) / (2.0);
La même logique fonctionne pour n’importe quel type numérique : int, , shortlong, floatdecimalou tout type qui représente un nombre. Vous devez avoir un moyen d’utiliser les opérateurs + et /, et de définir une valeur pour 2. Vous pouvez utiliser l’interface pour écrire la System.Numerics.INumber<TSelf> méthode précédente comme méthode générique suivante :
public static T MidPoint<T>(T left, T right)
where T : INumber<T> => (left + right) / T.CreateChecked(2); // note: the addition of left and right may overflow here; it's just for demonstration purposes
Tout type qui implémente l’interface INumber<TSelf> doit inclure une définition pour operator +, et pour operator /. Le dénominateur est défini par T.CreateChecked(2) pour créer la valeur 2 pour tout type numérique, ce qui force le dénominateur à être du même type que les deux paramètres.
INumberBase<TSelf>.CreateChecked<TOther>(TOther) crée une instance du type à partir de la valeur spécifiée et lève une OverflowException si la valeur dépasse la plage représentable. (Cette implémentation a le risque de dépassement de capacité si left et right sont à la fois suffisamment grandes. Il existe d’autres algorithmes qui peuvent éviter ce problème potentiel.)
Vous définissez des membres abstraits statiques dans une interface en utilisant une syntaxe familière : vous ajoutez les modificateurs static et abstract à tout membre statique qui ne fournit pas d'implémentation. L’exemple suivant définit une IGetNext<T> interface qui peut être appliquée à n’importe quel type qui remplace operator ++:
public interface IGetNext<T> where T : IGetNext<T>
{
static abstract T operator ++(T other);
}
La contrainte selon laquelle l’argument de type T implémente IGetNext<T> garantit que la signature de l’opérateur inclut le type contenant, ou son argument de type. De nombreux opérateurs appliquent que ses paramètres doivent correspondre au type ou être le paramètre de type contraint pour implémenter le type conteneur. Sans cette contrainte, l’opérateur ++ n’a pas pu être défini dans l’interface IGetNext<T> .
Vous pouvez créer une structure qui crée une chaîne de caractères « A » où chaque incrément ajoute un autre caractère à la chaîne à l’aide du code suivant :
public struct RepeatSequence : IGetNext<RepeatSequence>
{
private const char Ch = 'A';
public string Text = new string(Ch, 1);
public RepeatSequence() {}
public static RepeatSequence operator ++(RepeatSequence other)
=> other with { Text = other.Text + Ch };
public override string ToString() => Text;
}
Plus généralement, vous pouvez créer n’importe quel algorithme dans lequel vous souhaiterez peut-être définir ++ pour signifier « produire la valeur suivante de ce type ». L’utilisation de cette interface produit du code et des résultats clairs :
var str = new RepeatSequence();
for (int i = 0; i < 10; i++)
Console.WriteLine(str++);
L’exemple précédent génère la sortie suivante :
A
AA
AAA
AAAA
AAAAA
AAAAAA
AAAAAAA
AAAAAAAA
AAAAAAAAA
AAAAAAAAAA
Ce petit exemple illustre la motivation de cette fonctionnalité. Vous pouvez utiliser la syntaxe naturelle pour les opérateurs, les valeurs constantes et d’autres opérations statiques. Vous pouvez explorer ces techniques lorsque vous créez plusieurs types qui s’appuient sur des membres statiques, y compris des opérateurs surchargés. Définissez les interfaces qui correspondent aux fonctionnalités de vos types, puis déclarez la prise en charge de ces types pour la nouvelle interface.
Mathématiques génériques
Le scénario de motivation pour autoriser les méthodes statiques, y compris les opérateurs, dans les interfaces consiste à prendre en charge les algorithmes mathématiques génériques . La bibliothèque de classes de base .NET 7 contient des définitions d’interface pour de nombreux opérateurs arithmétiques et des interfaces dérivées qui combinent de nombreux opérateurs arithmétiques dans une INumber<T> interface. Nous allons appliquer ces types pour générer un Point<T> enregistrement qui peut utiliser n’importe quel type numérique pour T. Vous pouvez déplacer le point en fonction de XOffset et YOffset en utilisant l'opérateur +.
Commencez par créer une nouvelle application console, soit à l'aide de dotnet new, soit en utilisant Visual Studio.
L’interface publique pour les Translation<T> et Point<T> devrait ressembler au code suivant :
// Note: Not complete. This won't compile yet.
public record Translation<T>(T XOffset, T YOffset);
public record Point<T>(T X, T Y)
{
public static Point<T> operator +(Point<T> left, Translation<T> right);
}
Vous utilisez le type record pour les types Translation<T> et Point<T> : les deux stockent deux valeurs et représentent le stockage des données, plutôt qu'un comportement sophistiqué. L’implémentation de operator + se présenterait comme le code suivant :
public static Point<T> operator +(Point<T> left, Translation<T> right) =>
left with { X = left.X + right.XOffset, Y = left.Y + right.YOffset };
Pour que le code précédent soit compilé, vous devez déclarer qu’il T prend en charge l’interface IAdditionOperators<TSelf, TOther, TResult> . Cette interface inclut la operator + méthode statique. Il déclare trois paramètres de type : un pour l’opérande gauche, un pour l’opérande droit et un pour le résultat. Certains types implémentent + pour différents types d’opérandes et de résultats. Ajoutez une déclaration que l’argument de type, T implémente IAdditionOperators<T, T, T>:
public record Point<T>(T X, T Y) where T : IAdditionOperators<T, T, T>
Après avoir ajouté cette contrainte, votre Point<T> classe peut utiliser l’opérateur + d’ajout. Ajoutez la même contrainte sur la Translation<T> déclaration :
public record Translation<T>(T XOffset, T YOffset) where T : IAdditionOperators<T, T, T>;
La IAdditionOperators<T, T, T> contrainte empêche un développeur d’utiliser votre classe pour créer un Translation en utilisant un type qui ne respecte pas la contrainte pour l'ajout à un point. Vous avez ajouté les contraintes nécessaires au paramètre de type pour Translation<T> et Point<T> afin que ce code fonctionne. Vous pouvez tester en ajoutant du code comme ci-dessous avant les déclarations de Translation et Point dans votre fichier Program.cs :
var pt = new Point<int>(3, 4);
var translate = new Translation<int>(5, 10);
var final = pt + translate;
Console.WriteLine(pt);
Console.WriteLine(translate);
Console.WriteLine(final);
Vous pouvez rendre ce code plus réutilisable en déclarant que ces types implémentent les interfaces arithmétiques appropriées. La première modification à apporter consiste à déclarer que Point<T, T> implémente l'interface IAdditionOperators<Point<T>, Translation<T>, Point<T>>. Le Point type utilise différents types pour les opérandes et le résultat. Le type Point implémente déjà un operator + avec cette signature, donc il suffit d'ajouter l’interface à la déclaration.
public record Point<T>(T X, T Y) : IAdditionOperators<Point<T>, Translation<T>, Point<T>>
where T : IAdditionOperators<T, T, T>
Enfin, lorsque vous effectuez un ajout, il est utile d’avoir une propriété qui définit la valeur d’identité additive pour ce type. Il existe une nouvelle interface pour cette fonctionnalité : IAdditiveIdentity<TSelf,TResult>. La traduction de {0, 0} est l'identité additive : le point résultant est le même que l'opérande gauche. L’interface IAdditiveIdentity<TSelf, TResult> définit une propriété en lecture seule, AdditiveIdentityqui retourne la valeur d’identité. Le Translation<T> nécessite quelques modifications pour implémenter cette interface :
using System.Numerics;
public record Translation<T>(T XOffset, T YOffset) : IAdditiveIdentity<Translation<T>, Translation<T>>
where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
public static Translation<T> AdditiveIdentity =>
new Translation<T>(XOffset: T.AdditiveIdentity, YOffset: T.AdditiveIdentity);
}
Il y a quelques changements ici. Nous allons donc les parcourir un par un. Tout d’abord, vous déclarez que le Translation type implémente l’interface IAdditiveIdentity :
public record Translation<T>(T XOffset, T YOffset) : IAdditiveIdentity<Translation<T>, Translation<T>>
Vous pouvez ensuite essayer d’implémenter le membre de l’interface, comme indiqué dans le code suivant :
public static Translation<T> AdditiveIdentity =>
new Translation<T>(XOffset: 0, YOffset: 0);
Le code précédent ne compile pas, car 0 dépend du type. Réponse : Utiliser IAdditiveIdentity<T>.AdditiveIdentity pour 0. Cette modification signifie que vos contraintes doivent désormais inclure que T implémente IAdditiveIdentity<T>. Cela entraîne l’implémentation suivante :
public static Translation<T> AdditiveIdentity =>
new Translation<T>(XOffset: T.AdditiveIdentity, YOffset: T.AdditiveIdentity);
Maintenant que vous avez ajouté cette contrainte sur Translation<T>, vous devez ajouter la même contrainte à Point<T>.
using System.Numerics;
public record Point<T>(T X, T Y) : IAdditionOperators<Point<T>, Translation<T>, Point<T>>
where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
public static Point<T> operator +(Point<T> left, Translation<T> right) =>
left with { X = left.X + right.XOffset, Y = left.Y + right.YOffset };
}
Cet exemple vous a donné un aperçu de la façon dont les interfaces s'agencent en mathématiques génériques. Vous avez appris à :
- Écrivez une méthode qui s’appuyait sur l’interface
INumber<T>afin que cette méthode puisse être utilisée avec n’importe quel type numérique. - Créez un type qui s’appuie sur les interfaces d’ajout pour implémenter un type qui ne prend en charge qu’une seule opération mathématique. Ce type déclare sa prise en charge de ces mêmes interfaces afin qu'il puisse être assemblé de différentes manières. Les algorithmes sont écrits à l’aide de la syntaxe la plus naturelle des opérateurs mathématiques.
Expérimentez ces fonctionnalités et inscrivez des commentaires. Vous pouvez utiliser l’élément de menu Envoyer des commentaires dans Visual Studio ou créer un problème dans le référentiel roslyn sur GitHub. Générez des algorithmes génériques qui fonctionnent avec n’importe quel type numérique. Générez des algorithmes à l’aide de ces interfaces où l’argument de type implémente uniquement un sous-ensemble de fonctionnalités de type nombre. Même si vous ne créez pas de nouvelles interfaces qui utilisent ces fonctionnalités, vous pouvez les utiliser dans vos algorithmes.