Membres abstraits statiques dans les interfaces
Remarque
Cet article est une spécification de fonctionnalité. La spécification sert de document de conception pour la fonctionnalité. Elle inclut les changements de spécification proposés, ainsi que les informations nécessaires à la conception et au développement de la fonctionnalité. Ces articles sont publiés jusqu'à ce que les changements proposés soient finalisés et incorporés dans la spécification ECMA actuelle.
Il peut y avoir des différences entre la spécification de la fonctionnalité et l'implémentation réalisée. Ces différences sont consignées dans les notes pertinentes de la réunion de conception linguistique (LDM).
Pour en savoir plus sur le processus d'adoption des speclets de fonctionnalité dans la norme du langage C#, consultez l'article sur les spécifications.
Problème de champion : https://github.com/dotnet/csharplang/issues/4436
Récapitulatif
Une interface peut spécifier des membres statiques abstraits, et les classes et structs qui l'implémentent doivent ensuite fournir une implémentation explicite ou implicite de ces membres. Les membres peuvent être accessibles à partir des paramètres de type qui sont contraints par l'interface.
Motivation
Il n’existe actuellement aucun moyen d’abstraction sur les membres statiques ni d’écriture du code généralisé qui s’applique aux types qui définissent ces membres statiques. Cela est particulièrement problématique pour les types de membres qui existent seulement sous une forme statique, notamment les opérateurs.
Cette fonctionnalité permet des algorithmes génériques sur des types numériques, représentés par des contraintes d’interface qui spécifient la présence d’opérateurs donnés. Les algorithmes peuvent donc être exprimés en termes d’opérateurs de ce type :
// Interface specifies static properties and operators
interface IAddable<T> where T : IAddable<T>
{
static abstract T Zero { get; }
static abstract T operator +(T t1, T t2);
}
// Classes and structs (including built-ins) can implement interface
struct Int32 : …, IAddable<Int32>
{
static Int32 IAddable.operator +(Int32 x, Int32 y) => x + y; // Explicit
public static int Zero => 0; // Implicit
}
// Generic algorithms can use static members on T
public static T AddAll<T>(T[] ts) where T : IAddable<T>
{
T result = T.Zero; // Call static operator
foreach (T t in ts) { result += t; } // Use `+`
return result;
}
// Generic method can be applied to built-in and user-defined types
int sixtyThree = AddAll(new [] { 1, 2, 4, 8, 16, 32 });
Syntaxe
Membres d’interfaces
Cette fonctionnalité permettrait de déclarer des membres d’interface statiques comme virtuels.
Règles avant C# 11
Avant C# 11, les membres d’instance dans les interfaces sont implicitement abstraits (ou virtuels s’ils ont une implémentation par défaut), mais peuvent éventuellement avoir un modificateur abstract
(ou virtual
). Les membres d’instance non virtuels doivent être explicitement marqués comme sealed
.
Les membres d’interface statiques sont aujourd’hui implicitement non virtuels et n’autorisent pas les modificateurs abstract
, virtual
ou sealed
.
Proposition
Membres statiques abstraits
Les membres d’interface statiques autres que les champs sont également autorisés à avoir le modificateur abstract
. Les membres statiques abstraits ne sont pas autorisés à avoir un corps (ou dans le cas des propriétés, les accesseurs ne sont pas autorisés à avoir un corps).
interface I<T> where T : I<T>
{
static abstract void M();
static abstract T P { get; set; }
static abstract event Action E;
static abstract T operator +(T l, T r);
static abstract bool operator ==(T l, T r);
static abstract bool operator !=(T l, T r);
static abstract implicit operator T(string s);
static abstract explicit operator string(T t);
}
Membres statiques virtuels
Les membres d’interface statiques autres que les champs sont également autorisés à avoir le modificateur virtual
. Les membres statiques virtuels doivent avoir un corps.
interface I<T> where T : I<T>
{
static virtual void M() {}
static virtual T P { get; set; }
static virtual event Action E;
static virtual T operator +(T l, T r) { throw new NotImplementedException(); }
}
Membres statiques explicitement non virtuels
Pour des raisons de symétrie avec les membres d’instance non virtuels, les membres statiques (à l’exception des champs) devraient être autorisés à avoir un modificateur sealed
facultatif, même s’ils sont non virtuels par défaut :
interface I0
{
static sealed void M() => Console.WriteLine("Default behavior");
static sealed int f = 0;
static sealed int P1 { get; set; }
static sealed int P2 { get => f; set => f = value; }
static sealed event Action E1;
static sealed event Action E2 { add => E1 += value; remove => E1 -= value; }
static sealed I0 operator +(I0 l, I0 r) => l;
}
Implémentation des membres d'interface
Règles actuelles
Les classes et les structs peuvent implémenter des membres d’instance abstraits d’interfaces implicitement ou explicitement. Un membre d'interface implémenté implicitement est une déclaration de membre normale (virtuelle ou non virtuelle) de la classe ou de la structure qui « se trouve » également à implémenter le membre de l'interface. Le membre peut même être hérité d’une classe de base et donc ne pas être présent dans la déclaration de classe.
Un membre d’interface explicitement implémenté utilise un nom qualifié pour identifier le membre d’interface en question. L’implémentation n’est pas directement accessible en tant que membre sur la classe ou le struct, mais uniquement via l’interface.
Proposition
Aucune nouvelle syntaxe n’est nécessaire dans les classes et les structs pour faciliter l’implémentation implicite des membres d’interface abstraits statiques. Les déclarations de membres statiques existantes sont utilisées à cet effet.
Les implémentations explicites des membres d’interface abstraits statiques utilisent un nom qualifié avec le modificateur static
.
class C : I<C>
{
string _s;
public C(string s) => _s = s;
static void I<C>.M() => Console.WriteLine("Implementation");
static C I<C>.P { get; set; }
static event Action I<C>.E // event declaration must use field accessor syntax
{
add { ... }
remove { ... }
}
static C I<C>.operator +(C l, C r) => new C($"{l._s} {r._s}");
static bool I<C>.operator ==(C l, C r) => l._s == r._s;
static bool I<C>.operator !=(C l, C r) => l._s != r._s;
static implicit I<C>.operator C(string s) => new C(s);
static explicit I<C>.operator string(C c) => c._s;
}
Sémantique
Restrictions d’opérateur
Aujourd’hui, toutes les déclarations d’opérateurs unaires et binaires doivent avoir au moins l’un de leurs opérandes du type T
ou T?
, où T
est le type d’instance du type englobant.
Ces exigences doivent être assouplies de sorte qu'un opérande restreint soit autorisé à être d'un paramètre de type qui compte comme « le type d'instance du type englobant ».
Pour qu’un paramètre de type T
compte comme le « type d’instance du type englobant », il doit répondre aux exigences suivantes :
-
T
est un paramètre de type direct sur l’interface dans laquelle la déclaration d’opérateur se produit et -
T
est directement contraint par ce que la spécification appelle « le type d'instance » : c'est-à-dire l'interface environnante avec ses propres paramètres de type utilisés comme arguments de type.
Opérateurs d'égalité et conversions
Les déclarations abstraites/virtuelles des opérateurs ==
et !=
, ainsi que les déclarations abstraites/virtuelles des opérateurs de conversion implicites et explicites sont autorisées dans les interfaces. Les interfaces dérivées seront également autorisées à les implémenter.
Pour les opérateurs ==
et !=
, au moins un type de paramètre doit être un paramètre de type qui compte comme le « type d’instance du type englobant », tel que défini dans la section précédente.
Implémentation de membres abstraits statiques
Les règles selon lesquelles une déclaration de membre statique dans une classe ou un struct est considérée comme implémentant un membre d’interface abstrait statique et les exigences qui s’appliquent lorsqu’elle le fait, sont identiques à celles des membres d’instance.
À déterminer : des règles supplémentaires ou différentes auxquelles nous n’avons pas encore pensé peuvent être nécessaires ici.
Interfaces comme arguments de type
Nous avons abordé le problème soulevé par https://github.com/dotnet/csharplang/issues/5955 et avons décidé d’ajouter une restriction autour de l’utilisation d’une interface comme argument de type (https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md#type-hole-in-static-abstracts). Voici la restriction telle qu’elle a été proposée par https://github.com/dotnet/csharplang/issues/5955 et approuvée par le LDM.
Une interface contenant ou héritant d’un membre abstrait/virtuel statique qui n’a pas d’implémentation la plus spécifique dans l’interface ne peut pas être utilisée comme argument de type. Si tous les membres abstraits/virtuels statiques ont l’implémentation la plus spécifique, l’interface peut être utilisée comme argument de type.
Accès aux membres d’interface abstraits statiques
Un membre d’interface abstraite statique M
peut être accédé sur un paramètre de type T
à l’aide de l’expression T.M
lorsque T
est contraint par une interface I
et lorsque M
est un membre abstrait statique accessible de I
.
T M<T>() where T : I<T>
{
T.M();
T t = T.P;
T.E += () => { };
return t + T.P;
}
Lors de l’exécution, l’implémentation réelle des membres utilisée est celle qui existe sur le type réel fourni en tant qu’argument de type.
C c = M<C>(); // The static members of C get called
Étant donné que les expressions de requête sont spécifiées comme une réécriture syntaxique, C# vous permet en fait d’utiliser un type comme source de requête, tant qu’il possède des membres statiques pour les opérateurs de requête que vous utilisez ! En d'autres termes, si la syntaxe convient, nous le permettons ! Nous pensons que ce comportement n’était pas intentionnel ou important dans le LINQ original et nous ne voulons pas effectuer le travail nécessaire pour le prendre en charge sur les paramètres de type. S’il existe des scénarios, nous en entendrons parler et pourrons choisir d’adopter cette version ultérieurement.
Sécurité de variance §18.2.3.2
Les règles de sécurité de variance doivent s’appliquer aux signatures des membres abstraits statiques. L’ajout proposé dans https://github.com/dotnet/csharplang/blob/main/proposals/variance-safety-for-static-interface-members.md#variance-safety devrait être ajusté à partir de
Ces restrictions ne s’appliquent pas aux occurrences de types dans les déclarations des membres statiques.
à
Ces restrictions ne s'appliquent pas aux occurrences de types au sein des déclarations de non-virtuels, non-abstraits membres statiques.
§10.5.4 Conversions implicites définies par l’utilisateur
Les points suivants
- Déterminez les types
S
,S₀
etT₀
.- Si
E
a un type, laissezS
être ce type. - Si
S
ouT
sont des types de valeurs nullables, laissezSᵢ
etTᵢ
être leurs types sous-jacents, sinon laissezSᵢ
etTᵢ
êtreS
etT
, respectivement. - Si
Sᵢ
ouTᵢ
sont des paramètres de type, laissezS₀
etT₀
être leurs classes de base effectives, sinon laissezS₀
etT₀
êtreSₓ
etTᵢ
, respectivement.
- Si
- Recherchez l’ensemble de types,
D
, à partir duquel les opérateurs de conversion définis par l’utilisateur seront pris en compte. Cet ensemble se compose deS0
(siS0
est une classe ou un struct), des classes de base deS0
(siS0
est une classe) et deT0
(siT0
est une classe ou un struct). - Trouvez l'ensemble des opérateurs de conversion définis par l'utilisateur et surélevés,
U
. Cet ensemble se compose des opérateurs de conversion implicite définis par l’utilisateur et de ceux levés par les classes ou les structs dansD
qui se convertissent d’un type comprenantS
en un type compris dansT
. SiU
est vide, la conversion n’est pas définie et une erreur de compilation se produit.
sont ajustés comme suit :
- Déterminez les types
S
,S₀
etT₀
.- Si
E
a un type, laissezS
être ce type. - Si
S
ouT
sont des types de valeurs nullables, laissezSᵢ
etTᵢ
être leurs types sous-jacents, sinon laissezSᵢ
etTᵢ
êtreS
etT
, respectivement. - Si
Sᵢ
ouTᵢ
sont des paramètres de type, laissezS₀
etT₀
être leurs classes de base effectives, sinon laissezS₀
etT₀
êtreSₓ
etTᵢ
, respectivement.
- Si
- Trouvez l'ensemble des opérateurs de conversion définis par l'utilisateur et surélevés,
U
.- Recherchez l’ensemble de types,
D1
, à partir duquel les opérateurs de conversion définis par l’utilisateur seront pris en compte. Cet ensemble se compose deS0
(siS0
est une classe ou un struct), des classes de base deS0
(siS0
est une classe) et deT0
(siT0
est une classe ou un struct). - Trouvez l'ensemble des opérateurs de conversion définis par l'utilisateur et surélevés,
U1
. Cet ensemble se compose des opérateurs de conversion implicite définis par l’utilisateur et de ceux levés par les classes ou les structs dansD1
qui se convertissent d’un type comprenantS
en un type compris dansT
. - Si
U1
n’est pas vide, alorsU
estU1
. Sinon,- Recherchez l’ensemble de types,
D2
, à partir duquel les opérateurs de conversion définis par l’utilisateur seront pris en compte. Cet ensemble se compose de l’ensemble d’interfaces effectifSᵢ
et de leurs interfaces de base (siSᵢ
est un paramètre de type), ainsi que de l’ensemble d’interfaces effectifTᵢ
(siTᵢ
est un paramètre de type). - Trouvez l'ensemble des opérateurs de conversion définis par l'utilisateur et surélevés,
U2
. Cet ensemble se compose des opérateurs de conversion implicites définis par l'utilisateur et surélevés déclarés par les interfaces dansD2
qui convertissent d'un type englobantS
à un type englobé parT
. - Si
U2
n’est pas vide, alorsU
estU2
- Recherchez l’ensemble de types,
- Recherchez l’ensemble de types,
- Si
U
est vide, la conversion n’est pas définie et une erreur de compilation se produit.
§10.3.9 Conversions explicites définies par l'utilisateur
Les points suivants
- Déterminez les types
S
,S₀
etT₀
.- Si
E
a un type, laissezS
être ce type. - Si
S
ouT
sont des types de valeurs nullables, laissezSᵢ
etTᵢ
être leurs types sous-jacents, sinon laissezSᵢ
etTᵢ
êtreS
etT
, respectivement. - Si
Sᵢ
ouTᵢ
sont des paramètres de type, laissezS₀
etT₀
être leurs classes de base effectives, sinon laissezS₀
etT₀
êtreSᵢ
etTᵢ
, respectivement.
- Si
- Recherchez l’ensemble de types,
D
, à partir duquel les opérateurs de conversion définis par l’utilisateur seront pris en compte. Cet ensemble se compose deS0
(siS0
est une classe ou un struct), des classes de base deS0
(siS0
est une classe), deT0
(siT0
est une classe ou un struct) ainsi que des classes de base deT0
(siT0
est une classe). - Trouvez l'ensemble des opérateurs de conversion définis par l'utilisateur et surélevés,
U
. Cet ensemble se compose des opérateurs de conversion implicites ou explicites définis par l'utilisateur et surélevés déclarés par les classes ou structures dansD
qui convertissent d'un type englobant ou englobé parS
à un type englobant ou englobé parT
. SiU
est vide, la conversion n’est pas définie et une erreur de compilation se produit.
sont ajustés comme suit :
- Déterminez les types
S
,S₀
etT₀
.- Si
E
a un type, laissezS
être ce type. - Si
S
ouT
sont des types de valeurs nullables, laissezSᵢ
etTᵢ
être leurs types sous-jacents, sinon laissezSᵢ
etTᵢ
êtreS
etT
, respectivement. - Si
Sᵢ
ouTᵢ
sont des paramètres de type, laissezS₀
etT₀
être leurs classes de base effectives, sinon laissezS₀
etT₀
êtreSᵢ
etTᵢ
, respectivement.
- Si
- Trouvez l'ensemble des opérateurs de conversion définis par l'utilisateur et surélevés,
U
.- Recherchez l’ensemble de types,
D1
, à partir duquel les opérateurs de conversion définis par l’utilisateur seront pris en compte. Cet ensemble se compose deS0
(siS0
est une classe ou un struct), des classes de base deS0
(siS0
est une classe), deT0
(siT0
est une classe ou un struct) ainsi que des classes de base deT0
(siT0
est une classe). - Trouvez l'ensemble des opérateurs de conversion définis par l'utilisateur et surélevés,
U1
. Cet ensemble se compose des opérateurs de conversion implicites ou explicites définis par l'utilisateur et surélevés déclarés par les classes ou structures dansD1
qui convertissent d'un type englobant ou englobé parS
à un type englobant ou englobé parT
. - Si
U1
n’est pas vide, alorsU
estU1
. Sinon,- Recherchez l’ensemble de types,
D2
, à partir duquel les opérateurs de conversion définis par l’utilisateur seront pris en compte. Cet ensemble se compose de l’ensemble d’interfaces effectifSᵢ
et de leurs interfaces de base (siSᵢ
est un paramètre de type), ainsi que de l’ensemble d’interfaces effectifTᵢ
et de leurs interfaces de base (siTᵢ
est un paramètre de type). - Trouvez l'ensemble des opérateurs de conversion définis par l'utilisateur et surélevés,
U2
. Cet ensemble se compose des opérateurs de conversion implicites ou explicites définis par l'utilisateur et surélevés déclarés par les interfaces dansD2
qui convertissent d'un type englobant ou englobé parS
à un type englobant ou englobé parT
. - Si
U2
n’est pas vide, alorsU
estU2
- Recherchez l’ensemble de types,
- Recherchez l’ensemble de types,
- Si
U
est vide, la conversion n’est pas définie et une erreur de compilation se produit.
Implémentations par défaut
Une des fonctionnalités supplémentaires à cette proposition permet aux membres virtuels statiques dans les interfaces d'avoir des implémentations par défaut, tout comme le font les membres virtuels/abstraits d'instance.
Une complication ici est que les implémentations par défaut voudraient appeler « virtuellement » d’autres membres virtuels statiques. L’autorisation d’appeler des membres virtuels statiques directement sur l’interface nécessiterait le passage d’un paramètre de type caché représentant le type « self » sur lequel la méthode statique actuelle a réellement été invoquée. Cela semble compliqué, coûteux et potentiellement déroutant.
Nous avons discuté d'une version plus simple qui maintient les limitations de la proposition actuelle selon laquelle les membres virtuels statiques peuvent seulement être invoqués sur des paramètres de type. Étant donné que les interfaces avec des membres virtuels statiques auront souvent un paramètre de type explicite représentant un type « propre », cela ne serait pas une grande perte : d'autres membres virtuels statiques pourraient simplement être appelés sur ce type propre. Cette version est beaucoup plus simple et semble tout à fait réalisable.
À https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-24.md#default-implementations-of-abstract-statics nous avons décidé de prendre en charge les implémentations par défaut des membres statiques en suivant/étendant les règles établies dans https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/default-interface-methods.md en conséquence.
Critères spéciaux
Étant donné le code suivant, un utilisateur pourrait raisonnablement s’attendre à ce qu’il affiche « True » (comme ce serait le cas si le modèle constant était écrit en ligne) :
M(1.0);
static void M<T>(T t) where T : INumberBase<T>
{
Console.WriteLine(t is 1); // Error. Cannot use a numeric constant
Console.WriteLine((t is int i) && (i is 1));
}
Toutefois, étant donné que le type d’entrée du modèle n’est pas double
, le modèle constant 1
vérifiera d’abord le type T
entrant par rapport au type int
. Cette approche étant non intuitive, elle est suspendue jusqu’à ce qu’une version C# future ajoute une meilleure gestion des correspondances numériques avec les types dérivés de INumberBase<T>
. Pour cela, nous dirons que nous reconnaîtrons explicitement INumberBase<T>
comme le type dont dériveront tous les « nombres », et nous bloquerons le modèle si nous essayons de faire correspondre un modèle de constante numérique à un type numérique dans lequel nous ne pouvons pas représenter le modèle (par exemple, un paramètre de type contraint à INumberBase<T>
ou un type de nombre défini par l’utilisateur héritant de INumberBase<T>
).
Formellement, nous ajoutons une exception à la définition de pattern-compatible pour les modèles de constantes :
Un modèle de constante teste la valeur d’une expression par rapport à une valeur de constante. La constante peut être n’importe quelle expression constante, telle qu’un littéral, le nom d’une variable
const
déclarée ou une constante d’énumération. Lorsque la valeur d’entrée n’est pas un type ouvert, l’expression constante est implicitement convertie dans le type de l’expression correspondante ; si le type de la valeur d’entrée n’est pas pattern-compatible avec le type de l’expression constante, l’opération de correspondance de modèle est une erreur. Si l’expression constante mise en correspondance est une valeur numérique, la valeur d’entrée est un type qui hérite deSystem.Numerics.INumberBase<T>
, et il n’y a pas de conversion constante de l’expression constante au type de la valeur d’entrée, l’opération de correspondance de modèle est une erreur.
Nous ajoutons également une exception similaire pour les modèles relationnels :
Lorsque l’entrée est un type pour lequel un opérateur relationnel binaire intégré approprié est défini et applicable avec l’entrée comme opérande gauche et la constante donnée comme opérande droit, l’évaluation de cet opérateur est considérée comme la signification du modèle relationnel. Sinon, nous convertissons l'entrée au type de l'expression en utilisant une conversion nullable ou de déballage explicite. Il s’agit d’une erreur de compilation si aucune conversion de ce type n’existe. Il s’agit d’une erreur de compilation si le type d’entrée est un paramètre de type contraint à ou un type héritant de
System.Numerics.INumberBase<T>
et que le type d’entrée n’a pas d’opérateur relationnel binaire intégré approprié défini. Le modèle est considéré comme ne correspondant pas si la conversion échoue. Si la conversion réussit, le résultat de l’opération de correspondance de modèle est le résultat de l’évaluation de l’expression e OP v où e est l’entrée convertie, OP est l’opérateur relationnel et v est l’expression constante.
Inconvénients
- « abstrait statique » est un nouveau concept qui renforcera de manière significative la charge conceptuelle de C#.
- Ce n’est pas une fonctionnalité économique à développer. Il faut s’assurer que cela en vaut la peine.
Alternatives
Contraintes structurelles
Une autre approche consisterait à avoir des « contraintes structurelles » nécessitant directement et explicitement la présence d’opérateurs spécifiques sur un paramètre de type. Les inconvénients de cela sont : - Il faudrait l'écrire à chaque fois. Avoir une contrainte nommée paraît être une meilleure solution. - Il s’agit d’un tout nouveau type de contrainte, tandis que la fonctionnalité proposée utilise le concept existant de contraintes d’interface. - Cela ne fonctionnerait que pour les opérateurs et pas (facilement) pour d'autres types de membres statiques.
Questions non résolues
Interfaces abstraites statiques et classes statiques
Pour plus d'informations, consultez https://github.com/dotnet/csharplang/issues/5783 et https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md#static-abstract-interfaces-and-static-classes.
Réunions de conception
- https://github.com/dotnet/csharplang/blob/master/meetings/2021/LDM-2021-02-08.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-04-05.md
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-06-29.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-24.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-04-06.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-06-06.md
C# feature specifications