Retours covariants
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 .
Problème phare : https://github.com/dotnet/csharplang/issues/49
Résumé
Prenez en charge les types de retour covariants. Plus précisément, autorisez le remplacement d’une méthode pour déclarer un type de retour plus dérivé que la méthode qu’elle substitue, et pour autoriser de la même manière le remplacement d’une propriété en lecture seule pour déclarer un type plus dérivé. Le remplacement des déclarations apparaissant dans d’autres types dérivés serait requis pour fournir un type de retour au moins aussi spécifique que celui qui apparaît dans les remplacements dans ses types de base. Les appelants de la méthode ou de la propriété recevraient de manière statique le type de retour plus affiné à partir d’un appel.
Motivation
Il s'agit d'un modèle courant dans le code où il faut inventer différents noms de méthode pour contourner la contrainte de langage selon laquelle une surcharge doit retourner le même type que la méthode surchargée.
Cela serait utile dans le modèle d’usine. Par exemple, dans la base de code Roslyn, nous aurions
class Compilation ...
{
public virtual Compilation WithOptions(Options options)...
}
class CSharpCompilation : Compilation
{
public override CSharpCompilation WithOptions(Options options)...
}
Conception détaillée
Il s'agit d'une spécification sur les types de retour covariants en C#. Notre intention est d’autoriser le remplacement d’une méthode pour retourner un type de retour plus dérivé que la méthode qu’elle remplace, et pour autoriser par la même occasion le remplacement d’une propriété en lecture seule pour retourner un type de retour plus dérivé. Les appelants de la méthode ou de la propriété recevraient statiquement le type de retour plus affiné à partir d’un appel, et les remplacements apparaissant dans des types plus dérivés seraient nécessaires pour fournir un type de retour au moins aussi spécifique que celui qui apparaît dans les remplacements dans ses types de base.
Remplacement de méthode de classe
Contrainte existante sur les méthodes de remplacement de classe (§15.6.5)
- La méthode override et la méthode de base substituée ont le même type de retour.
est remplacé par
- La méthode de remplacement doit avoir un type de retour convertible par une conversion d’identité ou (si la méthode a un retour de valeur, pas un retour de référence, voir §13.1.0.5) une conversion de référence implicite vers le type de retour de la méthode de base substituée.
Les exigences supplémentaires suivantes sont ajoutées à cette liste :
- La méthode de remplacement doit avoir un type de retour convertible par une conversion d’identité ou (si la méthode a un retour de valeur, pas un retour de référence, §13.1.0.5) une conversion de référence implicite vers le type de retour de chaque remplacement de la méthode de base substituée déclarée dans un type de base (direct ou indirect) de la méthode de remplacement.
- Le type de retour de la méthode de remplacement doit être au moins aussi accessible que la méthode de remplacement (domaines d’accessibilité - §7.5.3).
Cette contrainte permet à une méthode de remplacement dans une classe private
d’avoir un type de retour private
. Toutefois, il nécessite une méthode de remplacement public
dans un type public
pour avoir un type de retour public
.
Propriété de classe et redéfinition de l'indexeur
Contrainte existante sur les propriétés de remplacement de classe (§15.7.6)
Une déclaration de propriété de substitution doit spécifier exactement les mêmes modificateurs d’accessibilité et nom que la propriété héritée, et il doit y avoir une conversion d’identité
entre le type de substitution et la propriété héritée. Si la propriété héritée n’a qu’un seul accesseur (c’est-à-dire si la propriété héritée est en lecture seule ou en écriture seule), la propriété de substitution doit inclure uniquement cet accesseur. Si la propriété héritée inclut les deux accesseurs (c’est-à-dire si la propriété héritée est en lecture-écriture), la propriété de substitution peut inclure un seul accesseur ou les deux accesseurs.
est remplacé par
Une déclaration de propriété substituée doit spécifier exactement les mêmes modificateurs d’accessibilité et le même nom que la propriété héritée, et il doit y avoir une conversion d’identité ou (si la propriété héritée est en lecture seule et a un retour de valeur, et non un retour de référence, §13.1.0.5) une conversion de référence implicite du type de la propriété substituée vers le type de la propriété héritée. Si la propriété héritée n’a qu’un seul accesseur (c’est-à-dire si la propriété héritée est en lecture seule ou en écriture seule), la propriété de substitution doit inclure uniquement cet accesseur. Si la propriété héritée inclut les deux accesseurs (c’est-à-dire si la propriété héritée est en lecture-écriture), la propriété de substitution peut inclure un seul accesseur ou les deux accesseurs. Le type de propriété substituée doit être au moins aussi accessible que la propriété de substitution (domaines d’accessibilité - §7.5.3).
Le reste de la spécification préliminaire ci-dessous propose une extension supplémentaire aux retours covariants des méthodes d’interface à considérer ultérieurement.
Remplacement de méthode d’interface, de propriété et d’indexeur
En ajoutant les types de membres autorisés dans une interface grâce à la fonctionnalité DIM de C# 8.0, nous ajoutons également la prise en charge des membres override
ainsi que des retours covariants. Ces règles des membres override
suivent celles spécifiées pour les classes, avec les différences suivantes :
Texte suivant dans les classes :
La méthode substituée par une déclaration de remplacement est appelée méthode de base substituée. Pour une méthode de remplacement
M
déclarée dans une classeC
, la méthode de base substituée est déterminée en examinant chaque classe de base deC
, en commençant par la classe de base directe deC
et en continuant avec chaque classe de base directe successive, jusqu’à ce que dans un type de classe de base donné au moins une méthode accessible soit située qui a la même signature queM
après la substitution d’arguments de type.
la spécification correspondante pour les interfaces est fournie :
La méthode substituée par une déclaration de remplacement est appelée méthode de base substituée. Pour une méthode de remplacement
M
déclarée dans une interfaceI
, la méthode de base substituée est déterminée en examinant chaque interface de base directe ou indirecte deI
, en collectant l’ensemble d’interfaces déclarant une méthode accessible qui a la même signature queM
après la substitution d’arguments de type. Si cet ensemble d’interfaces a le type le plus dérivé, pour lequel il existe une conversion d’identité ou de référence implicite à partir de chaque type de cet ensemble et si ce type contient une déclaration de méthode unique, il s’agit alors de la méthode de base remplacée.
De même, nous autorisons les propriétés et les indexeurs override
dans les interfaces, comme spécifié pour les classes dans §15.7.6 Accesseurs virtuels, sealed, de remplacement et abstraits.
Recherche de nom
La recherche de noms en présence de déclarations de classe override
modifie actuellement le résultat de la recherche en imposant au membre trouvé les détails de la déclaration override
la plus dérivée dans la hiérarchie de classes, en partant du type du qualificateur de l’identificateur (ou this
lorsqu’il n’y a aucun qualificateur). Par exemple, dans §12.6.2.2 Paramètres correspondants nous avons
Pour les méthodes virtuelles et les indexeurs définis dans les classes, la liste de paramètres est choisie à partir de la première déclaration ou remplacement du membre de fonction trouvé lors du démarrage avec le type statique du récepteur et la recherche dans ses classes de base.
à cela, nous ajoutons
Pour les méthodes virtuelles et les indexeurs définis dans les interfaces, la liste de paramètres est choisie à partir de la déclaration ou du remplacement du membre de fonction trouvé dans le type le plus dérivé parmi ces types contenant la déclaration de remplacement du membre de fonction. Il s’agit d’une erreur au moment de la compilation si aucun type de ce type n’existe.
Pour le type de résultat de l'accès à une propriété ou à un indexeur, le texte existant
- Si
I
identifie une propriété d’instance, le résultat est un accès aux propriétés avec une expression d’instance associée deE
et un type associé qui est le type de la propriété. SiT
est un type de classe, le type associé est sélectionné à partir de la première déclaration ou redéfinition de la propriété trouvée en commençant parT
et en cherchant dans ses classes de base.
est amélioré par
Si
T
est un type d’interface, le type associé est choisi à partir de la déclaration ou de la surcharge de la propriété trouvée dans l'interface la plus dérivée deT
ou parmi ses interfaces de base directes ou indirectes. Il s’agit d’une erreur au moment de la compilation si aucun type de ce type n’existe.
Une modification similaire doit être apportée dans §12.8.12.3 Accès à l’indexeur
Dans §12.8.10 Expressions d’appel nous augmentons le texte existant
- Sinon, le résultat est une valeur, avec un type associé du type de retour de la méthode ou du délégué. Si l’appel est d’une méthode d’instance et que le récepteur est d’un type de classe
T
, le type associé est sélectionné à partir de la première déclaration ou remplacement de la méthode trouvée lors du démarrage deT
et de la recherche dans ses classes de base.
par
Si l’appel est d’une méthode d’instance et que le récepteur est d’un type d’interface
T
, le type associé est choisi à partir de la déclaration ou du remplacement de la méthode trouvée dans l’interface la plus dérivée entreT
et ses interfaces de base directes et indirectes. Il s’agit d’une erreur au moment de la compilation si aucun type de ce type n’existe.
Implémentations d’interface implicites
Cette section de la spécification
À des fins de mappage d’interface, un membre de classe
A
correspond à un membre d’interfaceB
quand :
A
etB
sont des méthodes, et le nom, le type et les listes de paramètres formels desA
et desB
sont identiques.A
etB
sont des propriétés, le nom et le type deA
etB
sont identiques etA
a les mêmes accesseurs queB
(A
est autorisé à avoir des accesseurs supplémentaires s’il n’est pas une implémentation de membre d’interface explicite).A
etB
sont des événements, et le nom et le type deA
etB
sont identiques.A
etB
sont des indexeurs, le type et les listes de paramètres formels desA
et desB
sont identiques, etA
a les mêmes accesseurs queB
(A
est autorisé à avoir des accesseurs supplémentaires s’il n’est pas une implémentation de membre d’interface explicite).
est modifié comme suit :
À des fins de mappage d’interface, un membre de classe
A
correspond à un membre d’interfaceB
quand :
A
etB
sont des méthodes, et le nom et les listes de paramètres formels deA
et deB
sont identiques, et le type de retour deA
est convertible en type de retour deB
via une identité de conversion de référence implicite en type de retour deB
.A
etB
sont des propriétés, le nom deA
etB
sont identiques,A
a les mêmes accesseurs queB
(A
est autorisé à avoir des accesseurs supplémentaires s’il n’est pas une implémentation de membre d’interface explicite) et que le type deA
est convertible en type de retour deB
via une conversion d’identité ou, siA
est une propriété en lecture seule, une conversion de référence implicite.A
etB
sont des événements, et le nom et le type deA
etB
sont identiques.A
etB
sont des indexeurs, les listes de paramètres formels desA
et desB
sont identiques,A
a les mêmes accesseurs queB
(A
est autorisé à avoir des accesseurs supplémentaires s’il n’est pas une implémentation de membre d’interface explicite) et le type deA
est convertible en type de retour deB
via une conversion d’identité ou, siA
est un indexeur en lecture seule, une conversion de référence implicite.
Il s’agit techniquement d’une rupture, car le programme ci-dessous imprime « C1.M » aujourd’hui, mais imprimerait « C2.M » dans le cadre de la révision proposée.
using System;
interface I1 { object M(); }
class C1 : I1 { public object M() { return "C1.M"; } }
class C2 : C1, I1 { public new string M() { return "C2.M"; } }
class Program
{
static void Main()
{
I1 i = new C2();
Console.WriteLine(i.M());
}
}
En raison de ce changement incompatible, nous pourrions envisager de ne pas prendre en charge les types de retour covariants sur les implémentations implicites.
Contraintes sur l’implémentation de l’interface
Nous aurons besoin d’une règle indiquant qu’une implémentation d’interface explicite doit déclarer un type de retour pas moins dérivé que le type de retour déclaré dans n’importe quel remplacement dans ses interfaces de base.
Implications en matière de compatibilité des API
À déterminer
Problèmes ouverts
La spécification ne dit pas comment l’appelant obtient le type de retour plus affiné. Probablement que cela serait fait d’une manière similaire à la façon dont les appelants obtiennent les spécifications des paramètres de remplacement les plus dérivés.
Si nous avons les interfaces suivantes :
interface I1 { I1 M(); }
interface I2 { I2 M(); }
interface I3: I1, I2 { override I3 M(); }
Notez que dans I3
, les méthodes I1.M()
et les I2.M()
ont été « fusionnées ». Lors de l’implémentation de I3
, il est nécessaire de les implémenter ensemble.
En règle générale, nous avons besoin d’une implémentation explicite pour faire référence à la méthode d’origine. La question porte sur une classe.
class C : I1, I2, I3
{
C IN.M();
}
Qu’est-ce que cela veut dire ici ? Que devrait être N ?
Je suggère que nous permettions l'implémentation de I1.M
ou de I2.M
(mais pas les deux), et de considérer cela comme une implémentation des deux.
Inconvénients
- [ ] Chaque modification linguistique doit se justifier.
- [ ] Nous devons nous assurer que les performances sont raisonnables, même dans le cas de hiérarchies d’héritage profondes
- [ ] Nous devons nous assurer que les artefacts de la stratégie de traduction n’affectent pas la sémantique du langage, même lorsque vous consommez de nouveaux il à partir d’anciens compilateurs.
Alternatives
Nous pourrions assouplir légèrement les règles linguistiques pour permettre, dans la source,
// Possible alternative. This was not implemented.
abstract class Cloneable
{
public abstract Cloneable Clone();
}
class Digit : Cloneable
{
public override Cloneable Clone()
{
return this.Clone();
}
public new Digit Clone() // Error: 'Digit' already defines a member called 'Clone' with the same parameter types
{
return this;
}
}
Questions non résolues
- [ ] Comment les API qui ont été compilées pour utiliser cette fonctionnalité fonctionnent-elles dans les versions antérieures du langage ?
Concevoir des réunions
- quelques discussions sous https://github.com/dotnet/roslyn/issues/357.
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-01-08.md
- Discussion hors connexion en faveur d’une décision de prise en charge du remplacement des méthodes de classe uniquement en C# 9.0.
C# feature specifications