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é. 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é
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 et d’écrire 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 de l’interface
La fonctionnalité permettrait aux membres de l’interface statique d’être déclarés 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 virtuelle doivent être explicitement marqués comme sealed
.
Les membres de l’interface statique sont aujourd’hui implicitement non virtuels et n’autorisent pas abstract
, virtual
ou sealed
modificateurs.
Proposition
Membres statiques abstraits
Les membres de l’interface statique autres que les champs sont autorisés à avoir également 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 de l’interface statique autres que les champs sont autorisés à avoir également 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 la symétrie avec les membres d’instance non virtuelle, les membres statiques (sauf les champs) doivent être autorisés à un modificateur de sealed
facultatif, même s’ils ne sont pas 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 ne peut donc même 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 de l’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 de l’interface abstraite statique. Les déclarations de membres statiques existantes servent cet objectif.
Les implémentations explicites des membres de l’interface abstraite statique 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érateur unaire et binaire ont une exigence impliquant au moins l’un de leurs opérandes pour être de 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
compter comme « 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 de ==
et de !=
, ainsi que les déclarations abstraites/virtuelles des opérateurs de conversion implicite et explicite sont autorisées dans les interfaces. Les interfaces dérivées seront également autorisées à les implémenter.
Pour ==
et les opérateurs !=
, au moins un type de paramètre doit être un paramètre de type qui compte comme « 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 relatives au moment où une déclaration de membre statique dans une classe ou un struct est considérée comme implémentant un membre d’interface abstraite statique et pour les exigences qui s’appliquent quand elle le fait, sont identiques à celles des membres d’instance.
TBD : Il peut y avoir des règles supplémentaires ou différentes nécessaires ici que nous n’avons pas encore pensé.
Interfaces en tant qu’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 en tant qu’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érite 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 une implémentation la plus spécifique, l’interface peut être utilisée comme argument de type.
Accès aux membres de l’interface abstraite statique
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 de membre réelle 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 réécriture syntaxique, C# vous permet en fait d’utiliser un type comme source de requête, tant qu’il a 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 linQ d’origine et que nous ne voulons pas effectuer le travail pour le prendre en charge sur les paramètres de type. S’il existe des scénarios là-bas, nous en entendrons parler et pouvons 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 valeur 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), les classes de base deS0
(siS0
est une classe) etT0
(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 au moment de la 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 valeur 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), les classes de base deS0
(siS0
est une classe) etT0
(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 élargis, déclarés dansD1
par les classes ou structs, qui se convertissent d’un type qui englobeS
en un type qui est englobé parT
. - Si
U1
n’est pas vide,U
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 deSᵢ
jeu d’interfaces effectifs et de leurs interfaces de base (siSᵢ
est un paramètre de type) etTᵢ
jeu d’interfaces effectifs (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,U
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 au moment de la 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 valeur 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), les classes de base deS0
(siS0
est une classe),T0
(siT0
est une classe ou un struct) et les 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 au moment de la 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 valeur 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), les classes de base deS0
(siS0
est une classe),T0
(siT0
est une classe ou un struct) et les 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,U
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 deSᵢ
ensemble d’interfaces effectives et de leurs interfaces de base (siSᵢ
est un paramètre de type), etTᵢ
ensemble d’interfaces effectives et 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,U
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 au moment de la 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 souhaitent appeler d’autres membres virtuels statiques « virtuellement ». 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 doable.
À 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 peut raisonnablement s’attendre à ce qu’il imprime « True » (comme 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
. Il s’agit d’une solution non intuitive, donc elle est bloquée 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 ce faire, nous allons dire que, nous reconnaissons explicitement INumberBase<T>
comme type dont tous les « nombres » dériveront, et 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 dans (par exemple, un paramètre de type limité à INumberBase<T>
, ou un type de nombre défini par l’utilisateur qui hérite de INumberBase<T>
).
Formellement, nous ajoutons une exception à la définition de compatibles avec des modèles constants :
Un modèle constant teste la valeur d’une expression par rapport à une valeur 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 en type de l’expression correspondante ; si le type de la valeur d’entrée n’est pas compatible avec le modèle 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 vers le 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 qui s’applique à l’entrée en tant qu’opérande gauche et à la constante donnée comme opérande droit, l’évaluation de cet opérateur est prise comme 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 au moment de la compilation si aucune conversion de ce type n’existe. Il s’agit d’une erreur au moment de la compilation si le type d’entrée est un paramètre de type limité à ou un type hérite 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
- « static abstract » est un nouveau concept qui s’ajoute de manière significative à la charge conceptuelle de C#.
- Ce n’est pas une fonctionnalité bon marché à créer. On devrait s’assurer que ça vaut la peine.
Alternatives
Contraintes structurelles
Une autre approche consisterait à avoir des « contraintes structurelles » directement et explicitement nécessitant 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 semble mieux. - 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.
Concevoir des réunions
- 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