Partager via


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₀ et T₀.
    • Si E a un type, laissez S être ce type.
    • Si S ou T sont des types valeur nullables, laissez Sᵢ et Tᵢ être leurs types sous-jacents, sinon laissez Sᵢ et Tᵢ être S et T, respectivement.
    • Si Sᵢ ou Tᵢ sont des paramètres de type, laissez S₀ et T₀ être leurs classes de base effectives, sinon laissez S₀ et T₀ être Sₓ et Tᵢ, respectivement.
  • 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 de S0 (si S0 est une classe ou un struct), les classes de base de S0 (si S0 est une classe) et T0 (si T0 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 dans D qui se convertissent d’un type comprenant S en un type compris dans T. Si U 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₀ et T₀.
    • Si E a un type, laissez S être ce type.
    • Si S ou T sont des types valeur nullables, laissez Sᵢ et Tᵢ être leurs types sous-jacents, sinon laissez Sᵢ et Tᵢ être S et T, respectivement.
    • Si Sᵢ ou Tᵢ sont des paramètres de type, laissez S₀ et T₀ être leurs classes de base effectives, sinon laissez S₀ et T₀ être Sₓ et Tᵢ, respectivement.
  • 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 de S0 (si S0 est une classe ou un struct), les classes de base de S0 (si S0 est une classe) et T0 (si T0 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 dans D1 par les classes ou structs, qui se convertissent d’un type qui englobe S en un type qui est englobé par T.
    • Si U1 n’est pas vide, U est U1. 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 Sᵢjeu d’interfaces effectifs et de leurs interfaces de base (si Sᵢ est un paramètre de type) et Tᵢjeu d’interfaces effectifs (si Tᵢ 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 dans D2 qui convertissent d'un type englobant S à un type englobé par T.
      • Si U2 n’est pas vide, U est U2
  • 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₀ et T₀.
    • Si E a un type, laissez S être ce type.
    • Si S ou T sont des types valeur nullables, laissez Sᵢ et Tᵢ être leurs types sous-jacents, sinon laissez Sᵢ et Tᵢ être S et T, respectivement.
    • Si Sᵢ ou Tᵢ sont des paramètres de type, laissez S₀ et T₀ être leurs classes de base effectives, sinon laissez S₀ et T₀ être Sᵢ et Tᵢ, respectivement.
  • 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 de S0 (si S0 est une classe ou un struct), les classes de base de S0 (si S0 est une classe), T0 (si T0 est une classe ou un struct) et les classes de base de T0 (si T0 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 dans D qui convertissent d'un type englobant ou englobé par S à un type englobant ou englobé par T. Si U 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₀ et T₀.
    • Si E a un type, laissez S être ce type.
    • Si S ou T sont des types valeur nullables, laissez Sᵢ et Tᵢ être leurs types sous-jacents, sinon laissez Sᵢ et Tᵢ être S et T, respectivement.
    • Si Sᵢ ou Tᵢ sont des paramètres de type, laissez S₀ et T₀ être leurs classes de base effectives, sinon laissez S₀ et T₀ être Sᵢ et Tᵢ, respectivement.
  • 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 de S0 (si S0 est une classe ou un struct), les classes de base de S0 (si S0 est une classe), T0 (si T0 est une classe ou un struct) et les classes de base de T0 (si T0 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 dans D1 qui convertissent d'un type englobant ou englobé par S à un type englobant ou englobé par T.
    • Si U1 n’est pas vide, U est U1. 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 Sᵢensemble d’interfaces effectives et de leurs interfaces de base (si Sᵢ est un paramètre de type), et Tᵢensemble d’interfaces effectives et leurs interfaces de base (si Tᵢ 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 dans D2 qui convertissent d'un type englobant ou englobé par S à un type englobant ou englobé par T.
      • Si U2 n’est pas vide, U est U2
  • 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 de System.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