Une interface est un contrat : une liste nommée de membres qu'un type promet de fournir. Elle dit ce qu'un type sait faire et rien sur la façon de le faire. Les classes et les structs implémentent des interfaces, et le code écrit pour l'interface fonctionne avec chaque implémentation, y compris celles écrites plus tard.
Déclarer et implémenter une interface
Une interface déclare des membres sans corps. Par convention, son nom commence par I. Une classe l'implémente en l'indiquant après deux-points et en fournissant un membre public pour chacun :
Sortie :
15% off: 68
10 off: 70
Best for 80: 68
Best for 40: 30
BestPrice ne sait rien de PercentOff ni de FixedOff. Elle ne dépend que d'IDiscount, donc ajouter plus tard une classe BuyOneGetOne ne demande aucune modification de sa part. Ce découplage est toute la raison d'être des interfaces.
Quelques règles sur la déclaration :
- Les membres d'une interface sont publics par défaut, et les membres qui les implémentent doivent aussi être publics (sauf en implémentation explicite, voir plus bas).
- Une interface peut déclarer des méthodes, des propriétés, des indexeurs et des événements. Elle ne peut pas déclarer de champs d'instance ni de constructeurs.
- Une classe qui omet un membre ne compile pas : erreur CS0535, « 'FixedOff' does not implement interface member 'IDiscount.Apply(decimal)' ».
- Vous ne pouvez pas créer une interface avec
new. Vous créez une classe qui l'implémente et pouvez la garder dans une variable du type de l'interface :IDiscount d = new FixedOff(5);.
L'interface comme type
Une variable, un paramètre, un champ ou un type de retour peut être déclaré avec une interface. Il accepte alors tout objet qui l'implémente, et n'expose que les membres de l'interface. C'est ainsi qu'on écrit du code indifférent à l'implémentation qu'il reçoit, et ainsi que les tests substituent une imitation :
public class Checkout
{
private readonly IPaymentGateway gateway; // not StripeGateway, not PayPalGateway
public Checkout(IPaymentGateway gateway) { this.gateway = gateway; }
public bool Pay(decimal amount) => gateway.Charge(amount);
}
// Production: new Checkout(new StripeGateway(apiKey))
// Unit test: new Checkout(new FakeGateway(alwaysSucceeds: true))
Les conteneurs d'injection de dépendances d'ASP.NET Core reposent sur ce principe : les services sont enregistrés et demandés par interface.
Implémenter plusieurs interfaces
Une classe a au plus une classe de base mais peut implémenter un nombre quelconque d'interfaces. La classe de base d'abord, puis les interfaces, séparées par des virgules :
Sortie :
INVOICE Studio rent March: 950.00
True
True
Les interfaces peuvent aussi hériter d'autres interfaces : interface IRepository<T> : IReadRepository<T> ajoute des membres à ceux qu'elle hérite, et une classe qui implémente IRepository<T> doit fournir les deux ensembles.
Implémentation explicite d'interface
Parfois, deux interfaces déclarent un membre du même nom avec des sens différents, ou un membre d'interface n'a pas de sens sur la surface publique de la classe. Implémentez-le explicitement en le préfixant du nom de l'interface et en omettant le modificateur d'accès :
Sortie :
Desk lamp,34.90
Desk lamp for 34.90
Un membre implémenté explicitement n'est accessible que via l'interface. Le framework s'en sert pour épargner du bruit aux types courants : les tableaux implémentent IList.Add explicitement (la méthode lève NotSupportedException, puisque les tableaux ont une taille fixe), donc elle n'apparaît pas quand vous tapez array..
Les interfaces du framework que vous implémenterez
Implémenter une interface standard branche votre type sur le code existant du framework. Trois reviennent sans cesse.
IComparable<T> donne à un type un ordre naturel, qu'utilisent List<T>.Sort(), Array.Sort et Max(). CompareTo renvoie un nombre négatif, zéro ou un nombre positif :
Sortie :
1.4, 2.9, 2.10, 10.0
Trier les mêmes versions sous forme de chaînes donnerait 1.4, 10.0, 2.10, 2.9. Sans IComparable<T>, Sort() lève InvalidOperationException, car il n'a aucun moyen de comparer deux objets Version.
IEnumerable<T> rend un type utilisable dans foreach et avec LINQ. La façon simple d'implémenter GetEnumerator passe par yield return, présenté sur la page IEnumerable et yield.
IDisposable marque un type qui détient quelque chose à libérer (un descripteur de fichier, une connexion, un timer). Sa seule méthode, Dispose, est ce qu'appelle l'instruction using à la fin du bloc, même si une exception est levée :
Sortie :
open sales.txt
write to sales.txt: March total: 12400
close sales.txt
after using
Méthodes d'interface par défaut (C# 8)
Ajouter un membre à une interface publiée cassait auparavant chaque classe qui l'implémentait. Depuis C# 8, un membre d'interface peut avoir un corps, dont héritent les classes qui l'implémentent, sauf si elles fournissent le leur :
public interface ILogger
{
void Write(string message);
// New in version 2 of the library. Existing implementers keep compiling.
void Error(string message) => Write("ERROR: " + message);
}
public class ConsoleLogger : ILogger
{
public void Write(string message) => Console.WriteLine(message);
}
ILogger log = new ConsoleLogger();
log.Error("disk full"); // ERROR: disk full
var direct = new ConsoleLogger();
// direct.Error("x"); // does not compile: the default method belongs to the interface
La dernière ligne est ce qui surprend : une méthode par défaut n'est pas héritée parmi les membres propres de la classe, elle n'est donc appelable que via le type de l'interface. Les méthodes par défaut ne peuvent pas non plus utiliser de champs d'instance (les interfaces n'en ont pas) ; elles passent par les autres membres de l'interface. Elles exigent .NET Core 3.0 ou plus et ne sont pas disponibles sur .NET Framework.
C# 8 a aussi autorisé les membres statiques dans les interfaces, et C# 11 a ajouté les membres statiques abstraits, qui permettent au code générique d'appeler une méthode statique ou un opérateur sur T. C'est la base des mathématiques génériques de .NET 7 (INumber<T>, where T : INumber<T>).
Interface ou classe abstraite ?
Utilisez une interface pour une capacité que des types sans rapport peuvent partager, quand des structs doivent y participer, ou quand une classe a besoin de plusieurs rôles de ce genre. Utilisez une classe abstraite quand les implémentations sont des variantes d'une même chose et partagent un état ou un algorithme fixe. La page sur les classes abstraites propose un tableau comparatif. Beaucoup de conceptions utilisent les deux : une interface pour les consommateurs, et une classe de base abstraite qui implémente les parties fastidieuses pour les implémenteurs.
Erreurs courantes
- Une implémentation manquante ou non publique. Chaque membre d'interface a besoin d'un membre public de signature correspondante, ou d'une implémentation explicite. Sinon CS0535 (ou CS0737 quand la méthode existe mais n'est pas publique).
- Des interfaces à une seule implémentation « au cas où ». Une interface mérite sa place quand il y a plusieurs implémentations ou une imitation pour les tests. Sinon, c'est un fichier de plus à parcourir.
- Des interfaces obèses. Une interface de vingt membres oblige chaque implémenteur à écrire vingt membres. Découpez-la en rôles plus petits (
IReader,IWriter) que les classes combinent. - Reconvertir une interface en classe.
((StripeGateway)gateway).Refund()annule le découplage. Si les appelants ont besoin deRefund, cette méthode appartient à l'interface. - S'attendre à appeler une méthode d'interface par défaut sur la classe. On l'atteint via le type de l'interface.
Questions fréquentes
Qu'est-ce qu'une interface en C# ?
Une interface est un ensemble nommé de membres (méthodes, propriétés, événements, indexeurs) qu'un type promet de fournir, sans état d'instance. interface IDiscount { decimal Apply(decimal price); } dit « tout ce qui est un IDiscount peut s'appliquer à un prix ». Les classes et les structs l'implémentent avec deux-points, et le code peut ensuite travailler avec n'importe lequel d'entre eux via le type de l'interface.
Comment implémenter une interface en C# ?
Indiquez-la après deux-points dans la déclaration de la classe (après la classe de base, s'il y en a une) et fournissez un membre public pour chaque membre de l'interface : class HolidayDiscount : IDiscount { public decimal Apply(decimal price) => price * 0.9m; }. Un membre manquant donne l'erreur CS0535. Visual Studio et Rider peuvent générer les squelettes avec une correction rapide.
Une classe C# peut-elle implémenter plusieurs interfaces ?
Oui, autant que nécessaire, séparées par des virgules : class Invoice : IPrintable, IComparable<Invoice>, IDisposable. C'est la réponse de C# à l'héritage multiple : une classe a une seule classe de base mais peut jouer de nombreux rôles. Si deux interfaces déclarent le même membre, une seule méthode publique peut satisfaire les deux, ou vous pouvez utiliser l'implémentation explicite pour donner à chacune la sienne.
Qu'est-ce que l'implémentation explicite d'interface en C# ?
Implémenter un membre en le préfixant du nom de l'interface, sans modificateur d'accès : string IExportable.Format() { ... }. Le membre n'est alors appelable que via une variable du type de l'interface, pas via la classe. On s'en sert pour résoudre des conflits de noms entre deux interfaces et pour garder les membres d'interface rarement utilisés hors de la surface publique de la classe.
Que sont les méthodes d'interface par défaut en C# ?
Depuis C# 8, un membre d'interface peut avoir un corps : void Error(string m) => Write("ERROR: " + m);. Les classes qui implémentent l'interface le reçoivent gratuitement et peuvent fournir le leur. La fonctionnalité existe pour que les auteurs de bibliothèques puissent ajouter des membres à une interface publiée sans casser chaque implémentation. Une méthode par défaut s'atteint via le type de l'interface, pas via la classe.
Pourquoi les noms d'interfaces C# commencent-ils par I ?
C'est la convention de nommage .NET (IEnumerable, IDisposable, IComparable<T>) : un I majuscule suivi d'un nom en PascalCase, souvent un adjectif anglais en « able ». Le compilateur ne l'exige pas, mais la suivre rend les types d'interface reconnaissables au premier coup d'œil, surtout dans une déclaration de classe où la classe de base et les interfaces partagent une même liste.