méthodes d’interface par défaut
Remarque
Cet article est une spécification de fonctionnalité. La spécification sert de document de conception pour la fonctionnalité. Il inclut les modifications de spécification proposées, ainsi que les informations nécessaires pendant la conception et le développement de la fonctionnalité. Ces articles sont publiés jusqu’à ce que les modifications de spécification proposées soient finalisées et incorporées dans la spécification ECMA actuelle.
Il peut y avoir des différences entre la spécification de la fonctionnalité et l’implémentation terminée. Ces différences sont consignées dans les notes pertinentes de la réunion de conception linguistique (LDM).
Vous pouvez en savoir plus sur le processus d’adoption des speclets de fonctionnalités dans la norme de langage C# dans l’article sur les spécifications .
Résumé
Ajout de la prise en charge des méthodes d’extension virtuelles, c’est-à-dire des méthodes dans les interfaces avec des implémentations concrètes. Une classe ou une structure qui implémente une telle interface doit avoir une implémentation unique la plus spécifique pour la méthode de l’interface, soit implémentée par la classe ou la structure, soit héritée de ses classes ou interfaces de base. Les méthodes d’extension virtuelle permettent à un auteur d’API d’ajouter des méthodes à une interface dans les versions ultérieures sans rompre la compatibilité source ou binaire avec les implémentations existantes de cette interface.
Ces méthodes sont similaires aux « Méthodes par défaut » de Java.
(En fonction de la technique d’implémentation probable) cette fonctionnalité nécessite une prise en charge correspondante dans l’interface CLI/CLR. Les programmes qui tirent parti de cette fonctionnalité ne peuvent pas s’exécuter sur des versions antérieures de la plateforme.
Motivation
Les principales motivations de cette fonctionnalité sont
- Les méthodes d’interface par défaut permettent à un auteur d’API d’ajouter des méthodes à une interface dans les versions ultérieures sans rompre la compatibilité source ou binaire avec les implémentations existantes de cette interface.
- La fonctionnalité permet à C# d’interagir avec les API ciblant Android (Java) et iOS (Swift), qui prennent en charge des fonctionnalités similaires.
- Il s’avère que l’ajout d’implémentations d’interface par défaut fournit les éléments de la fonctionnalité de langage « traits » (https://en.wikipedia.org/wiki/Trait_(computer_programming)). Les caractéristiques ont prouvé qu’il s’agit d’une technique de programmation puissante (http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf).
Conception détaillée
La syntaxe d’une interface est étendue pour autoriser
- déclarations de membres qui déclarent des constantes, des opérateurs, des constructeurs statiques et des types imbriqués ;
- un corps pour une méthode ou un indexeur, une propriété ou un accesseur d’événement (autrement dit, une implémentation « par défaut ») ;
- déclarations membres qui déclarent des champs statiques, des méthodes, des propriétés, des indexeurs et des événements ;
- déclarations de membres utilisant la syntaxe d’implémentation d’interface explicite ; et
- Modificateurs d’accès explicites (l’accès par défaut est
public
).
Les membres dotés de corps permettent à l’interface de fournir une implémentation « par défaut » pour la méthode dans les classes et les structs qui ne fournissent pas leur propre implémentation.
Les interfaces peuvent ne pas contenir d’état d’instance. Bien que les champs statiques soient désormais autorisés, les champs d’instance ne sont pas autorisés dans les interfaces. Les propriétés automatiques d’instance ne sont pas prises en charge dans les interfaces, car elles déclarent implicitement un champ masqué.
Les méthodes statiques et privées permettent une refactorisation et une organisation de code utiles utilisées pour implémenter l’API publique de l’interface.
Une substitution de méthode dans une interface doit utiliser la syntaxe d’implémentation d’interface explicite.
C'est une erreur de déclarer un type de classe, un type de struct ou un type d’énumération dans le domaine d’un paramètre de type déclaré avec une annotation de variance . Par exemple, la déclaration de C
ci-dessous est une erreur.
interface IOuter<out T>
{
class C { } // error: class declaration within the scope of variant type parameter 'T'
}
Méthodes concrètes dans les interfaces
La forme la plus simple de cette fonctionnalité est la possibilité de déclarer une méthode concrète dans une interface, qui est une méthode avec un corps.
interface IA
{
void M() { WriteLine("IA.M"); }
}
Une classe qui implémente cette interface n’a pas besoin d’implémenter sa méthode concrète.
class C : IA { } // OK
IA i = new C();
i.M(); // prints "IA.M"
Le remplacement final de IA.M
dans la classe C
est la méthode concrète M
déclarée dans IA
. Notez qu’une classe n’hérite pas des membres de ses interfaces ; qui n’est pas modifié par cette fonctionnalité :
new C().M(); // error: class 'C' does not contain a member 'M'
Dans un membre d’instance d’une interface, this
a le type de l’interface englobante.
Modificateurs dans les interfaces
La syntaxe d’une interface est assouplie pour permettre l’utilisation des modificateurs sur ses membres. Les éléments suivants sont autorisés : private
, protected
, internal
, public
, virtual
, abstract
, sealed
, static
, extern
et partial
.
Membre d’interface dont la déclaration inclut un corps est un membre virtual
, sauf si le modificateur sealed
ou private
est utilisé. Le modificateur virtual
peut être utilisé sur une fonction membre qui serait autrement implicitement virtual
. De même, bien que abstract
soit la valeur par défaut sur les membres de l’interface sans corps, ce modificateur peut être donné explicitement. Un membre non virtuel peut être déclaré à l’aide du mot clé sealed
.
Il s’agit d’une erreur pour une fonction membre private
ou sealed
d’une interface de ne pas avoir de corps. Un membre de fonction private
n’a peut-être pas le modificateur sealed
.
Les modificateurs d’accès peuvent être utilisés sur les membres d’interface de tous les types de membres autorisés. Le niveau d’accès public
est la valeur par défaut, mais il peut être donné explicitement.
Problème ouvert : Nous devons spécifier la signification précise des modificateurs d’accès tels que
protected
etinternal
, et quelles déclarations ne les remplacent pas (dans une interface dérivée) ou les implémentent (dans une classe qui implémente l’interface).
Les interfaces peuvent déclarer static
membres, notamment les types imbriqués, les méthodes, les indexeurs, les propriétés, les événements et les constructeurs statiques. Le niveau d’accès par défaut pour tous les membres de l’interface est public
.
Les interfaces ne peuvent pas déclarer de constructeurs d’instances, de destructeurs ou de champs.
Problème fermé : Les déclarations d’opérateur doivent-elles être autorisées dans une interface ? Probablement pas les opérateurs de conversion, mais qu’en est-il des autres ? Décision : les opérateurs sont autorisés sauf pour les opérateurs de conversion, d’égalité et d’inégalité.
Problème fermé : est-ce que
new
doit être autorisé sur les déclarations de membres d’interface qui masquent des membres des interfaces de base ? Décision : Oui.
Problème fermé : Nous n'autorisons actuellement pas
partial
sur une interface ou ses membres. Cela nécessiterait une proposition distincte. Décision : Oui. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#permit-partial-in-interface
Implémentation explicite dans les interfaces
Les implémentations explicites permettent au programmeur de fournir une implémentation la plus spécifique d’un membre virtuel dans une interface où le compilateur ou le runtime ne le trouverait pas autrement. Une déclaration d’implémentation est autorisée à implémenter explicitement une méthode d’interface de base particulière en qualifiant la déclaration avec le nom de l’interface (aucun modificateur d’accès n’est autorisé dans ce cas). Les implémentations implicites ne sont pas autorisées.
interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
void IA.M() { WriteLine("IB.M"); } // Explicit implementation
}
interface IC : IA
{
void M() { WriteLine("IC.M"); } // Creates a new M, unrelated to `IA.M`. Warning
}
Les implémentations explicites dans les interfaces peuvent ne pas être déclarées sealed
.
Les membres de fonction publics virtual
d'une interface ne peuvent être implémentés dans une interface dérivée que de manière explicite (en qualifiant le nom dans la déclaration avec le type d'interface qui a initialement déclaré la méthode, tout en omettant un modificateur d'accès). Le membre doit être accessible là où il est implémenté.
Réabstraction
Une méthode virtuelle (concrète) déclarée dans une interface peut être réabstrérée dans une interface dérivée
interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
abstract void IA.M();
}
class C : IB { } // error: class 'C' does not implement 'IA.M'.
Le modificateur abstract
est requis dans la déclaration de IB.M
, pour indiquer que IA.M
est réabstré.
Cela est utile dans les interfaces dérivées où l’implémentation par défaut d’une méthode est inappropriée et qu’une implémentation plus appropriée doit être fournie par l’implémentation de classes.
Règle d’implémentation la plus spécifique
Nous avons besoin que chaque interface et chaque classe disposent d’une implémentation la plus spécifique pour chaque membre virtuel parmi les implémentations apparaissant dans le type ou ses interfaces directes et indirectes. La implémentation la plus spécifique est une implémentation unique plus spécifique que toutes les autres implémentations. S’il n’existe aucune implémentation, le membre lui-même est considéré comme l’implémentation la plus spécifique.
Une implémentation M1
est considérée comme plus spécifique qu'une autre implémentation M2
si M1
est déclarée sur le type T1
, M2
est déclarée sur le type T2
et soit
T1
contientT2
parmi ses interfaces directes ou indirectes, ouT2
est un type d’interface, maisT1
n’est pas un type d’interface.
Par exemple:
interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
void IA.M() { WriteLine("IB.M"); }
}
interface IC : IA
{
void IA.M() { WriteLine("IC.M"); }
}
interface ID : IB, IC { } // compiles, but error when a class implements 'ID'
abstract class C : IB, IC { } // error: no most specific implementation for 'IA.M'
abstract class D : IA, IB, IC // ok
{
public abstract void M();
}
public class E : ID { } // Error. No most specific implementation for 'IA.M'
La règle d’implémentation la plus spécifique garantit qu’un conflit (c’est-à-dire une ambiguïté résultant de l’héritage du diamant) est résolu explicitement par le programmeur au moment où le conflit se produit.
Étant donné que nous prenons en charge les réabstractions explicites dans les interfaces, nous pourrions également le faire dans les classes.
abstract class E : IA, IB, IC // ok
{
abstract void IA.M();
}
problème fermé: devons-nous prendre en charge les implémentations abstraites d’interface explicites dans les classes ? Décision : NON
En outre, il s’agit d’une erreur si, dans une déclaration de classe, l’implémentation la plus spécifique de certaines méthodes d’interface est une implémentation abstraite déclarée dans une interface. Il s’agit d’une règle existante reposant sur la nouvelle terminologie.
interface IF
{
void M();
}
abstract class F : IF { } // error: 'F' does not implement 'IF.M'
Il est possible qu’une propriété virtuelle déclarée dans une interface ait une implémentation la plus spécifique pour son accesseur get
dans une interface et une implémentation la plus spécifique pour son accesseur set
dans une autre interface. Cela est considéré comme une violation de la règle d’implémentation la plus spécifique et génère une erreur du compilateur.
méthodes static
et private
Étant donné que les interfaces peuvent maintenant contenir du code exécutable, il est utile d’extraire du code commun en méthodes privées et statiques. Nous les autoriseons maintenant dans les interfaces.
problème fermé: devons-nous prendre en charge les méthodes privées ? Devons-nous prendre en charge les méthodes statiques ? décision : OUI
Problème ouvert: devons-nous permettre aux méthodes d’interface d’être
protected
,internal
ou d'avoir un autre type d'accès ? Si c’est le cas, quelles sont les sémantiques ? Sont-ilsvirtual
par défaut ? Si c’est le cas, y a-t-il un moyen de les rendre non virtuels ?
problème fermé: si nous prenons en charge des méthodes statiques, devons-nous prendre en charge les opérateurs (statiques) ? décision : OUI
Appels d’interface de base
La syntaxe de cette section n’a pas été implémentée. Il reste une proposition active.
Le code d’un type qui dérive d’une interface avec une méthode par défaut peut appeler explicitement l’implémentation « base » de cette interface.
interface I0
{
void M() { Console.WriteLine("I0"); }
}
interface I1 : I0
{
override void M() { Console.WriteLine("I1"); }
}
interface I2 : I0
{
override void M() { Console.WriteLine("I2"); }
}
interface I3 : I1, I2
{
// an explicit override that invoke's a base interface's default method
void I0.M() { I2.base.M(); }
}
Une méthode d’instance (non statique) est autorisée à appeler l’implémentation d’une méthode d’instance accessible dans une interface de base directe de manière non virtuelle en l’nommant à l’aide de la syntaxe base(Type).M
. Cela s’avère utile lorsqu’une substitution qu’il faut fournir en raison de l’héritage en diamant est résolue en déléguant à une implémentation de base particulière.
interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
override void IA.M() { WriteLine("IB.M"); }
}
interface IC : IA
{
override void IA.M() { WriteLine("IC.M"); }
}
class D : IA, IB, IC
{
void IA.M() { base(IB).M(); }
}
Lorsqu’un membre virtual
ou abstract
est accessible à l’aide de la syntaxe base(Type).M
, il faut que Type
contienne un remplacement le plus spécifique unique pour M
.
Clauses de base obligatoires
Les interfaces contiennent désormais des types. Ces types peuvent être utilisés dans la clause de base en tant qu’interfaces de base. Lorsqu’on lie une clause de base, il peut s’avérer nécessaire de connaître l’ensemble des interfaces de base pour lier ces types (par exemple, pour les rechercher et pour résoudre l’accès protégé). La signification de la clause de base d’une interface est donc définie de manière circulaire. Pour rompre le cycle, nous ajoutons une nouvelle règle de langage correspondant à une règle similaire déjà en place pour les classes.
Lors de la détermination de la signification de la interface_base d’une interface, les interfaces de base sont temporairement supposées être vides. Cela garantit intuitivement que la signification d’une clause de base ne peut pas dépendre de manière récursive.
Nous avons utilisé les règles suivantes :
« Lorsqu’une classe B dérive d’une classe A, il s’agit d’une erreur au moment de la compilation pour A de dépendre de B. Une classe dépend directement de sa classe de base directe (le cas échéant) et dépend directement de la classe dans laquelle elle est immédiatement imbriquée (le cas échéant). Compte tenu de cette définition, l’ensemble des classes dont une classe dépend est la fermeture réflexive et transitive de la relation dépend directement de. »
Il s'agit d'une erreur de compilation lorsqu'une interface hérite directement ou indirectement d'elle-même. Les interfaces de base d’une interface sont les interfaces de base explicites et leurs interfaces de base. En d’autres termes, l’ensemble des interfaces de base est la fermeture transitive complète des interfaces de base explicites, leurs interfaces de base explicites, et ainsi de suite.
Nous les ajustons comme suit :
Lorsqu’une classe B dérive d’une classe A, il s’agit d’une erreur au moment de la compilation pour A de dépendre de B. Une classe dépend directement de sa classe de base directe (le cas échéant) et dépend directement de le type dans lequel il est immédiatement imbriqué (le cas échéant).
Lorsqu'une interface IB étend une interface IA, c'est une erreur de compilation si IA dépend de IB. Une interface dépend directement de ses interfaces de base directes (le cas échéant) et dépend directement de le type dans lequel elle est immédiatement imbriquée (le cas échéant).
Compte tenu de ces définitions, l’ensemble complet de types dont dépend un type est la fermeture réflexive et transitive de la relation dépend directement de.
Effet sur les programmes existants
Les règles présentées ici sont destinées à ne pas avoir d’effet sur la signification des programmes existants.
Exemple 1 :
interface IA
{
void M();
}
class C: IA // Error: IA.M has no concrete most specific override in C
{
public static void M() { } // method unrelated to 'IA.M' because static
}
Exemple 2 :
interface IA
{
void M();
}
class Base: IA
{
void IA.M() { }
}
class Derived: Base, IA // OK, all interface members have a concrete most specific override
{
private void M() { } // method unrelated to 'IA.M' because private
}
Les mêmes règles donnent des résultats similaires à la situation analogue impliquant des méthodes d’interface par défaut :
interface IA
{
void M() { }
}
class Derived: IA // OK, all interface members have a concrete most specific override
{
private void M() { } // method unrelated to 'IA.M' because private
}
problème fermé: vérifiez qu’il s’agit d’une conséquence prévue de la spécification. décision : OUI
Résolution de méthode Runtime
Problème fermé : La spécification doit décrire l’algorithme de résolution de méthode runtime face aux méthodes par défaut de l’interface. Nous devons nous assurer que la sémantique est cohérente avec la sémantique du langage, par exemple, quelles méthodes déclarées remplacent ou n’implémentent pas une méthode
internal
.
API de prise en charge du CLR
Pour que les compilateurs détectent lorsqu’ils compilent pour un runtime qui prend en charge cette fonctionnalité, les bibliothèques de ces runtimes sont modifiées pour publier ce fait via l’API décrite dans https://github.com/dotnet/corefx/issues/17116. Nous ajoutons
namespace System.Runtime.CompilerServices
{
public static class RuntimeFeature
{
// Presence of the field indicates runtime support
public const string DefaultInterfaceImplementation = nameof(DefaultInterfaceImplementation);
}
}
Ouvrir le problème: est-ce le meilleur nom pour la fonctionnalité CLR ? La fonctionnalité CLR fait beaucoup plus que cela (par exemple, assouplit les contraintes de protection, prend en charge les remplacements dans les interfaces, etc.). Peut-être devrait-il être appelé quelque chose comme « méthodes concrètes dans les interfaces », ou « traits » ?
Autres zones à spécifier
- [ ] Il serait utile de cataloguer les types d’effets de compatibilité source et binaire provoqués par l’ajout de méthodes d’interface par défaut et de remplacements à des interfaces existantes.
Inconvénients
Cette proposition nécessite une mise à jour coordonnée de la spécification CLR (pour prendre en charge des méthodes concrètes dans les interfaces et la résolution des méthodes). Elle est donc assez « coûteuse » et il peut être intéressant de la combiner avec d’autres fonctionnalités qui, selon nos prévisions, nécessiteraient également des modifications du CLR.
Alternatives
Aucun.
Questions non résolues
- Les questions ouvertes sont appelées tout au long de la proposition ci-dessus.
- Consultez également https://github.com/dotnet/csharplang/issues/406 pour obtenir la liste des questions ouvertes.
- La spécification détaillée doit décrire le mécanisme de résolution utilisé au moment de l’exécution pour sélectionner la méthode précise à appeler.
- L’interaction des métadonnées produites par de nouveaux compilateurs et consommées par des compilateurs plus anciens doit être détaillée. Par exemple, nous devons nous assurer que la représentation des métadonnées que nous utilisons n’entraîne pas l’ajout d’une implémentation par défaut dans une interface pour rompre une classe existante qui implémente cette interface lorsqu’elle est compilée par un compilateur plus ancien. Cela peut affecter la représentation des métadonnées que nous pouvons utiliser.
- La conception doit prendre en compte l’interopérabilité avec d’autres langages et les compilateurs existants pour d’autres langages.
Questions résolues
Remplacement abstrait
La spécification préliminaire précédente contenait la possibilité de réabstraire une méthode héritée :
interface IA
{
void M();
}
interface IB : IA
{
override void M() { }
}
interface IC : IB
{
override void M(); // make it abstract again
}
Mes notes pour 2017-03-20 ont montré que nous avons décidé de ne pas autoriser cela. Toutefois, il existe au moins deux cas d’usage pour celui-ci :
- Les API Java, avec lesquelles certains utilisateurs de cette fonctionnalité espèrent interagir, dépendent de cette installation.
- La programmation avec des traits en bénéficie. La réabstraction est l’un des éléments de la fonction de langage « traits » (https://en.wikipedia.org/wiki/Trait_(computer_programming)). Ce qui suit est autorisé avec les classes :
public abstract class Base
{
public abstract void M();
}
public abstract class A : Base
{
public override void M() { }
}
public abstract class B : A
{
public override abstract void M(); // reabstract Base.M
}
Malheureusement, ce code ne peut pas être refactorisé en tant qu’ensemble d’interfaces (caractéristiques), sauf si cela est autorisé. Par le principe Jared de l'avidité, cela devrait être autorisé.
Problème fermé : faut-il autoriser la réabstraction ? [OUI] Mes notes étaient erronées. Les notes de la LDM indiquent que la réabstraction est autorisée dans une interface. Pas dans une classe.
Modificateur virtuel vs Modificateur scellé
De la part de Aleksey Tsingauz:
Nous avons décidé d’autoriser les modificateurs explicitement déclarés sur les membres d’interface, sauf s’il existe une raison de les interdire. Cela apporte une question intéressante autour du modificateur virtuel. Doit-il être requis sur les membres avec l’implémentation par défaut ?
On pourrait dire que :
- s’il n’y a pas d’implémentation et que ni virtuel ni scellé ne sont spécifiés, nous partons du principe que le membre est abstrait.
- S’il existe une implémentation et que ni "abstrait" ni "scellé" n'est spécifié, nous partons du principe que le membre est virtuel.
- le modificateur scellé est requis pour qu'une méthode ne soit ni virtuelle, ni abstraite.
Nous pourrions également dire que le modificateur virtuel est requis pour un membre virtuel. Par exemple, s’il existe un membre avec une implémentation qui n’est pas explicitement marquée avec un modificateur virtuel, il n’est ni virtuel, ni abstrait. Cette approche peut offrir une meilleure expérience lorsqu’une méthode est déplacée d’une classe vers une interface :
- une méthode abstraite reste abstraite.
- une méthode virtuelle reste virtuelle.
- une méthode sans modificateur ne reste ni virtuelle, ni abstraite.
- le modificateur scellé ne peut pas être appliqué à une méthode qui n’est pas un remplacement.
Qu’en penses-tu?
problème fermé : une méthode concrète (avec implémentation) doit-elle être implicitement
virtual
? [OUI]
Décisions : prises dans le LDM 2017-04-05 :
- les valeurs non virtuelles doivent être explicitement exprimées par
sealed
ouprivate
. sealed
est le mot clé pour rendre les membres d’instance d’une interface, dotés de corps, non virtuels.- Nous voulons autoriser tous les modificateurs dans les interfaces
- L’accessibilité par défaut pour les membres de l’interface est publique, y compris les types imbriqués
- les membres de fonction privés dans les interfaces sont implicitement scellés et
sealed
n’est pas autorisé sur eux. - Les classes privées (dans les interfaces) sont autorisées et peuvent être scellées, ce qui signifie scellé dans le sens de classe.
- En l’absence d’une bonne proposition, le caractère partiel n’est toujours pas autorisé sur les interfaces ou leurs membres.
Compatibilité binaire 1
Lorsqu’une bibliothèque fournit une implémentation par défaut
interface I1
{
void M() { Impl1 }
}
interface I2 : I1
{
}
class C : I2
{
}
Nous comprenons que l’implémentation de I1.M
dans C
est I1.M
. Que se passe-t-il si l’assembly contenant I2
est modifié comme suit et recompilé
interface I2 : I1
{
override void M() { Impl2 }
}
mais C
n’est pas recompilée. Que se passe-t-il quand le programme est exécuté ? Invocation de (C as I1).M()
- Exécute
I1.M
- Exécute
I2.M
- Génère une sorte d’erreur d’exécution
Décision : Effectuée 11/04/2017 : Exécute I2.M
, qui est le remplacement le plus spécifique sans ambiguïté lors de l’exécution.
Accesseurs d’événements (fermés)
Problème fermé : Un événement peut-il être substitué « par morceaux » ?
Tenez compte de ce cas :
public interface I1
{
event T e1;
}
public interface I2 : I1
{
override event T
{
add { }
// error: "remove" accessor missing
}
}
Cette implémentation « partielle » de l’événement n’est pas autorisée, car, comme dans une classe, la syntaxe d’une déclaration d’événement n’autorise pas qu’un seul accesseur ; les deux (ou aucun) doivent être fournis. Vous pouvez accomplir la même chose en autorisant l’accesseur de suppression abstraite dans la syntaxe à être implicitement abstrait par l’absence d’un corps :
public interface I1
{
event T e1;
}
public interface I2 : I1
{
override event T
{
add { }
remove; // implicitly abstract
}
}
Notez que il s’agit d’une nouvelle syntaxe (proposée). Dans la grammaire actuelle, les accesseurs d’événements ont obligatoirement un corps.
Problème fermé : un accesseur d’événement peut-il être (implicitement) abstrait par l’omission d’un corps, de la même façon qu’une méthode dans une interface et un accesseur de propriété sont (implicitement) abstraits par l’omission d’un corps ?
Décision : (18/04/2017) non, les déclarations d’événements nécessitent les deux accesseurs concrets (ou aucun).
Reabstraction dans une classe (fermée)
Problème fermé : Il faut confirmer que cela est autorisé (sinon l’ajout d’une implémentation par défaut serait une modification entraînant une rupture) :
interface I1
{
void M() { }
}
abstract class C : I1
{
public abstract void M(); // implement I1.M with an abstract method in C
}
Décision : (18/04/2017) oui, l’ajout d’un corps à une déclaration de membre d’interface ne doit pas rompre C.
Dérogation scellée (fermée)
La question précédente suppose implicitement que le modificateur sealed
peut être appliqué à une override
dans une interface. Cela contredit le projet de spécification. Voulons-nous permettre de sceller une dérogation ? Les effets de compatibilité source et binaire du scellement doivent être pris en compte.
Problème fermé : devrions-nous autoriser le scellement d’une substitution ?
Décision : (18/04/2017) ne pas autoriser sealed
sur les remplacements dans les interfaces. La seule utilisation de sealed
sur les membres de l'interface consiste à les rendre non-virtuels dans leur déclaration initiale.
Héritage et classes en diamant (fermé)
L'ébauche de la proposition préfère les surcharges de classes aux surcharges d’interfaces dans les scénarios d'héritage en diamant :
Nous avons besoin que chaque interface et chaque classe aient un remplacement plus spécifique pour chaque méthode d’interface parmi les remplacements apparaissant dans le type ou ses interfaces directes et indirectes. Le remplacement le plus spécifique est un remplacement unique qui est plus spécifique que tous les autres remplacements. S’il n’existe aucun remplacement, la méthode elle-même est considérée comme le remplacement le plus spécifique.
Un remplacement
M1
est considéré comme plus spécifique qu’un autreM2
siM1
est déclaré sur le typeT1
,M2
est déclaré sur le typeT2
, et si
T1
contientT2
parmi ses interfaces directes ou indirectes, ouT2
est un type d’interface, maisT1
n’est pas un type d’interface.
Le scénario est celui-ci
interface IA
{
void M();
}
interface IB : IA
{
override void M() { WriteLine("IB"); }
}
class Base : IA
{
void IA.M() { WriteLine("Base"); }
}
class Derived : Base, IB // allowed?
{
static void Main()
{
IA a = new Derived();
a.M(); // what does it do?
}
}
Nous devons confirmer ce comportement (ou décider autrement)
Problème fermé : confirmer le projet de spécification ci-dessus pour le remplacement le plus spécifique telle qu’il s’applique aux classes et interfaces mixtes (une classe prend la priorité sur une interface). Voir https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#diamonds-with-classes.
Méthodes d’interface vs structures (fermé)
Il existe des interactions malheureuses entre les méthodes d’interface par défaut et les structs.
interface IA
{
public void M() { }
}
struct S : IA
{
}
Notez que les membres d’interface ne sont pas hérités :
var s = default(S);
s.M(); // error: 'S' does not contain a member 'M'
Par conséquent, le client doit boxer la structure pour appeler des méthodes d’interface
IA s = default(S); // an S, boxed
s.M(); // ok
Le boxing réalisé de cette manière élimine les principaux avantages d’un type struct
. De plus, toutes les méthodes de mutation n’auront aucun effet apparent, car elles fonctionnent sur une copie boxée de la structure :
interface IB
{
public void Increment() { P += 1; }
public int P { get; set; }
}
struct T : IB
{
public int P { get; set; } // auto-property
}
T t = default(T);
Console.WriteLine(t.P); // prints 0
(t as IB).Increment();
Console.WriteLine(t.P); // prints 0
problème fermé : Que pouvons-nous faire à ce sujet :
- Interdire à un
struct
d’hériter d’une implémentation par défaut. Toutes les méthodes d’interface sont traitées comme abstraites dans unstruct
. Ensuite, nous pouvons prendre du temps plus tard pour décider comment le rendre plus efficace.- Trouver un type de stratégie de génération de code qui évite le boxing À l’intérieur d’une méthode comme
IB.Increment
, le type dethis
serait peut-être semblable à un paramètre de type limité àIB
. Dans ce contexte, pour éviter de limiter l’appelant, les méthodes non abstraites seraient héritées des interfaces. Cela peut augmenter sensiblement le travail d’implémentation du compilateur et du CLR.- Ne pas s’en préoccuper et la laisser comme une verrue.
- D’autres idées ?
Décision : Ne vous inquiétez pas et laissez-le comme une verrue. Voir https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#structs-and-default-implementations.
Appels d’interface de base (fermés)
Cette décision n’a pas été mise en œuvre en C# 8. La syntaxe base(Interface).M()
n’est pas implémentée.
Le brouillon suggère une syntaxe pour les appels d’interface de base inspirés par Java : Interface.base.M()
. Nous devons sélectionner une syntaxe, au moins pour le prototype initial. Mon favori est base<Interface>.M()
.
Problème clos : Quelle est la syntaxe pour invoquer un membre de base ?
Décision : La syntaxe est base(Interface).M()
. Voir https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#base-invocation. L’interface ainsi nommée doit être une interface de base, mais n’a pas besoin d’être une interface de base directe.
Problème ouvert : les appels d’interface de base doivent-ils être autorisés dans les membres de classe ?
Décision : Oui. https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#base-invocation
Remplacement de membres d’interface non publics (fermés)
Dans une interface, les membres non publics des interfaces de base sont substitués à l’aide du modificateur override
. S’il s’agit d’un remplacement « explicite » qui nomme l’interface contenant le membre, le modificateur d’accès est omis.
Problème fermé : s’il s’agit d’un remplacement « implicite » qui ne nomme pas l’interface, le modificateur d’accès doit-il correspondre ?
Décision : seuls les membres publics peuvent être implicitement remplacés, et l’accès doit correspondre. Voir https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-18.md#dim-implementing-a-non-public-interface-member-not-in-list.
Problème ouvert : le modificateur d’accès est-il obligatoire, facultatif ou omis sur un remplacement explicite tel que
override void IB.M() {}
?
Problème ouvert :
override
est-il obligatoire, facultatif ou omis lors d’un remplacement explicite, par exemplevoid IB.M() {}
?
Comment implémenter un membre d’interface non public dans une classe ? Peut-être faut-il le faire explicitement ?
interface IA
{
internal void MI();
protected void MP();
}
class C : IA
{
// are these implementations? Decision: NO
internal void MI() {}
protected void MP() {}
}
problème fermé : Comment implémenter un membre d’interface non publique dans une classe ?
Décision : Vous ne pouvez implémenter que des membres d’interface non publics explicitement. Voir https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-18.md#dim-implementing-a-non-public-interface-member-not-in-list.
Décision : non, le mot-clé override
n’est pas autorisé sur les membres d’interface. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#does-an-override-in-an-interface-introduce-a-new-member
Compatibilité binaire 2 (fermée)
Considérez le code suivant dans lequel chaque type se trouve dans un assembly distinct
interface I1
{
void M() { Impl1 }
}
interface I2 : I1
{
override void M() { Impl2 }
}
interface I3 : I1
{
}
class C : I2, I3
{
}
Nous comprenons que l’implémentation de I1.M
dans C
est I2.M
. Que se passe-t-il si l’assembly contenant I3
est modifié comme suit et recompilé
interface I3 : I1
{
override void M() { Impl3 }
}
mais C
n’est pas recompilée. Que se passe-t-il quand le programme est exécuté ? Invocation de (C as I1).M()
- Exécute
I1.M
- Exécute
I2.M
- Exécute
I3.M
- 2 ou 3, déterministement
- Génère un type d’exception d’exécution
Décision : générer une exception (5). Voir https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#issues-in-default-interface-methods.
Autoriser partial
dans l’interface ? (fermé)
Étant donné que les interfaces peuvent être utilisées de manière analogue à la façon dont les classes abstraites sont utilisées, il peut être utile de les déclarer partial
. Cela serait particulièrement utile face aux générateurs.
Proposition : Supprimer la restriction linguistique selon laquelle les interfaces et les membres des interfaces ne peuvent pas être déclarés
partial
.
Décision : Oui. Voir https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#permit-partial-in-interface.
Main
dans une interface ? (fermé)
Problème ouvert : Une méthode
static Main
dans une interface peut-elle être candidate pour être le point d’entrée du programme ?
Décision : Oui. Voir https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#main-in-an-interface.
Confirmer l’intention de soutenir les méthodes publiques non virtuelles (fermées)
Pouvons-nous confirmer (ou inverser) notre décision d’autoriser les méthodes publiques non virtuelles dans une interface ?
interface IA
{
public sealed void M() { }
}
Semi-Closed Problème : (2017-04-18) Nous pensons qu’il va être utile, mais nous y reviendrons. Il s’agit d’un obstacle au modèle mental.
Décision : Oui. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#confirm-that-we-support-public-non-virtual-methods.
Un override
dans une interface introduit-t-il un nouveau membre ? (fermé)
Il existe plusieurs façons d’observer si une déclaration de remplacement introduit un nouveau membre ou non.
interface IA
{
void M(int x) { }
}
interface IB : IA
{
override void M(int y) { } // 'override' not permitted
}
interface IC : IB
{
static void M2()
{
M(y: 3); // permitted? Decision: No.
}
override void IB.M(int z) { } // permitted? What does it override? Decision: No.
}
Problème ouvert : Une déclaration de remplacement dans une interface introduit-elle un nouveau membre ? (fermé)
Dans une classe, une méthode de remplacement est « visible » dans certains sens. Par exemple, les noms de ses paramètres sont prioritaires sur les noms des paramètres dans la méthode substituée. Il est peut-être possible de dupliquer ce comportement dans les interfaces, car il existe toujours l'override le plus spécifique. Mais voulons-nous dupliquer ce comportement ?
En outre, il est possible de « remplacer » une méthode de remplacement ? [Moot]
Décision: aucun mot-clé override
n'est autorisé sur les éléments d'interface. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#does-an-override-in-an-interface-introduce-a-new-member.
Propriétés avec un accesseur privé (fermé)
Nous disons que les membres privés ne sont pas virtuels et que la combinaison de membres virtuels et privés n’est pas autorisée. Mais qu’en est-il d’une propriété avec un accesseur privé ?
interface IA
{
public virtual int P
{
get => 3;
private set { }
}
}
Est-ce autorisé ? L’accesseur set
est-il ici virtual
ou non ? Peut-il être substitué là où il est accessible ? Le code suivant implémente-t-il implicitement uniquement l’accesseur get
?
class C : IA
{
public int P
{
get => 4;
set { }
}
}
C'est probablement une erreur, car IA.P.set n'est pas virtuel et parce qu'il n'est pas accessible non plus ?
class C : IA
{
int IA.P
{
get => 4;
set { } // Decision: Not valid
}
}
décision: le premier exemple semble valide, tandis que le dernier ne le fait pas. Cela est résolu de façon analogue à la façon dont il fonctionne déjà en C#. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#properties-with-a-private-accessor
Appels d’interface de base, tour 2 (fermé)
Cela n’a pas été implémenté en C# 8.
Notre « résolution » précédente sur la façon de gérer les appels de base ne fournit pas réellement suffisamment d’expressivité. Il s’avère que dans C# et le CLR, contrairement à Java, vous devez spécifier à la fois l’interface contenant la déclaration de méthode et l’emplacement de l’implémentation que vous souhaitez appeler.
Je propose la syntaxe suivante pour les appels de base dans les interfaces. Je ne suis pas amoureux de celui-ci, mais il illustre ce que toute syntaxe doit être capable d’exprimer :
interface I1 { void M(); }
interface I2 { void M(); }
interface I3 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I4 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I5 : I3, I4
{
void I1.M()
{
base<I3>(I1).M(); // calls I3's implementation of I1.M
base<I4>(I1).M(); // calls I4's implementation of I1.M
}
void I2.M()
{
base<I3>(I2).M(); // calls I3's implementation of I2.M
base<I4>(I2).M(); // calls I4's implementation of I2.M
}
}
S’il n’y a pas d’ambiguïté, vous pouvez l’écrire plus simplement
interface I1 { void M(); }
interface I3 : I1 { void I1.M() { } }
interface I4 : I1 { void I1.M() { } }
interface I5 : I3, I4
{
void I1.M()
{
base<I3>.M(); // calls I3's implementation of I1.M
base<I4>.M(); // calls I4's implementation of I1.M
}
}
ou
interface I1 { void M(); }
interface I2 { void M(); }
interface I3 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I5 : I3
{
void I1.M()
{
base(I1).M(); // calls I3's implementation of I1.M
}
void I2.M()
{
base(I2).M(); // calls I3's implementation of I2.M
}
}
ou
interface I1 { void M(); }
interface I3 : I1 { void I1.M() { } }
interface I5 : I3
{
void I1.M()
{
base.M(); // calls I3's implementation of I1.M
}
}
Décision : décidé sur base(N.I1<T>).M(s)
, en admettant que si nous avons une invocation contraignante, il peut y avoir un problème ici plus tard. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-11-14.md#default-interface-implementations
Avertissement concernant une struct qui n’implémente pas la méthode par défaut. (fermé)
@vancem affirme que nous devrions sérieusement envisager de générer un avertissement si une déclaration de type valeur ne parvient pas à remplacer une méthode d’interface, même si elle hériterait d’une implémentation de cette méthode à partir d’une interface. Parce que cela provoque le boxing et sape les appels restreints.
Décision : cela semble plus adapté à un analyste. Il semble également que cet avertissement puisse être bruyant, car il se déclenche même si la méthode d’interface par défaut n’est jamais appelée et aucun boxing ne se produit jamais. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#warning-for-struct-not-implementing-default-method
Constructeurs statiques d’interface (fermés)
Quand les constructeurs statiques d’interface sont-ils exécutés ? Le brouillon actuel du CLI suggère que cela se produise lorsque la première méthode ou champ statique est accessible. S’il n’y a ni l’un ni l’autre, il se peut qu’il ne soit jamais exécuté ?
[09/10/2018 L’équipe CLR propose « Nous allons reproduire ce que nous faisons pour les valuetypes (vérification cctor sur l’accès à chaque méthode d’instance) »]
Décision : les constructeurs statiques sont également exécutés à l’entrée des méthodes d’instance, si le constructeur statique n’était pas beforefieldinit
, auquel cas les constructeurs statiques sont exécutés avant l’accès au premier champ statique. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#when-are-interface-static-constructors-run
Concevoir des réunions
Notes de réunion LDM 08/03/2017Notes de réunion LDM 21/03/2017Réunion 23/03/2017 « Comportement CLR pour les méthodes d’interface par défaut »Notes de réunion LDM 05/04/2017Notes de réunion LDM 11/04/2017Notes de réunion LDM 18/04/2017Notes de réunion LDM 19/04/2017Notes de réunion LDM 17/05/2017Notes de réunion LDM 31/05/2017Notes de réunion LDM 14/06/2017Notes de réunion LDM 17/10/2017Notes de réunion LDM 14/11/2018
C# feature specifications