Partager via


15 classes

15.1 Général

Une classe est une structure de données qui peut contenir des membres de données (constantes et champs), des membres de fonction (méthodes, propriétés, événements, indexeurs, opérateurs, constructeurs d’instances, finaliseurs et constructeurs statiques) et des types imbriqués. Les types de classes prennent en charge l’héritage, un mécanisme dans lequel une classe dérivée peut étendre et spécialiser une classe de base.

15.2 Déclarations de classe

15.2.1 Général

Un class_declaration est un type_declaration (§14.7) qui déclare une nouvelle classe.

class_declaration
    : attributes? class_modifier* 'partial'? 'class' identifier
        type_parameter_list? class_base? type_parameter_constraints_clause*
        class_body ';'?
    ;

Un class_declaration se compose d’un ensemble facultatif d’attributs(§22), suivi d’un ensemble facultatif de class_modifiers (§15.2.2), suivi d’un modificateur facultatif partial (§15.2.7), suivi du mot clé class et d’un identificateur qui nomme la classe, suivi d’un type_parameter_list facultatif (§15.2.3), suivi d’une spécification class_base facultative (§15.2.4), suivi d’un ensemble facultatif de type_parameter_constraints_clause s (§15.2.5), suivi d’un class_body (§15.2.6), éventuellement suivi d’unpoint-virgule.

Une déclaration de classe ne fournit pas de type_parameter_constraints_clauses, sauf s’il fournit également un type_parameter_list.

Une déclaration de classe qui fournit un type_parameter_list est une déclaration de classe générique. En outre, toute classe imbriquée à l’intérieur d’une déclaration de classe générique ou d’une déclaration de struct générique est elle-même une déclaration de classe générique, car les arguments de type pour le type conteneur doivent être fournis pour créer un type construit (§8.4).

15.2.2 Modificateurs de classe

15.2.2.1 Général

Une class_declaration peut éventuellement inclure une séquence de modificateurs de classe :

class_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'abstract'
    | 'sealed'
    | 'static'
    | unsafe_modifier   // unsafe code support
    ;

unsafe_modifier (§23.2) est disponible uniquement dans le code non sécurisé (§23).

Il s’agit d’une erreur au moment de la compilation pour que le même modificateur apparaisse plusieurs fois dans une déclaration de classe.

Le new modificateur est autorisé sur les classes imbriquées. Elle spécifie que la classe masque un membre hérité du même nom, comme décrit dans le §15.3.5. Il s’agit d’une erreur au moment de la compilation pour que le new modificateur apparaisse sur une déclaration de classe qui n’est pas une déclaration de classe imbriquée.

Les publicmodificateurs , et protectedinternal les modificateurs privatecontrôlent l’accessibilité de la classe. Selon le contexte dans lequel la déclaration de classe se produit, certains de ces modificateurs peuvent ne pas être autorisés (§7.5.2).

Lorsqu’une déclaration de type partielle (§15.2.7) inclut une spécification d’accessibilité (via les publicmodificateurs, protectedetcinternalprivate.), cette spécification est d’accord avec toutes les autres parties qui incluent une spécification d’accessibilité. Si aucune partie d’un type partiel n’inclut une spécification d’accessibilité, le type reçoit l’accessibilité par défaut appropriée (§7.5.2).

Les abstractmodificateurs et sealed les staticmodificateurs sont abordés dans les sous-sections suivantes.

15.2.2.2 Classes abstraites

Le abstract modificateur est utilisé pour indiquer qu’une classe est incomplète et qu’elle est destinée à être utilisée uniquement comme classe de base. Une classe abstraite diffère d’une classe non abstraite de la manière suivante :

  • Une classe abstraite ne peut pas être instanciée directement et il s’agit d’une erreur au moment de la compilation pour utiliser l’opérateur new sur une classe abstraite. Bien qu’il soit possible d’avoir des variables et des valeurs dont les types au moment de la compilation sont abstraits, ces variables et valeurs seront nécessairement ou null contiennent des références à des instances de classes non abstraites dérivées des types abstraits.
  • Une classe abstraite est autorisée (mais pas obligatoire) à contenir des membres abstraits.
  • Une classe abstraite ne peut pas être scellée.

Lorsqu’une classe non abstraite est dérivée d’une classe abstraite, la classe non abstraite doit inclure des implémentations réelles de tous les membres abstraits hérités, remplaçant ainsi ces membres abstraits.

Exemple : dans le code suivant

abstract class A
{
    public abstract void F();
}

abstract class B : A
{
    public void G() {}
}

class C : B
{
    public override void F()
    {
        // Actual implementation of F
    }
}

la classe A abstraite introduit une méthode Fabstraite. La classe B introduit une méthode Gsupplémentaire, mais, dans la mesure où elle ne fournit pas d’implémentation F, B doit également être déclarée abstraite. Remplacements de C classes F et fournit une implémentation réelle. Étant donné qu’il n’y a pas de membres abstraits dans C, C est autorisé (mais pas obligatoire) à être non abstrait.

exemple de fin

Si une ou plusieurs parties d’une déclaration de type partielle (§15.2.7) d’une classe incluent le abstract modificateur, la classe est abstraite. Sinon, la classe n’est pas abstraite.

15.2.2.3 Classes sealed

Le sealed modificateur est utilisé pour empêcher la dérivation d’une classe. Une erreur au moment de la compilation se produit si une classe sealed est spécifiée comme classe de base d’une autre classe.

Une classe sealed ne peut pas également être une classe abstraite.

Remarque : Le sealed modificateur est principalement utilisé pour empêcher la dérivation involontaire, mais il active également certaines optimisations au moment de l’exécution. En particulier, étant donné qu’une classe sealed est connue pour ne jamais avoir de classes dérivées, il est possible de transformer des appels de membres de fonction virtuelle sur des instances de classe scellées en appels non virtuels. Note de fin

Si une ou plusieurs parties d’une déclaration de type partielle (§15.2.7) d’une classe incluent le sealed modificateur, la classe est scellée. Sinon, la classe est non scellée.

15.2.2.4 Classes statiques

15.2.2.4.1 Général

Le static modificateur est utilisé pour marquer la classe déclarée en tant que classe statique. Une classe statique ne doit pas être instanciée, ne doit pas être utilisée comme type et ne doit contenir que des membres statiques. Seule une classe statique peut contenir des déclarations de méthodes d’extension (§15.6.10).

Une déclaration de classe statique est soumise aux restrictions suivantes :

  • Une classe statique ne doit pas inclure de modificateur ou sealed d’un abstract modificateur. (Toutefois, étant donné qu’une classe statique ne peut pas être instanciée ou dérivée, elle se comporte comme si elle était à la fois scellée et abstraite.)
  • Une classe statique n’inclut pas de spécification de class_base (§15.2.4) et ne peut pas spécifier explicitement une classe de base ou une liste d’interfaces implémentées. Une classe statique hérite implicitement du type object.
  • Une classe statique ne doit contenir que des membres statiques (§15.3.8).

    Remarque : toutes les constantes et les types imbriqués sont classés comme membres statiques. Note de fin

  • Une classe statique n’a pas de membres avec protected, private protectedou protected internal d’accessibilité déclarée.

Il s’agit d’une erreur au moment de la compilation pour violer l’une de ces restrictions.

Une classe statique n’a aucun constructeur d’instance. Il n’est pas possible de déclarer un constructeur d’instance dans une classe statique et aucun constructeur d’instance par défaut (§15.11.5) n’est fourni pour une classe statique.

Les membres d’une classe statique ne sont pas statiques automatiquement, et les déclarations membres doivent inclure explicitement un static modificateur (à l’exception des constantes et des types imbriqués). Lorsqu’une classe est imbriquée dans une classe externe statique, la classe imbriquée n’est pas une classe statique, sauf si elle inclut explicitement un static modificateur.

Si une ou plusieurs parties d’une déclaration de type partielle (§15.2.7) d’une classe incluent le static modificateur, la classe est statique. Sinon, la classe n’est pas statique.

15.2.2.4.2 Référencement des types de classes statiques

Une namespace_or_type_name (§7.8) est autorisée à référencer une classe statique si

  • Le namespace_or_type_name est le T namespace_or_type_name de la forme T.I, ou
  • Le namespace_or_type-name est le T typeof_expression (§12.8.18) du formulaire typeof(T).

Une primary_expression (§12.8) est autorisée à référencer une classe statique si

  • Le primary_expression est le E member_access(§12.8.7) de la forme E.I.

Dans tout autre contexte, il s’agit d’une erreur au moment de la compilation pour référencer une classe statique.

Remarque : Par exemple, il s’agit d’une erreur d’utilisation d’une classe statique comme classe de base, d’un type constituant (§15.3.7) d’un membre, d’un argument de type générique ou d’une contrainte de paramètre de type. De même, une classe statique ne peut pas être utilisée dans un type de tableau, une nouvelle expression, une expression de cast, une expression est une expression, sizeof une expression ou une expression de valeur par défaut. Note de fin

15.2.3 Paramètres de type

Un paramètre de type est un identificateur simple qui désigne un espace réservé pour un argument de type fourni pour créer un type construit. Par constrast, un argument de type (§8.4.2) est le type qui est remplacé par le paramètre de type lorsqu’un type construit est créé.

type_parameter_list
    : '<' type_parameters '>'
    ;

type_parameters
    : attributes? type_parameter
    | type_parameters ',' attributes? type_parameter
    ;

type_parameter est définie dans le §8.5.

Chaque paramètre de type d’une déclaration de classe définit un nom dans l’espace de déclaration (§7.3) de cette classe. Par conséquent, il ne peut pas avoir le même nom qu’un autre paramètre de type de cette classe ou d’un membre déclaré dans cette classe. Un paramètre de type ne peut pas avoir le même nom que le type lui-même.

Deux déclarations de type générique partielles (dans le même programme) contribuent au même type générique indépendant s’ils ont le même nom complet (qui inclut un generic_dimension_specifier (§12.8.18) pour le nombre de paramètres de type) (§7.8.3). Deux déclarations de type partielles doivent spécifier le même nom pour chaque paramètre de type, dans l’ordre.

Spécification de base de la classe 15.2.4

15.2.4.1 Général

Une déclaration de classe peut inclure une spécification class_base , qui définit la classe de base directe de la classe et les interfaces (§18) directement implémentées par la classe.

class_base
    : ':' class_type
    | ':' interface_type_list
    | ':' class_type ',' interface_type_list
    ;

interface_type_list
    : interface_type (',' interface_type)*
    ;

15.2.4.2 Classes de base

Lorsqu’un class_type est inclus dans le class_base, il spécifie la classe de base directe de la classe déclarée. Si une déclaration de classe non partielle n’a pas de class_base ou si la class_base répertorie uniquement les types d’interface, la classe de base directe est supposée être object. Lorsqu’une déclaration de classe partielle inclut une spécification de classe de base, cette spécification de classe de base référence le même type que toutes les autres parties de ce type partiel qui incluent une spécification de classe de base. Si aucune partie d’une classe partielle n’inclut une spécification de classe de base, la classe de base est object. Une classe hérite des membres de sa classe de base directe, comme décrit dans le §15.3.4.

Exemple : dans le code suivant

class A {}
class B : A {}

La classe A est dite être la classe de base directe de B, et B est dite être dérivée de A. Étant A donné que ne spécifie pas explicitement une classe de base directe, sa classe de base directe est implicitement object.

exemple de fin

Pour un type de classe construit, y compris un type imbriqué déclaré dans une déclaration de type générique (§15.3.9.7), si une classe de base est spécifiée dans la déclaration de classe générique, la classe de base du type construit est obtenue en remplaçant, pour chaque type_parameter dans la déclaration de classe de base, le type_argument correspondant du type construit.

Exemple : Compte tenu des déclarations de classe générique

class B<U,V> {...}
class G<T> : B<string,T[]> {...}

la classe de base du type G<int> construit serait B<string,int[]>.

exemple de fin

La classe de base spécifiée dans une déclaration de classe peut être un type de classe construit (§8.4). Une classe de base ne peut pas être un paramètre de type propre (§8.5), bien qu’elle puisse impliquer les paramètres de type qui sont dans l’étendue.

Exemple :

class Base<T> {}

// Valid, non-constructed class with constructed base class
class Extend1 : Base<int> {}

// Error, type parameter used as base class
class Extend2<V> : V {}

// Valid, type parameter used as type argument for base class
class Extend3<V> : Base<V> {}

exemple de fin

La classe de base directe d’un type de classe doit être au moins aussi accessible que le type de classe lui-même (§7.5.5). Par exemple, il s’agit d’une erreur au moment de la compilation pour qu’une classe publique dérive d’une classe privée ou interne.

La classe de base directe d’un type de classe ne doit pas être l’un des types suivants : System.Array, , System.Delegateou System.EnumSystem.ValueType le dynamic type. En outre, une déclaration de classe générique ne doit pas être utilisée System.Attribute comme classe de base directe ou indirecte (§22.2.1).

Pour déterminer la signification de la spécification A de classe de base directe d’une classe B, la classe de base directe d’est B temporairement supposée être object, ce qui garantit que la signification d’une spécification de classe de base ne peut pas dépendre de manière récursive.

Exemple : les éléments suivants

class X<T>
{
    public class Y{}
}

class Z : X<Z.Y> {}

est en erreur, car dans la spécification X<Z.Y> de classe de base, la classe de base directe de Z est considérée comme étant object, et par conséquent (par les règles du §7.8) Z n’est pas considérée comme ayant un membre Y.

exemple de fin

Les classes de base d’une classe sont la classe de base directe et ses classes de base. En d’autres termes, l’ensemble de classes de base est la fermeture transitive de la relation de classe de base directe.

Exemple : Dans les éléments suivants :

class A {...}
class B<T> : A {...}
class C<T> : B<IComparable<T>> {...}
class D<T> : C<T[]> {...}

les classes de base de D<int> sont C<int[]>, B<IComparable<int[]>>, A, et object.

exemple de fin

À l’exception de la classe object, chaque classe a exactement une classe de base directe. La object classe n’a pas de classe de base directe et est la classe de base ultime de toutes les autres classes.

Il s’agit d’une erreur au moment de la compilation pour qu’une classe dépende de lui-même. Dans le cadre de cette règle, une classe dépend directement de sa classe de base directe (le cas échéant) et dépend directement de la classe englobante la plus proche dans laquelle elle est imbriquée (le cas échéant). Étant donné cette définition, l’ensemble complet de classes sur lequel dépend une classe est la fermeture transitive de la relation directe .

Exemple : l’exemple

class A : A {}

est erroné parce que la classe dépend de lui-même. De même, l’exemple

class A : B {}
class B : C {}
class C : A {}

est en erreur, car les classes dépendent circulairement d’eux-mêmes. Enfin, l’exemple

class A : B.C {}
class B : A
{
    public class C {}
}

entraîne une erreur au moment de la compilation, car A dépend B.C (sa classe de base directe), qui dépend B (sa classe englobante immédiatement), qui dépend Ade manière circulaire.

exemple de fin

Une classe ne dépend pas des classes qui sont imbriquées dans celle-ci.

Exemple : dans le code suivant

class A
{
    class B : A {}
}

BA dépend (car A est à la fois sa classe de base directe et sa classe englobante immédiatement), mais A ne dépend B pas (car B n’est ni une classe de base ni une classe englobante de A). Par conséquent, l’exemple est valide.

exemple de fin

Il n’est pas possible de dériver d’une classe scellée.

Exemple : dans le code suivant

sealed class A {}
class B : A {} // Error, cannot derive from a sealed class

La classe B est en erreur, car elle tente de dériver de la classe Asealed .

exemple de fin

Implémentations de l’interface 15.2.4.3

Une spécification class_base peut inclure une liste de types d’interface, auquel cas la classe est dite pour implémenter les types d’interface donnés. Pour un type de classe construit, y compris un type imbriqué déclaré dans une déclaration de type générique (§15.3.9.7), chaque type d’interface implémenté est obtenu en remplaçant, pour chaque type_parameter dans l’interface donnée, le type_argument correspondant du type construit.

L’ensemble d’interfaces d’un type déclaré dans plusieurs parties (§15.2.7) est l’union des interfaces spécifiées sur chaque partie. Une interface particulière ne peut être nommée qu’une seule fois sur chaque partie, mais plusieurs parties peuvent nommer la ou les mêmes interfaces de base. Il n’y a qu’une seule implémentation de chaque membre d’une interface donnée.

Exemple : Dans les éléments suivants :

partial class C : IA, IB {...}
partial class C : IC {...}
partial class C : IA, IB {...}

l’ensemble d’interfaces de base pour la classe C est IA, IBet IC.

exemple de fin

En règle générale, chaque partie fournit une implémentation de la ou des interfaces déclarées sur cette partie ; toutefois, il ne s’agit pas d’une exigence. Un composant peut fournir l’implémentation d’une interface déclarée dans une autre partie.

Exemple :

partial class X
{
    int IComparable.CompareTo(object o) {...}
}

partial class X : IComparable
{
    ...
}

exemple de fin

Les interfaces de base spécifiées dans une déclaration de classe peuvent être construites de types d’interface (§8.4, §18.2). Une interface de base ne peut pas être un paramètre de type seul, bien qu’elle puisse impliquer les paramètres de type qui se trouvent dans l’étendue.

Exemple : Le code suivant illustre comment une classe peut implémenter et étendre des types construits :

class C<U, V> {}
interface I1<V> {}
class D : C<string, int>, I1<string> {}
class E<T> : C<int, T>, I1<T> {}

exemple de fin

Les implémentations d’interface sont abordées plus loin dans le §18.6.

15.2.5 Contraintes de paramètre de type

Les déclarations de type et de méthode génériques peuvent éventuellement spécifier des contraintes de paramètre de type en incluant type_parameter_constraints_clauses.

type_parameter_constraints_clauses
    : type_parameter_constraints_clause
    | type_parameter_constraints_clauses type_parameter_constraints_clause
    ;

type_parameter_constraints_clause
    : 'where' type_parameter ':' type_parameter_constraints
    ;

type_parameter_constraints
    : primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
    | secondary_constraints (',' constructor_constraint)?
    | constructor_constraint
    ;

primary_constraint
    : class_type nullable_type_annotation?
    | 'class' nullable_type_annotation?
    | 'struct'
    | 'notnull'
    | 'unmanaged'
    ;

secondary_constraint
    : interface_type nullable_type_annotation?
    | type_parameter nullable_type_annotation?
    ;

secondary_constraints
    : secondary_constraint (',' secondary_constraint)*
    ;

constructor_constraint
    : 'new' '(' ')'
    ;

Chaque type_parameter_constraints_clause se compose du jeton where, suivi du nom d’un paramètre de type, suivi d’un signe deux-points et de la liste des contraintes pour ce paramètre de type. Il peut y avoir au maximum une where clause pour chaque paramètre de type, et les where clauses peuvent être répertoriées dans n’importe quel ordre. Comme les jetons et get les set jetons d’un accesseur de propriété, le where jeton n’est pas un mot clé.

La liste des contraintes fournies dans une where clause peut inclure l’un des composants suivants, dans cet ordre : une seule contrainte primaire, une ou plusieurs contraintes secondaires et la contrainte de constructeur, new().

Une contrainte primaire peut être un type de classe, la class de type référence, la contraintestruct type valeur, lanotnull non null ou la contrainteunmanagedtype non managé . Le type de classe et la contrainte de type référence peuvent inclure le nullable_type_annotation.

Une contrainte secondaire peut être une interface_type ou une type_parameter, éventuellement suivie d’un nullable_type_annotation. La présence du nullable_type_annotatione* indique que l’argument de type est autorisé à être le type de référence nullable qui correspond à un type de référence non Nullable qui satisfait à la contrainte.

La contrainte de type de référence spécifie qu’un argument de type utilisé pour le paramètre de type doit être un type référence. Tous les types de classes, les types d’interface, les types délégués, les types de tableaux et les paramètres de type connus comme étant un type de référence (tel que défini ci-dessous) répondent à cette contrainte.

Le type de classe, la contrainte de type référence et les contraintes secondaires peuvent inclure l’annotation de type nullable. La présence ou l’absence de cette annotation sur le paramètre de type indique les attentes de nullabilité pour l’argument de type :

  • Si la contrainte n’inclut pas l’annotation de type Nullable, l’argument de type est censé être un type référence non nullable. Un compilateur peut émettre un avertissement si l’argument de type est un type de référence nullable.
  • Si la contrainte inclut l’annotation de type Nullable, la contrainte est satisfaite par un type référence non Nullable et un type référence Nullable.

La valeur Nullabilité de l’argument de type n’a pas besoin de correspondre à la valeur Nullabilité du paramètre de type. Un compilateur peut émettre un avertissement si la valeur Nullabilité du paramètre de type ne correspond pas à la valeur Nullabilité de l’argument de type.

Remarque : Pour spécifier qu’un argument de type est un type référence nullable, n’ajoutez pas l’annotation de type nullable en tant que contrainte (utilisation T : class ou T : BaseClass), mais utilisez T? tout au long de la déclaration générique pour indiquer le type de référence nullable correspondant pour l’argument de type. Note de fin

L’annotation de type nullable, ?ne peut pas être utilisée sur un argument de type non contrainte.

Pour un paramètre T de type lorsque l’argument de type est un type C?référence nullable, les instances d’entre T? elles sont interprétées comme C?, et non C??.

Exemple : Les exemples suivants montrent comment la nullabilité d’un argument de type affecte la nullabilité d’une déclaration de son paramètre de type :

public class C
{
}

public static class  Extensions
{
    public static void M<T>(this T? arg) where T : notnull
    {

    }
}

public class Test
{
    public void M()
    {
        C? mightBeNull = new C();
        C notNull = new C();

        int number = 5;
        int? missing = null;

        mightBeNull.M(); // arg is C?
        notNull.M(); //  arg is C?
        number.M(); // arg is int?
        missing.M(); // arg is int?
    }
}

Lorsque l’argument de type est un type non Nullable, l’annotation de ? type indique que le paramètre est le type nullable correspondant. Lorsque l’argument de type est déjà un type référence nullable, le paramètre est le même type nullable.

exemple de fin

La contrainte non Null spécifie qu’un argument de type utilisé pour le paramètre de type doit être un type valeur non nullable ou un type référence non Nullable. Un argument de type qui n'est pas un type valeur non nullable ou un type référence non nullable est autorisé, mais un compilateur peut générer un avertissement diagnostique.

La contrainte de type valeur spécifie qu’un argument de type utilisé pour le paramètre de type doit être un type valeur non nullable. Tous les types de struct non nullables, les types d’énumération et les paramètres de type ayant la contrainte de type valeur répondent à cette contrainte. Notez que bien que classé comme type valeur, un type valeur nullable (§8.3.12) ne répond pas à la contrainte de type valeur. Un paramètre de type ayant la contrainte de type valeur n’a pas non plus la constructor_constraint, bien qu’il puisse être utilisé comme argument de type pour un autre paramètre de type avec un constructor_constraint.

Remarque : Le System.Nullable<T> type spécifie la contrainte de type valeur non nullable pour T. Ainsi, les types récursifs construits de manière récursive des formulaires T?? et Nullable<Nullable<T>> sont interdits. Note de fin

Comme unmanaged il ne s’agit pas d’un mot clé, dans primary_constraint la contrainte non managée est toujours ambiguë de façon syntactique avec class_type. Pour des raisons de compatibilité, si une recherche de nom (§12.8.4) du nom unmanaged réussit, elle est traitée comme un class_type. Sinon, elle est traitée comme la contrainte non managée.

La contrainte de type non managé spécifie qu’un argument de type utilisé pour le paramètre de type doit être un type non managé non nullable (§8.8).

Les types de pointeur ne sont jamais autorisés à être des arguments de type et ne répondent à aucune contrainte de type, même non managée, en dépit d’être des types non gérés.

Si une contrainte est un type de classe, un type d’interface ou un paramètre de type, ce type spécifie un « type de base » minimal que chaque argument de type utilisé pour ce paramètre de type doit prendre en charge. Chaque fois qu’un type construit ou une méthode générique est utilisé, l’argument de type est vérifié par rapport aux contraintes du paramètre de type au moment de la compilation. L’argument de type fourni répond aux conditions décrites dans le §8.4.5.

Une contrainte de class_type doit respecter les règles suivantes :

  • Le type doit être un type de classe.
  • Le type ne doit pas être sealed.
  • Le type ne doit pas être l’un des types suivants : System.Array ou System.ValueType.
  • Le type ne doit pas être object.
  • Au plus une contrainte pour un paramètre de type donné peut être un type de classe.

Un type spécifié en tant que contrainte de interface_type doit respecter les règles suivantes :

  • Le type doit être un type d’interface.
  • Un type ne doit pas être spécifié plusieurs fois dans une clause donnée where .

Dans les deux cas, la contrainte peut impliquer l’un des paramètres de type de la déclaration de type ou de méthode associée dans le cadre d’un type construit et peut impliquer le type déclaré.

Tout type de classe ou d’interface spécifié comme contrainte de paramètre de type doit être au moins aussi accessible (§7.5.5) que le type générique ou la méthode déclarée.

Un type spécifié en tant que contrainte de type_parameter doit satisfaire aux règles suivantes :

  • Le type doit être un paramètre de type.
  • Un type ne doit pas être spécifié plusieurs fois dans une clause donnée where .

En outre, il n’y a pas de cycles dans la graphe des dépendances des paramètres de type, où la dépendance est une relation transitive définie par :

  • Si un paramètre T de type est utilisé comme contrainte pour le paramètre S de type, S dépendT alors.
  • Si un paramètre S de type dépend d’un paramètre T de type et T dépend d’un paramètre U de type, Scela dépendU.

Étant donné cette relation, il s’agit d’une erreur au moment de la compilation pour qu’un paramètre de type dépende de lui-même (directement ou indirectement).

Toutes les contraintes doivent être cohérentes entre les paramètres de type dépendant. Si le paramètre S de type dépend du paramètre T de type, puis :

  • T ne doit pas avoir la contrainte de type valeur. Sinon, T il est effectivement scellé de sorte qu’il S soit forcé d’être le même type que T, éliminant la nécessité de deux paramètres de type.
  • Si S la contrainte de type valeur est définie, elle T n’a pas de contrainte class_type .
  • Si S elle a une A et T a une B, il doit y avoir une conversion d’identité ou une conversion de référence implicite de A vers B ou une conversion de référence implicite de B vers A.
  • Si S elle dépend également du paramètre U de type et U a une A et T a une B, il doit y avoir une conversion d’identité ou une conversion de référence implicite de A vers B ou une conversion de référence implicite de B vers A.

Il est valide pour S avoir la contrainte de type valeur et T avoir la contrainte de type référence. Cela limite T efficacement les types System.Object, , System.ValueTypeSystem.Enumet tout type d’interface.

Si la where clause d’un paramètre de type inclut une contrainte de constructeur (qui a le formulaire new()), il est possible d’utiliser l’opérateur new pour créer des instances du type (§12.8.17.2). Tout argument de type utilisé pour un paramètre de type avec une contrainte de constructeur doit être un type valeur, une classe non abstraite ayant un constructeur sans paramètre public ou un paramètre de type ayant la contrainte de type valeur ou la contrainte de constructeur.

Il s’agit d’une erreur au moment de la compilation pour type_parameter_constraints avoir un primary_constraint de struct ou unmanaged pour avoir également une constructor_constraint.

Exemple : Voici des exemples de contraintes :

interface IPrintable
{
    void Print();
}

interface IComparable<T>
{
    int CompareTo(T value);
}

interface IKeyProvider<T>
{
    T GetKey();
}

class Printer<T> where T : IPrintable {...}
class SortedList<T> where T : IComparable<T> {...}

class Dictionary<K,V>
    where K : IComparable<K>
    where V : IPrintable, IKeyProvider<K>, new()
{
    ...
}

L’exemple suivant est en erreur, car il provoque une circularité dans l’graphe des dépendances des paramètres de type :

class Circular<S,T>
    where S: T
    where T: S // Error, circularity in dependency graph
{
    ...
}

Les exemples suivants illustrent des situations non valides supplémentaires :

class Sealed<S,T>
    where S : T
    where T : struct // Error, `T` is sealed
{
    ...
}

class A {...}
class B {...}

class Incompat<S,T>
    where S : A, T
    where T : B // Error, incompatible class-type constraints
{
    ...
}

class StructWithClass<S,T,U>
    where S : struct, T
    where T : U
    where U : A // Error, A incompatible with struct
{
    ...
}

exemple de fin

L’effacement dynamique d’un type C est Cₓ construit comme suit :

  • S’il C s’agit d’un type Outer.Inner imbriqué, Cₓ il s’agit d’un type Outerₓ.Innerₓimbriqué.
  • S’il CCₓs’agit d’un type G<A¹, ..., Aⁿ> construit avec des arguments A¹, ..., Aⁿ de type, Cₓ il s’agit du type G<A¹ₓ, ..., Aⁿₓ>construit.
  • S’il C s’agit d’un type E[] de tableau, Cₓ il s’agit du type Eₓ[]de tableau .
  • Si C c’est dynamique, Cₓ c’est object.
  • Sinon, Cₓ est C.

La classe de base effective d’un paramètre T de type est définie comme suit :

Supposons qu’il s’agit R d’un ensemble de types tels que :

  • Pour chaque contrainte d’un paramètre de type, T contient sa classe de R base effective.
  • Pour chaque contrainte de ce type est un type de T struct, R contient System.ValueType.
  • Pour chaque contrainte de T ce type est un type d’énumération, R contient System.Enum.
  • Pour chaque contrainte d’un T type délégué, R contient son effacement dynamique.
  • Pour chaque contrainte de ce type est un type de T tableau, R contient System.Array.
  • Pour chaque contrainte d’un type de T classe, R contient son effacement dynamique.

Alors

  • Si T la contrainte de type valeur est définie, sa classe de base effective est System.ValueType.
  • Sinon, si R elle est vide, la classe de base effective est object.
  • Sinon, la classe de base effective de T est le type le plus englobant (§10.5.3) de jeu R. Si le jeu n’a pas de type englobant, la classe de base effective est Tobject. Les règles de cohérence garantissent que le type le plus englobant existe.

Si le paramètre de type est un paramètre de type de méthode dont les contraintes sont héritées de la méthode de base, la classe de base effective est calculée après la substitution de type.

Ces règles garantissent que la classe de base effective est toujours une class_type.

L’ensemble d’interface effectif d’un paramètre T de type est défini comme suit :

  • S’il T n’a aucun secondary_constraints, son jeu d’interface effectif est vide.
  • Si T elle a interface_type contraintes mais pas de contraintes type_parameter , son jeu d’interface efficace est l’ensemble d’effacements dynamiques de ses contraintes de interface_type .
  • S’il T n’a pas de contraintes interface_type mais qu’il a type_parameter contraintes, son jeu d’interface effectif est l’union des ensembles d’interfaces effectifs de ses contraintes de type_parameter .
  • S’il T existe à la fois des contraintes interface_type et des contraintes type_parameter , son jeu d’interface efficace est l’union de l’ensemble d’effacements dynamiques de ses contraintes de interface_type et des ensembles d’interfaces efficaces de ses contraintes de type_parameter .

Un paramètre de type est connu pour être un type de référence s’il a la contrainte de type de référence ou sa classe de base effective n’est pas object ou System.ValueType. Un paramètre de type est connu comme un type de référence non Nullable s’il est connu pour être un type référence et a la contrainte de type référence non Nullable.

Les valeurs d’un type de paramètre de type contraint peuvent être utilisées pour accéder aux membres d’instance implicites par les contraintes.

Exemple : Dans les éléments suivants :

interface IPrintable
{
    void Print();
}

class Printer<T> where T : IPrintable
{
    void PrintOne(T x) => x.Print();
}

les méthodes de IPrintable peuvent être appelées directement sur x , car T elles sont contraintes d’implémenter IPrintabletoujours .

exemple de fin

Lorsqu’une déclaration de type générique partielle inclut des contraintes, les contraintes sont d’accord avec toutes les autres parties qui incluent des contraintes. Plus précisément, chaque partie qui inclut des contraintes doit avoir des contraintes pour le même ensemble de paramètres de type, et pour chaque paramètre de type, les ensembles de contraintes primaire, secondaire et constructeur doivent être équivalents. Deux ensembles de contraintes sont équivalents s’ils contiennent les mêmes membres. Si aucune partie d’un type générique partiel spécifie des contraintes de paramètre de type, les paramètres de type sont considérés comme non contraints.

Exemple :

partial class Map<K,V>
    where K : IComparable<K>
    where V : IKeyProvider<K>, new()
{
    ...
}

partial class Map<K,V>
    where V : IKeyProvider<K>, new()
    where K : IComparable<K>
{
    ...
}

partial class Map<K,V>
{
    ...
}

est correct, car ces parties qui incluent des contraintes (les deux premières) spécifient effectivement le même ensemble de contraintes primaire, secondaire et constructeur pour le même ensemble de paramètres de type, respectivement.

exemple de fin

15.2.6 Corps de la classe

La class_body d’une classe définit les membres de cette classe.

class_body
    : '{' class_member_declaration* '}'
    ;

15.2.7 Déclarations partielles

Le modificateur partial est utilisé lors de la définition d’une classe, d’un struct ou d’un type d’interface dans plusieurs parties. Le partial modificateur est un mot clé contextuel (§6.4.4) et a uniquement une signification spéciale immédiatement avant l’un des mots clés class, structou interface.

Chaque partie d’une déclaration de type partielle doit inclure un partial modificateur et doit être déclarée dans le même espace de noms ou contenant le type que les autres parties. Le partial modificateur indique que des parties supplémentaires de la déclaration de type peuvent exister ailleurs, mais que l’existence de ces parties supplémentaires n’est pas obligatoire ; il est valide pour la seule déclaration d’un type à inclure le partial modificateur. Il est valide pour une seule déclaration d’un type partiel afin d’inclure la classe de base ou les interfaces implémentées. Toutefois, toutes les déclarations d’une classe de base ou d’interfaces implémentées doivent correspondre, y compris la possibilité null des arguments de type spécifiés.

Toutes les parties d’un type partiel doivent être compilées de manière à ce que les parties puissent être fusionnées au moment de la compilation. Les types partiels n’autorisent pas spécifiquement l’extension des types compilés.

Les types imbriqués peuvent être déclarés dans plusieurs parties à l’aide du partial modificateur. En règle générale, le type conteneur est également déclaré à l’aide partial , et chaque partie du type imbriqué est déclarée dans une partie différente du type conteneur.

Exemple : la classe partielle suivante est implémentée en deux parties, qui résident dans des unités de compilation différentes. La première partie est générée par un outil de mappage de base de données alors que la deuxième partie est créée manuellement :

public partial class Customer
{
    private int id;
    private string name;
    private string address;
    private List<Order> orders;

    public Customer()
    {
        ...
    }
}

// File: Customer2.cs
public partial class Customer
{
    public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted);

    public bool HasOutstandingOrders() => orders.Count > 0;
}

Lorsque les deux parties ci-dessus sont compilées ensemble, le code résultant se comporte comme si la classe avait été écrite en tant qu’unité unique, comme suit :

public class Customer
{
    private int id;
    private string name;
    private string address;
    private List<Order> orders;

    public Customer()
    {
        ...
    }

    public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted);

    public bool HasOutstandingOrders() => orders.Count > 0;
}

exemple de fin

La gestion des attributs spécifiés sur le type ou les paramètres de type de différentes parties d’une déclaration partielle est abordée dans le §22.3.

15.3 Membres de la classe

15.3.1 Général

Les membres d’une classe se composent des membres introduits par ses class_member_declarations et les membres hérités de la classe de base directe.

class_member_declaration
    : constant_declaration
    | field_declaration
    | method_declaration
    | property_declaration
    | event_declaration
    | indexer_declaration
    | operator_declaration
    | constructor_declaration
    | finalizer_declaration
    | static_constructor_declaration
    | type_declaration
    ;

Les membres d’une classe sont divisés en catégories suivantes :

  • Constantes, qui représentent des valeurs constantes associées à la classe (§15.4).
  • Champs, qui sont les variables de la classe (§15.5).
  • Méthodes qui implémentent les calculs et les actions qui peuvent être effectués par la classe (§15.6).
  • Propriétés, qui définissent les caractéristiques nommées et les actions associées à la lecture et à l’écriture de ces caractéristiques (§15.7).
  • Événements, qui définissent les notifications qui peuvent être générées par la classe (§15.8).
  • Les indexeurs, qui permettent aux instances de la classe d’être indexées de la même façon (syntactiquement) que les tableaux (§15.9).
  • Opérateurs, qui définissent les opérateurs d’expression qui peuvent être appliqués aux instances de la classe (§15.10).
  • Constructeurs d’instances, qui implémentent les actions requises pour initialiser les instances de la classe (§15.11)
  • Finaliseurs, qui implémentent les actions à effectuer avant que les instances de la classe soient définitivement ignorées (§15.13).
  • Constructeurs statiques, qui implémentent les actions requises pour initialiser la classe elle-même (§15.12).
  • Types, qui représentent les types qui sont locaux à la classe (§14.7).

Un class_declaration crée un espace de déclaration (§7.3) et les type_parameter et les class_member_declarationimmédiatement contenus par le class_declaration introduisent de nouveaux membres dans cet espace de déclaration. Les règles suivantes s’appliquent aux class_member_declarations :

  • Les constructeurs d’instance, les finaliseurs et les constructeurs statiques doivent avoir le même nom que la classe englobante immédiatement. Tous les autres membres doivent avoir des noms qui diffèrent du nom de la classe englobante immédiatement.

  • Le nom d’un paramètre de type dans la type_parameter_list d’une déclaration de classe diffère des noms de tous les autres paramètres de type dans la même type_parameter_list et diffère du nom de la classe et des noms de tous les membres de la classe.

  • Le nom d’un type diffère des noms de tous les membres non de type déclarés dans la même classe. Si deux déclarations de type ou plus partagent le même nom complet, les déclarations doivent avoir le partial modificateur (§15.2.7) et ces déclarations se combinent pour définir un type unique.

Remarque : Étant donné que le nom complet d’une déclaration de type encode le nombre de paramètres de type, deux types distincts peuvent partager le même nom tant qu’ils ont un nombre différent de paramètres de type. Note de fin

  • Le nom d’une constante, d’un champ, d’une propriété ou d’un événement diffère des noms de tous les autres membres déclarés dans la même classe.

  • Le nom d’une méthode diffère des noms de toutes les autres méthodes non déclarées dans la même classe. En outre, la signature (§7.6) d’une méthode diffère des signatures de toutes les autres méthodes déclarées dans la même classe, et deux méthodes déclarées dans la même classe ne doivent pas avoir de signatures qui diffèrent uniquement par in, outet ref.

  • La signature d’un constructeur d’instance diffère des signatures de tous les autres constructeurs d’instance déclarés dans la même classe, et deux constructeurs déclarés dans la même classe ne doivent pas avoir de signatures qui diffèrent uniquement par ref et out.

  • La signature d’un indexeur diffère des signatures de tous les autres indexeurs déclarés dans la même classe.

  • La signature d’un opérateur doit différer des signatures de tous les autres opérateurs déclarés dans la même classe.

Les membres hérités d’une classe (§15.3.4) ne font pas partie de l’espace de déclaration d’une classe.

Remarque : Par conséquent, une classe dérivée est autorisée à déclarer un membre portant le même nom ou signature qu’un membre hérité (qui masque en effet le membre hérité). Note de fin

L’ensemble de membres d’un type déclaré dans plusieurs parties (§15.2.7) est l’union des membres déclarés dans chaque partie. Les corps de toutes les parties de la déclaration de type partagent le même espace de déclaration (§7.3) et l’étendue de chaque membre (§7.7) s’étend aux corps de toutes les parties. Le domaine d’accessibilité d’un membre inclut toujours toutes les parties du type englobant ; un membre privé déclaré dans une partie est librement accessible d’une autre partie. Il s’agit d’une erreur au moment de la compilation pour déclarer le même membre dans plusieurs parties du type, sauf si ce membre est un type ayant le partial modificateur.

Exemple :

partial class A
{
    int x;                   // Error, cannot declare x more than once

    partial class Inner      // Ok, Inner is a partial type
    {
        int y;
    }
}

partial class A
{
    int x;                   // Error, cannot declare x more than once

    partial class Inner      // Ok, Inner is a partial type
    {
        int z;
    }
}

exemple de fin

L’ordre d’initialisation des champs peut être significatif dans le code C#, et certaines garanties sont fournies, comme défini dans le §15.5.6.1. Sinon, l’ordre des membres au sein d’un type est rarement significatif, mais peut être significatif lors de l’interaction avec d’autres langages et environnements. Dans ces cas, l’ordre des membres dans un type déclaré dans plusieurs parties n’est pas défini.

15.3.2 Type d’instance

Chaque déclaration de classe a un type d’instance associé. Pour une déclaration de classe générique, le type d’instance est formé en créant un type construit (§8.4) à partir de la déclaration de type, chacun des arguments de type fournis étant le paramètre de type correspondant. Étant donné que le type d’instance utilise les paramètres de type, il ne peut être utilisé que lorsque les paramètres de type sont dans l’étendue ; c’est-à-dire à l’intérieur de la déclaration de classe. Le type d’instance est le type de code écrit à l’intérieur de this la déclaration de classe. Pour les classes non génériques, le type d’instance est simplement la classe déclarée.

Exemple : L’exemple suivant montre plusieurs déclarations de classe, ainsi que leurs types d’instances :

class A<T>             // instance type: A<T>
{
    class B {}         // instance type: A<T>.B
    class C<U> {}      // instance type: A<T>.C<U>
}
class D {}             // instance type: D

exemple de fin

15.3.3 Membres de types construits

Les membres non hérités d’un type construit sont obtenus en remplaçant, pour chaque type_parameter dans la déclaration de membre, le type_argument correspondant du type construit. Le processus de substitution est basé sur la signification sémantique des déclarations de type et n’est pas simplement une substitution textuelle.

Exemple : Compte tenu de la déclaration de classe générique

class Gen<T,U>
{
    public T[,] a;
    public void G(int i, T t, Gen<U,T> gt) {...}
    public U Prop { get {...} set {...} }
    public int H(double d) {...}
}

le type Gen<int[],IComparable<string>> construit a les membres suivants :

public int[,][] a;
public void G(int i, int[] t, Gen<IComparable<string>,int[]> gt) {...}
public IComparable<string> Prop { get {...} set {...} }
public int H(double d) {...}

Le type du membre a dans la déclaration Gen de classe générique est « tableau à deux dimensions de T», de sorte que le type du membre a dans le type construit ci-dessus est « tableau à deux dimensions d’un tableau unidimensionnel de int« ou int[,][].

exemple de fin

Dans les membres de la fonction d’instance, le type de this l’instance est le type d’instance (§15.3.2) de la déclaration conteneur.

Tous les membres d’une classe générique peuvent utiliser des paramètres de type à partir de n’importe quelle classe englobante, directement ou dans le cadre d’un type construit. Lorsqu’un type construit fermé particulier (§8.4.3) est utilisé au moment de l’exécution, chaque utilisation d’un paramètre de type est remplacée par l’argument de type fourni au type construit.

Exemple :

class C<V>
{
    public V f1;
    public C<V> f2;

    public C(V x)
    {
        this.f1 = x;
        this.f2 = this;
    }
}

class Application
{
    static void Main()
    {
        C<int> x1 = new C<int>(1);
        Console.WriteLine(x1.f1);              // Prints 1

        C<double> x2 = new C<double>(3.1415);
        Console.WriteLine(x2.f1);              // Prints 3.1415
    }
}

exemple de fin

Héritage 15.3.4

Une classe hérite des membres de sa classe de base directe. L’héritage signifie qu’une classe contient implicitement tous les membres de sa classe de base directe, à l’exception des constructeurs d’instance, des finaliseurs et des constructeurs statiques de la classe de base. Certains aspects importants de l’héritage sont les suivants :

  • L’héritage est transitif. Si C elle est dérivée B, et B est dérivée de A, C hérite des membres déclarés ainsi B que les membres déclarés dans A.

  • Une classe dérivée étend sa classe de base directe. Une classe dérivée peut ajouter des membres hérités, mais ne peut pas supprimer la définition d’un membre hérité.

  • Les constructeurs d’instances, les finaliseurs et les constructeurs statiques ne sont pas hérités, mais tous les autres membres sont, indépendamment de leur accessibilité déclarée (§7.5). Toutefois, selon leur accessibilité déclarée, les membres hérités peuvent ne pas être accessibles dans une classe dérivée.

  • Une classe dérivée peut masquer les membres hérités (§7.7.2.3) en déclarant de nouveaux membres portant le même nom ou signature. Toutefois, le masquage d’un membre hérité ne supprime pas ce membre. Il rend simplement ce membre inaccessible directement par le biais de la classe dérivée.

  • Une instance d’une classe contient un ensemble de tous les champs d’instance déclarés dans la classe et ses classes de base, et une conversion implicite (§10.2.8) existe d’un type de classe dérivé vers l’un de ses types de classe de base. Ainsi, une référence à une instance de certaines classes dérivées peut être traitée comme une référence à une instance de l’une de ses classes de base.

  • Une classe peut déclarer des méthodes virtuelles, des propriétés, des indexeurs et des événements, et les classes dérivées peuvent remplacer l’implémentation de ces membres de fonction. Cela permet aux classes d’exposer un comportement polymorphe dans lequel les actions effectuées par un appel de membre de fonction varient en fonction du type d’exécution de l’instance par le biais duquel ce membre de fonction est appelé.

Les membres hérités d’un type de classe construit sont les membres du type de classe de base immédiate (§15.2.4.2), qui est trouvé en remplaçant les arguments de type du type construit pour chaque occurrence des paramètres de type correspondants dans la base_class_specification. Ces membres, à leur tour, sont transformés en remplaçant, pour chaque type_parameter dans la déclaration de membre, le type_argument correspondant du base_class_specification.

Exemple :

class B<U>
{
    public U F(long index) {...}
}

class D<T> : B<T[]>
{
    public T G(string s) {...}
}

Dans le code ci-dessus, le type D<int> construit a un membre intG(string s) non hérité obtenu en remplaçant l’argument int de type pour le paramètre Tde type. D<int> a également un membre hérité de la déclaration Bde classe . Ce membre hérité est déterminé en premier lieu en déterminant le type B<int[]> de classe de base en D<int> remplaçant intT dans la spécification B<T[]>de la classe de base . Ensuite, en tant qu’argument de type à B, int[] est remplacé par U in public U F(long index), ce qui génère le membre public int[] F(long index)hérité .

exemple de fin

15.3.5 Le nouveau modificateur

Une class_member_declaration est autorisée à déclarer un membre portant le même nom ou signature qu’un membre hérité. Lorsque cela se produit, le membre de classe dérivé est dit pour masquer le membre de classe de base. Consultez le §7.7.2.3 pour obtenir une spécification précise du moment où un membre masque un membre hérité.

Un membre M hérité est considéré comme disponible s’il M est accessible et il n’existe aucun autre membre accessible hérité N qui masque Mdéjà . Le masquage implicite d’un membre hérité n’est pas considéré comme une erreur, mais un compilateur émet un avertissement, sauf si la déclaration du membre de classe dérivé inclut un modificateur new pour indiquer explicitement que le membre dérivé est destiné à masquer le membre de base. Si une ou plusieurs parties d’une déclaration partielle (§15.2.7) d’un type imbriqué incluent le new modificateur, aucun avertissement n’est émis si le type imbriqué masque un membre hérité disponible.

Si un new modificateur est inclus dans une déclaration qui ne masque pas un membre hérité disponible, un avertissement à cet effet est émis.

15.3.6 Modificateurs d’accès

Une class_member_declaration peut avoir l’un des types autorisés d’accessibilité déclarée (§7.5.2) : public, protected internal, , protected, private protected, internalou private. À l’exception des combinaisons et protected internal des combinaisons, il s’agit d’une erreur au moment de la private protected compilation pour spécifier plusieurs modificateurs d’accès. Lorsqu’un class_member_declaration n’inclut aucun modificateur d’accès, private il est supposé.

15.3.7 Types constituants

Les types utilisés dans la déclaration d’un membre sont appelés types constituants de ce membre. Les types constituants possibles sont le type d’une constante, d’un champ, d’une propriété, d’un événement ou d’un indexeur, du type de retour d’une méthode ou d’un opérateur et des types de paramètres d’une méthode, d’un indexeur, d’un opérateur ou d’un constructeur d’instance. Les types constituants d’un membre doivent être au moins aussi accessibles que ce membre lui-même (§7.5.5).

15.3.8 Membres statiques et d’instance

Les membres d’une classe sont des membres statiques ou des membres d’instance.

Remarque : En général, il est utile de considérer les membres statiques comme appartenant à des classes et des membres d’instance comme appartenant à des objets (instances de classes). Note de fin

Lorsqu’un champ, une méthode, une propriété, un événement, un opérateur ou une déclaration de constructeur inclut un static modificateur, il déclare un membre statique. En outre, une constante ou une déclaration de type déclare implicitement un membre statique. Les membres statiques ont les caractéristiques suivantes :

  • Lorsqu’un membre M statique est référencé dans un member_access (§12.8.7) du formulaire E.M, E désigne un type qui a un membre M. Il s’agit d’une erreur au moment de la compilation pour E désigner une instance.
  • Un champ statique dans une classe non générique identifie exactement un emplacement de stockage. Peu importe le nombre d’instances d’une classe non générique créées, il n’existe qu’une seule copie d’un champ statique. Chaque type construit fermé distinct (§8.4.3) a son propre ensemble de champs statiques, quel que soit le nombre d’instances du type construit fermé.
  • Un membre de fonction statique (méthode, propriété, événement, opérateur ou constructeur) ne fonctionne pas sur une instance spécifique, et il s’agit d’une erreur au moment de la compilation à laquelle faire référence dans un tel membre de fonction.

Lorsqu’un champ, une méthode, une propriété, un événement, un indexeur, un constructeur ou une déclaration finaliseur n’inclut pas de modificateur statique, il déclare un membre d’instance. (Un membre d’instance est parfois appelé membre non statique.) Les membres d’instance ont les caractéristiques suivantes :

  • Lorsqu’un membre M d’instance est référencé dans un member_access (§12.8.7) du formulaire E.M, E désigne une instance d’un type qui a un membre M. Il s’agit d’une erreur au moment de la liaison pour E de désigner un type.
  • Chaque instance d’une classe contient un ensemble distinct de tous les champs d’instance de la classe.
  • Un membre de fonction d’instance (méthode, propriété, indexeur, constructeur d’instance ou finaliseur) opère sur une instance donnée de la classe, et cette instance est accessible en tant que this (§12.8.14).

Exemple : L’exemple suivant illustre les règles d’accès aux membres statiques et d’instance :

class Test
{
    int x;
    static int y;
    void F()
    {
        x = 1;               // Ok, same as this.x = 1
        y = 1;               // Ok, same as Test.y = 1
    }

    static void G()
    {
        x = 1;               // Error, cannot access this.x
        y = 1;               // Ok, same as Test.y = 1
    }

    static void Main()
    {
        Test t = new Test();
        t.x = 1;       // Ok
        t.y = 1;       // Error, cannot access static member through instance
        Test.x = 1;    // Error, cannot access instance member through type
        Test.y = 1;    // Ok
    }
}

La F méthode montre que dans un membre de fonction d’instance, un simple_name (§12.8.4) peut être utilisé pour accéder à la fois aux membres de l’instance et aux membres statiques. La G méthode indique que dans un membre de fonction statique, il s’agit d’une erreur au moment de la compilation pour accéder à un membre d’instance via un simple_name. La Main méthode montre que dans un member_access (§12.8.7), les membres d’instance sont accessibles via des instances, et les membres statiques sont accessibles via des types.

exemple de fin

15.3.9 Types imbriqués

15.3.9.1 Général

Un type déclaré dans une classe ou un struct est appelé type imbriqué. Un type déclaré dans une unité de compilation ou un espace de noms est appelé type non imbriqué.

Exemple : Dans l’exemple suivant :

class A
{
    class B
    {
        static void F()
        {
            Console.WriteLine("A.B.F");
        }
    }
}

la classe B est un type imbriqué, car il est déclaré dans la classe A, et la classe A est un type non imbriqué, car il est déclaré dans une unité de compilation.

exemple de fin

15.3.9.2 Nom complet

Nom complet (§7.8.3) pour une déclaration S.N de type imbriquée, où S est le nom complet de la déclaration de type N déclarée et N est le nom non qualifié (§7.8.2) de la déclaration de type imbriquée (y compris les generic_dimension_specifier (§12.8.18)).

15.3.9.3 Accessibilité déclarée

Les types non imbriqués peuvent avoir public ou internal déclaré l’accessibilité et avoir internal déclaré l’accessibilité par défaut. Les types imbriqués peuvent également avoir ces formes d’accessibilité déclarée, plus une ou plusieurs formes supplémentaires d’accessibilité déclarée, selon que le type conteneur est une classe ou un struct :

  • Un type imbriqué déclaré dans une classe peut avoir l’un des types autorisés d’accessibilité déclarée et, comme d’autres membres de classe, par défaut pour private déclarer l’accessibilité.
  • Un type imbriqué déclaré dans un struct peut avoir l’une des trois formes d’accessibilité déclarée (public, internalou private) et, comme d’autres membres de struct, par défaut pour private déclarer l’accessibilité.

Exemple : l’exemple

public class List
{
    // Private data structure
    private class Node
    {
        public object Data;
        public Node? Next;

        public Node(object data, Node? next)
        {
            this.Data = data;
            this.Next = next;
        }
    }

    private Node? first = null;
    private Node? last = null;

    // Public interface
    public void AddToFront(object o) {...}
    public void AddToBack(object o) {...}
    public object RemoveFromFront() {...}
    public object RemoveFromBack() {...}
    public int Count { get {...} }
}

déclare une classe Nodeimbriquée privée .

exemple de fin

15.3.9.4 Masquage

Un type imbriqué peut masquer (§7.7.2.2) un membre de base. Le new modificateur (§15.3.5) est autorisé sur les déclarations de type imbriquées afin que le masquage puisse être exprimé explicitement.

Exemple : l’exemple

class Base
{
    public static void M()
    {
        Console.WriteLine("Base.M");
    }
}

class Derived: Base
{
    public new class M
    {
        public static void F()
        {
            Console.WriteLine("Derived.M.F");
        }
    }
}

class Test
{
    static void Main()
    {
        Derived.M.F();
    }
}

affiche une classe M imbriquée qui masque la méthode M définie dans Base.

exemple de fin

15.3.9.5 cet accès

Un type imbriqué et son type conteneur n’ont pas de relation spéciale en ce qui concerne this_access (§12.8.14). Plus précisément, this dans un type imbriqué, ne peut pas être utilisé pour faire référence aux membres d’instance du type conteneur. Dans les cas où un type imbriqué doit accéder aux membres d’instance de son type conteneur, l’accès peut être fourni en fournissant l’instance this du type conteneur en tant qu’argument de constructeur pour le type imbriqué.

Exemple : l’exemple suivant

class C
{
    int i = 123;
    public void F()
    {
        Nested n = new Nested(this);
        n.G();
    }

    public class Nested
    {
        C this_c;

        public Nested(C c)
        {
            this_c = c;
        }

        public void G()
        {
            Console.WriteLine(this_c.i);
        }
    }
}

class Test
{
    static void Main()
    {
        C c = new C();
        c.F();
    }
}

montre cette technique. Instance de création d’une instance de CNested, et transmet son propre constructeur au Nestedconstructeur afin de fournir un accès ultérieur aux Cmembres de l’instance.

exemple de fin

15.3.9.6 Accès aux membres privés et protégés du type conteneur

Un type imbriqué a accès à tous les membres accessibles à son type conteneur, y compris les membres du type conteneur qui ont private et protected déclaré l’accessibilité.

Exemple : l’exemple

class C
{
    private static void F() => Console.WriteLine("C.F");

    public class Nested
    {
        public static void G() => F();
    }
}

class Test
{
    static void Main() => C.Nested.G();
}

affiche une classe C qui contient une classe Nestedimbriquée . Dans Nested, la méthode G appelle la méthode F statique définie dans C, et F a l’accessibilité déclarée privée.

exemple de fin

Un type imbriqué peut également accéder aux membres protégés définis dans un type de base de son type conteneur.

Exemple : dans le code suivant

class Base
{
    protected void F() => Console.WriteLine("Base.F");
}

class Derived: Base
{
    public class Nested
    {
        public void G()
        {
            Derived d = new Derived();
            d.F(); // ok
        }
    }
}

class Test
{
    static void Main()
    {
        Derived.Nested n = new Derived.Nested();
        n.G();
    }
}

la classe Derived.Nested imbriquée accède à la méthode F protégée définie dans Derivedla classe de base, Baseen appelant via une instance de Derived.

exemple de fin

15.3.9.7 Types imbriqués dans les classes génériques

Une déclaration de classe générique peut contenir des déclarations de type imbriquées. Les paramètres de type de la classe englobante peuvent être utilisés dans les types imbriqués. Une déclaration de type imbriquée peut contenir des paramètres de type supplémentaires qui s’appliquent uniquement au type imbriqué.

Chaque déclaration de type contenue dans une déclaration de classe générique est implicitement une déclaration de type générique. Lors de l’écriture d’une référence à un type imbriqué dans un type générique, le type construit contenant, y compris ses arguments de type, doit être nommé. Toutefois, à partir de la classe externe, le type imbriqué peut être utilisé sans qualification ; le type d’instance de la classe externe peut être implicitement utilisé lors de la construction du type imbriqué.

Exemple : L’exemple suivant montre trois façons différentes de faire référence à un type construit créé à partir duquel Innerles deux premiers sont équivalents :

class Outer<T>
{
    class Inner<U>
    {
        public static void F(T t, U u) {...}
    }

    static void F(T t)
    {
        Outer<T>.Inner<string>.F(t, "abc");    // These two statements have
        Inner<string>.F(t, "abc");             // the same effect
        Outer<int>.Inner<string>.F(3, "abc");  // This type is different
        Outer.Inner<string>.F(t, "abc");       // Error, Outer needs type arg
    }
}

exemple de fin

Bien qu’il s’agit d’un style de programmation incorrect, un paramètre de type dans un type imbriqué peut masquer un paramètre membre ou de type déclaré dans le type externe.

Exemple :

class Outer<T>
{
    class Inner<T>                                  // Valid, hides Outer's T
    {
        public T t;                                 // Refers to Inner's T
    }
}

exemple de fin

15.3.10 Noms de membres réservés

15.3.10.1 Général

Pour faciliter l’implémentation au moment de l’exécution C# sous-jacente, pour chaque déclaration de membre source qui est une propriété, un événement ou un indexeur, l’implémentation réserve deux signatures de méthode basées sur le type de déclaration de membre, son nom et son type (§15.3.10.2, §15.3.10.3, §15.3.10.4). Il s’agit d’une erreur de compilation pour qu’un programme déclare un membre dont la signature correspond à une signature réservée par un membre déclaré dans la même étendue, même si l’implémentation au moment de l’exécution sous-jacente n’utilise pas ces réservations.

Les noms réservés n’introduisent pas de déclarations, ils ne participent donc pas à la recherche de membre. Toutefois, les signatures de méthode réservée associées à une déclaration participent à l’héritage (§15.3.4) et peuvent être masquées avec le new modificateur (§15.3.5).

Remarque : La réservation de ces noms sert à trois fins :

  1. Pour permettre à l’implémentation sous-jacente d’utiliser un identificateur ordinaire comme nom de méthode pour obtenir ou définir l’accès à la fonctionnalité de langage C#.
  2. Pour permettre à d’autres langues d’interagir à l’aide d’un identificateur ordinaire comme nom de méthode pour obtenir ou définir l’accès à la fonctionnalité de langage C#.
  3. Pour vous assurer que la source acceptée par un compilateur conforme est acceptée par une autre, en rendant les spécificités des noms de membres réservés cohérentes dans toutes les implémentations C#.

Note de fin

La déclaration d’un finaliseur (§15.13) entraîne également la réserve d’une signature (§15.3.10.5).

Certains noms sont réservés pour une utilisation en tant que noms de méthode d’opérateur (§15.3.10.6).

15.3.10.2 Noms de membres réservés aux propriétés

Pour une propriété P (§15.7) de type T, les signatures suivantes sont réservées :

T get_P();
void set_P(T value);

Les deux signatures sont réservées, même si la propriété est en lecture seule ou en écriture seule.

Exemple : dans le code suivant

class A
{
    public int P
    {
        get => 123;
    }
}

class B : A
{
    public new int get_P() => 456;

    public new void set_P(int value)
    {
    }
}

class Test
{
    static void Main()
    {
        B b = new B();
        A a = b;
        Console.WriteLine(a.P);
        Console.WriteLine(b.P);
        Console.WriteLine(b.get_P());
    }
}

Une classe A définit une propriété Pen lecture seule, réservant ainsi des signatures pour get_P et set_P des méthodes. A la classe B dérive de A ces deux signatures réservées et les masque. L’exemple produit la sortie :

123
123
456

exemple de fin

15.3.10.3 Noms de membres réservés aux événements

Pour un événement E (§15.8) de type Tdélégué, les signatures suivantes sont réservées :

void add_E(T handler);
void remove_E(T handler);

15.3.10.4 Noms de membres réservés aux indexeurs

Pour un indexeur (§15.9) de type T avec la liste Ldes paramètres, les signatures suivantes sont réservées :

T get_Item(L);
void set_Item(L, T value);

Les deux signatures sont réservées, même si l’indexeur est en lecture seule ou en écriture seule.

En outre, le nom Item du membre est réservé.

15.3.10.5 Noms de membres réservés aux finaliseurs

Pour une classe contenant un finaliseur (§15.13), la signature suivante est réservée :

void Finalize();

15.3.10.6 Noms de méthode réservés aux opérateurs

Les noms de méthode suivants sont réservés. Bien que de nombreux ont des opérateurs correspondants dans cette spécification, certains sont réservés pour une utilisation par les versions ultérieures, tandis que certains sont réservés à l’interopérabilité avec d’autres langues.

Nom de la méthode Opérateur C#
op_Addition + (binaire)
op_AdditionAssignment (réservé)
op_AddressOf (réservé)
op_Assign (réservé)
op_BitwiseAnd & (binaire)
op_BitwiseAndAssignment (réservé)
op_BitwiseOr \|
op_BitwiseOrAssignment (réservé)
op_CheckedAddition (réservé pour une utilisation ultérieure)
op_CheckedDecrement (réservé pour une utilisation ultérieure)
op_CheckedDivision (réservé pour une utilisation ultérieure)
op_CheckedExplicit (réservé pour une utilisation ultérieure)
op_CheckedIncrement (réservé pour une utilisation ultérieure)
op_CheckedMultiply (réservé pour une utilisation ultérieure)
op_CheckedSubtraction (réservé pour une utilisation ultérieure)
op_CheckedUnaryNegation (réservé pour une utilisation ultérieure)
op_Comma (réservé)
op_Decrement -- (préfixe et postfix)
op_Division /
op_DivisionAssignment (réservé)
op_Equality ==
op_ExclusiveOr ^
op_ExclusiveOrAssignment (réservé)
op_Explicit contrainte explicite (étroite)
op_False false
op_GreaterThan >
op_GreaterThanOrEqual >=
op_Implicit contrainte implicite (étendue)
op_Increment ++ (préfixe et postfix)
op_Inequality !=
op_LeftShift <<
op_LeftShiftAssignment (réservé)
op_LessThan <
op_LessThanOrEqual <=
op_LogicalAnd (réservé)
op_LogicalNot !
op_LogicalOr (réservé)
op_MemberSelection (réservé)
op_Modulus %
op_ModulusAssignment (réservé)
op_MultiplicationAssignment (réservé)
op_Multiply * (binaire)
op_OnesComplement ~
op_PointerDereference (réservé)
op_PointerToMemberSelection (réservé)
op_RightShift >>
op_RightShiftAssignment (réservé)
op_SignedRightShift (réservé)
op_Subtraction - (binaire)
op_SubtractionAssignment (réservé)
op_True true
op_UnaryNegation - (unaire)
op_UnaryPlus + (unaire)
op_UnsignedRightShift (réservé pour une utilisation ultérieure)
op_UnsignedRightShiftAssignment (réservé)

Constantes 15.4

Une constante est un membre de classe qui représente une valeur constante : valeur qui peut être calculée au moment de la compilation. Une constant_declaration introduit une ou plusieurs constantes d’un type donné.

constant_declaration
    : attributes? constant_modifier* 'const' type constant_declarators ';'
    ;

constant_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    ;

Un constant_declaration peut inclure un ensemble d’attributs (§22), un modificateur (new) et l’un des types autorisés d’accessibilité déclarée (§15.3.6). Les attributs et les modificateurs s’appliquent à tous les membres déclarés par la constant_declaration. Même si les constantes sont considérées comme des membres statiques, une constant_declaration n’exige ni n’autorise un static modificateur. Il s’agit d’une erreur pour que le même modificateur apparaisse plusieurs fois dans une déclaration constante.

Le type d’un constant_declaration spécifie le type des membres introduits par la déclaration. Le type est suivi d’une liste de constant_declarator s (§13.6.3), chacun d’entre eux introduitun nouveau membre. Une constant_declarator se compose d’un identificateur qui nomme le membre, suivi d’un jeton «= », suivi d’un constant_expression (§12.23) qui donne la valeur du membre.

Le type spécifié dans une déclaration constante doit être sbyte, byteshortushortintuint, long, , , ulongcharfloatdoubledecimalbool, stringun enum_type ou un reference_type. Chaque constant_expression génère une valeur du type cible ou d’un type qui peut être converti en type cible par une conversion implicite (§10.2).

Le type d’une constante doit être au moins aussi accessible que la constante elle-même (§7.5.5).

La valeur d’une constante est obtenue dans une expression à l’aide d’un simple_name (§12.8.4) ou d’un member_access (§12.8.7).

Une constante peut elle-même participer à un constant_expression. Ainsi, une constante peut être utilisée dans n’importe quelle construction qui nécessite une constant_expression.

Remarque : Les exemples de ces constructions incluent case des étiquettes, des instructions, goto caseenum des déclarations de membre, des attributs et d’autres déclarations constantes. Note de fin

Remarque : Comme décrit dans le §12.23, un constant_expression est une expression qui peut être entièrement évaluée au moment de la compilation. Étant donné que la seule façon de créer une valeur non null d’une reference_type autre que string d’appliquer l’opérateur new , et étant donné que l’opérateur new n’est pas autorisé dans un constant_expression, la seule valeur possible pour les constantes de reference_types autres que string l’est null. Note de fin

Lorsqu’un nom symbolique pour une valeur constante est souhaité, mais lorsque le type de cette valeur n’est pas autorisé dans une déclaration constante, ou lorsque la valeur ne peut pas être calculée au moment de la compilation par un constant_expression, un champ en lecture seule (§15.5.3) peut être utilisé à la place.

Remarque : Sémantique de contrôle de version et constreadonly différente (§15.5.3.3). Note de fin

Une déclaration constante qui déclare plusieurs constantes équivaut à plusieurs déclarations de constantes uniques avec les mêmes attributs, modificateurs et type.

Exemple :

class A
{
    public const double X = 1.0, Y = 2.0, Z = 3.0;
}

équivaut à :

class A
{
    public const double X = 1.0;
    public const double Y = 2.0;
    public const double Z = 3.0;
}

exemple de fin

Les constantes sont autorisées à dépendre d’autres constantes au sein du même programme tant que les dépendances ne sont pas d’une nature circulaire.

Exemple : dans le code suivant

class A
{
    public const int X = B.Z + 1;
    public const int Y = 10;
}

class B
{
    public const int Z = A.Y + 1;
}

un compilateur doit d’abord évaluer A.Y, puis évaluer B.Z, puis évaluer A.X, produire les valeurs 10, 11et 12.

exemple de fin

Les déclarations constantes peuvent dépendre de constantes d’autres programmes, mais ces dépendances ne sont possibles qu’dans une seule direction.

Exemple : en faisant référence à l’exemple ci-dessus, si A et B ont été déclarés dans des programmes distincts, il serait possible de A.X dépendre B.Z, mais B.Z ne peut pas dépendre simultanément A.Yde . exemple de fin

15.5 Champs

15.5.1 Général

Un champ est un membre qui représente une variable associée à un objet ou une classe. Une field_declaration introduit un ou plusieurs champs d’un type donné.

field_declaration
    : attributes? field_modifier* type variable_declarators ';'
    ;

field_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'readonly'
    | 'volatile'
    | unsafe_modifier   // unsafe code support
    ;

variable_declarators
    : variable_declarator (',' variable_declarator)*
    ;

variable_declarator
    : identifier ('=' variable_initializer)?
    ;

unsafe_modifier (§23.2) est disponible uniquement dans le code non sécurisé (§23).

Un field_declaration peut inclure un ensemble d’attributs (§22), un modificateur (new), une combinaison valide des quatre modificateurs d’accès (§15.3.6) et un static modificateur (§15.5.2). En outre, un field_declaration peut inclure un readonly modificateur (§15.5.3) ou un volatile modificateur (§15.5.4), mais pas les deux. Les attributs et les modificateurs s’appliquent à tous les membres déclarés par le field_declaration. Il s’agit d’une erreur pour que le même modificateur apparaisse plusieurs fois dans un field_declaration.

Le type d’un field_declaration spécifie le type des membres introduits par la déclaration. Le type est suivi d’une liste de variable_declarators, chacune d’entre elles introduit un nouveau membre. Un variable_declarator se compose d’un identificateur qui nomme ce membre, éventuellement suivi d’un jeton «= » et d’un variable_initializer (§15.5.6) qui donne la valeur initiale de ce membre.

Le type d’un champ doit être au moins aussi accessible que le champ lui-même (§7.5.5).

La valeur d’un champ est obtenue dans une expression à l’aide d’un simple_name (§12.8.4), d’un member_access (§12.8.7) ou d’un base_access (§12.8.15). La valeur d’un champ non lu en lecture seule est modifiée à l’aide d’une affectation (§12.21). La valeur d’un champ non luonly peut être obtenue et modifiée à l’aide d’opérateurs d’incrémentation et de décrémentation postfix (§12.8.16) et d’incrémentation de préfixe et d’opérateurs de décrémentation (§12.9.6).

Une déclaration de champ qui déclare plusieurs champs équivaut à plusieurs déclarations de champs uniques avec les mêmes attributs, modificateurs et type.

Exemple :

class A
{
    public static int X = 1, Y, Z = 100;
}

équivaut à :

class A
{
    public static int X = 1;
    public static int Y;
    public static int Z = 100;
}

exemple de fin

15.5.2 Champs statiques et d’instance

Lorsqu’une déclaration de champ inclut un static modificateur, les champs introduits par la déclaration sont des champs statiques. Lorsqu’aucun modificateur n’est static présent, les champs introduits par la déclaration sont des champs d’instance. Les champs statiques et les champs d’instance sont deux des différents types de variables (§9) pris en charge par C#, et ils sont parfois appelés variables statiques et variables d’instance, respectivement.

Comme expliqué dans le §15.3.8, chaque instance d’une classe contient un ensemble complet des champs d’instance de la classe, alors qu’il n’existe qu’un seul ensemble de champs statiques pour chaque classe non générique ou type construit fermé, quel que soit le nombre d’instances de la classe ou du type construit fermé.

15.5.3 Champs en lecture seule

15.5.3.1 Général

Lorsqu’un field_declaration inclut un readonly modificateur, les champs introduits par la déclaration sont des champs en lecture seule. Les affectations directes aux champs en lecture seule peuvent se produire dans le cadre de cette déclaration ou dans un constructeur d’instance ou dans un constructeur statique dans la même classe. (Un champ en lecture seule peut être affecté à plusieurs fois dans ces contextes.) Plus précisément, les affectations directes à un champ en lecture seule sont autorisées dans les contextes suivants :

  • Dans le variable_declarator qui introduit le champ (en incluant une variable_initializer dans la déclaration).
  • Pour un champ d’instance, dans les constructeurs d’instance de la classe qui contient la déclaration de champ ; pour un champ statique, dans le constructeur statique de la classe qui contient la déclaration de champ. Il s’agit également des seuls contextes dans lesquels il est valide de passer un champ en lecture seule en tant que paramètre de sortie ou de référence.

La tentative d’affectation d’un champ en lecture seule ou de la transmettre en tant que paramètre de sortie ou de référence dans un autre contexte est une erreur au moment de la compilation.

15.5.3.2 Utilisation de champs en lecture seule statiques pour les constantes

Un champ en lecture seule statique est utile lorsqu’un nom symbolique pour une valeur constante est souhaité, mais lorsque le type de la valeur n’est pas autorisé dans une déclaration const ou lorsque la valeur ne peut pas être calculée au moment de la compilation.

Exemple : dans le code suivant

public class Color
{
    public static readonly Color Black = new Color(0, 0, 0);
    public static readonly Color White = new Color(255, 255, 255);
    public static readonly Color Red = new Color(255, 0, 0);
    public static readonly Color Green = new Color(0, 255, 0);
    public static readonly Color Blue = new Color(0, 0, 255);

    private byte red, green, blue;

    public Color(byte r, byte g, byte b)
    {
        red = r;
        green = g;
        blue = b;
    }
}

les Blackmembres , , WhiteRedet GreenBlue les membres ne peuvent pas être déclarés en tant que membres const, car leurs valeurs ne peuvent pas être calculées au moment de la compilation. Toutefois, les static readonly déclarer à la place ont bien le même effet.

exemple de fin

15.5.3.3 Contrôle de version des constantes et des champs en lecture seule statique

Les constantes et les champs en lecture seule ont une sémantique de gestion de version binaire différente. Lorsqu’une expression fait référence à une constante, la valeur de la constante est obtenue au moment de la compilation, mais lorsqu’une expression fait référence à un champ en lecture seule, la valeur du champ n’est obtenue qu’au moment de l’exécution.

Exemple : Considérez une application qui se compose de deux programmes distincts :

namespace Program1
{
    public class Utils
    {
        public static readonly int x = 1;
    }
}

et

namespace Program2
{
    class Test
    {
        static void Main()
        {
            Console.WriteLine(Program1.Utils.X);
        }
    }
}

Les Program1 espaces de noms et Program2 les espaces de noms désignent deux programmes compilés séparément. Étant donné qu’elle Program1.Utils.X est déclarée en tant que static readonly champ, la sortie de la valeur par l’instruction n’est pas connue au moment de la compilation, mais elle est obtenue au moment de l’exécution Console.WriteLine . Par conséquent, si la valeur de la valeur est X modifiée et Program1 recompilée, l’instruction Console.WriteLine génère la nouvelle valeur même si Program2 elle n’est pas recompilée. Toutefois, il s’agissait X d’une constante, la valeur de l’objet aurait été obtenue au moment X de Program2 la compilation et ne serait pas affectée par les modifications Program1 apportées jusqu’à Program2 ce qu’elle soit recompilée.

exemple de fin

15.5.4 Champs volatiles

Lorsqu’un field_declaration inclut un modificateur, les champs introduits par cette déclaration sont des champs volatilesvolatile. Pour les champs non volatiles, les techniques d’optimisation qui réorganisent les instructions peuvent entraîner des résultats inattendus et imprévisibles dans des programmes multithreads qui accèdent aux champs sans synchronisation, tels que ceux fournis par le lock_statement (§13.13). Ces optimisations peuvent être effectuées par le compilateur, par le système d’exécution ou par le matériel. Pour les champs volatiles, ces optimisations de réorganisation sont limitées :

  • Une lecture d’un champ volatile est appelée lecture volatile. Une lecture volatile a une « sémantique d’acquisition » ; autrement dit, il est garanti qu’il se produise avant toute référence à la mémoire qui se produit après celle-ci dans la séquence d’instructions.
  • Une écriture d’un champ volatile est appelée écriture volatile. Une écriture volatile a la « sémantique de mise en production » ; autrement dit, il est garanti qu’il se produise après toutes les références de mémoire avant l’instruction d’écriture dans la séquence d’instructions.

Ces restrictions garantissent que tous les threads observent les écritures volatiles effectuées par un autre thread dans l’ordre dans lequel elles ont été effectuées. Une implémentation conforme n’est pas nécessaire pour fournir un ordre total d’écritures volatiles, comme indiqué à partir de tous les threads d’exécution. Le type d’un champ volatile doit être l’un des éléments suivants :

  • Un reference_type.
  • Un type_parameter qui est connu comme un type de référence (§15.2.5).
  • byteType , sbyteshortushortintuintcharfloatboolSystem.IntPtrou .System.UIntPtr
  • Un enum_type ayant un type enum_base de byte, , sbyteshort, ushort, , intou uint.

Exemple : l’exemple

class Test
{
    public static int result;
    public static volatile bool finished;

    static void Thread2()
    {
        result = 143;
        finished = true;
    }

    static void Main()
    {
        finished = false;

        // Run Thread2() in a new thread
        new Thread(new ThreadStart(Thread2)).Start();    

        // Wait for Thread2() to signal that it has a result
        // by setting finished to true.
        for (;;)
        {
            if (finished)
            {
                Console.WriteLine($"result = {result}");
                return;
            }
        }
    }
}

génère cette sortie :

result = 143

Dans cet exemple, la méthode Main démarre un nouveau thread qui exécute la méthode Thread2. Cette méthode stocke une valeur dans un champ non volatile appelé result, puis stocke true dans le champ finishedvolatile. Le thread principal attend que le champ finished soit défini truesur , puis lit le champ result. Étant finished donné qu’il a été déclaré volatile, le thread principal doit lire la valeur 143 du champ result. Si le champ finished n’avait pas été déclaré volatile, il serait permis au magasin result d’être visible par le thread principal après le magasin finishedet, par conséquent, pour que le thread principal lise la valeur 0 à partir du champ result. La déclaration finished en tant que volatile champ empêche toute telle incohérence.

exemple de fin

15.5.5 Initialisation de champ

La valeur initiale d’un champ, qu’il s’agisse d’un champ statique ou d’un champ d’instance, est la valeur par défaut (§9.3) du type du champ. Il n’est pas possible d’observer la valeur d’un champ avant que cette initialisation par défaut ne s’est produite, et un champ n’est donc jamais « non initialisé ».

Exemple : l’exemple

class Test
{
    static bool b;
    int i;

    static void Main()
    {
        Test t = new Test();
        Console.WriteLine($"b = {b}, i = {t.i}");
    }
}

génère la sortie

b = False, i = 0

car b et i sont automatiquement initialisés en valeurs par défaut.

exemple de fin

15.5.6 Initialiseurs de variables

15.5.6.1 Général

Les déclarations de champ peuvent inclure des variable_initializers. Pour les champs statiques, les initialiseurs de variables correspondent aux instructions d’affectation exécutées pendant l’initialisation de classe. Par exemple, les initialiseurs de variables correspondent aux instructions d’affectation qui sont exécutées lorsqu’une instance de la classe est créée.

Exemple : l’exemple

class Test
{
    static double x = Math.Sqrt(2.0);
    int i = 100;
    string s = "Hello";

    static void Main()
    {
        Test a = new Test();
        Console.WriteLine($"x = {x}, i = {a.i}, s = {a.s}");
    }
}

génère la sortie

x = 1.4142135623730951, i = 100, s = Hello

parce qu’une affectation à x effectuer se produit lorsque les initialiseurs de champs statiques s’exécutent et is se produisent lorsque les initialiseurs de champ d’instance s’exécutent.

exemple de fin

L’initialisation de valeur par défaut décrite dans le §15.5.5 se produit pour tous les champs, y compris les champs qui ont des initialiseurs de variables. Par conséquent, lorsqu’une classe est initialisée, tous les champs statiques de cette classe sont d’abord initialisés à leurs valeurs par défaut, puis les initialiseurs de champs statiques sont exécutés dans l’ordre textuel. De même, lorsqu’une instance d’une classe est créée, tous les champs d’instance de cette instance sont d’abord initialisés à leurs valeurs par défaut, puis les initialiseurs de champ d’instance sont exécutés dans l’ordre textuel. Lorsqu’il existe des déclarations de champ dans plusieurs déclarations de type partiel pour le même type, l’ordre des parties n’est pas spécifié. Toutefois, dans chaque partie, les initialiseurs de champ sont exécutés dans l’ordre.

Il est possible que les champs statiques avec des initialiseurs de variables soient observés dans leur état de valeur par défaut.

Exemple : Toutefois, cela est fortement déconseillé en matière de style. L’exemple

class Test
{
    static int a = b + 1;
    static int b = a + 1;

    static void Main()
    {
        Console.WriteLine($"a = {a}, b = {b}");
    }
}

présente ce comportement. Malgré les définitions circulaires et ab, le programme est valide. Elle entraîne la sortie

a = 1, b = 2

étant donné que les champs a statiques et b sont initialisés sur 0 (la valeur par défaut pour int) avant l’exécution de leurs initialiseurs. Lorsque l’initialiseur pour a les exécutions est b égal à zéro, et est donc a initialisé sur 1. Lorsque l’initialiseur pour b les exécutions, la valeur d’un est déjà 1, et elle est donc b initialisée sur 2.

exemple de fin

15.5.6.2 Initialisation de champ statique

Les initialiseurs de variable de champ statique d’une classe correspondent à une séquence d’affectations exécutées dans l’ordre textuel dans lequel elles apparaissent dans la déclaration de classe (§15.5.6.1). Dans une classe partielle, la signification de « ordre textuel » est spécifiée par le §15.5.6.1. Si un constructeur statique (§15.12) existe dans la classe, l’exécution des initialiseurs de champs statiques se produit immédiatement avant l’exécution de ce constructeur statique. Sinon, les initialiseurs de champs statiques sont exécutés à un moment dépendant de l’implémentation avant la première utilisation d’un champ statique de cette classe.

Exemple : l’exemple

class Test
{
    static void Main()
    {
        Console.WriteLine($"{B.Y} {A.X}");
    }

    public static int F(string s)
    {
        Console.WriteLine(s);
        return 1;
    }
}

class A
{
    public static int X = Test.F("Init A");
}

class B
{
    public static int Y = Test.F("Init B");
}

peut produire l’une ou l’autre des sorties :

Init A
Init B
1 1

ou la sortie :

Init B
Init A
1 1

étant donné que l’exécution du Xinitialiseur de 's initialiseur et 's initialiseur peut se produire dans l’un ou Yl’autre ordre ; elles ne sont limitées qu’à se produire avant les références à ces champs. Toutefois, dans l’exemple :

class Test
{
    static void Main()
    {
        Console.WriteLine($"{B.Y} {A.X}");
    }

    public static int F(string s)
    {
        Console.WriteLine(s);
        return 1;
    }
}

class A
{
    static A() {}
    public static int X = Test.F("Init A");
}

class B
{
    static B() {}
    public static int Y = Test.F("Init B");
}

la sortie doit être :

Init B
Init A
1 1

étant donné que les règles pour le moment où les constructeurs statiques s’exécutent (comme définis dans le §15.12) fournissent que Ble constructeur statique (et donc Bles initialiseurs de champs statiques) s’exécutent avant Ale constructeur statique et les initialiseurs de champs.

exemple de fin

15.5.6.3 Initialisation du champ d’instance

Les initialiseurs de variable de champ d’instance d’une classe correspondent à une séquence d’affectations exécutées immédiatement lors de l’entrée à l’un des constructeurs d’instance (§15.11.3) de cette classe. Dans une classe partielle, la signification de « ordre textuel » est spécifiée par le §15.5.6.1. Les initialiseurs de variables sont exécutés dans l’ordre textuel dans lequel ils apparaissent dans la déclaration de classe (§15.5.6.1). Le processus de création et d’initialisation de l’instance de classe est décrit plus loin dans le §15.11.

Un initialiseur de variable pour un champ d’instance ne peut pas référencer l’instance en cours de création. Par conséquent, il s’agit d’une erreur au moment de la compilation à référencer this dans un initialiseur de variable, car il s’agit d’une erreur au moment de la compilation pour qu’un initialiseur de variable référence n’importe quel membre d’instance via un simple_name.

Exemple : dans le code suivant

class A
{
    int x = 1;
    int y = x + 1;     // Error, reference to instance member of this
}

l’initialiseur de variable pour y obtenir une erreur au moment de la compilation, car elle fait référence à un membre de l’instance en cours de création.

exemple de fin

15.6 Méthodes

15.6.1 Général

Une méthode est un membre qui implémente un calcul ou une action qui peut être effectuée par un objet ou une classe. Les méthodes sont déclarées à l’aide de method_declarations :

method_declaration
    : attributes? method_modifiers return_type method_header method_body
    | attributes? ref_method_modifiers ref_kind ref_return_type method_header
      ref_method_body
    ;

method_modifiers
    : method_modifier* 'partial'?
    ;

ref_kind
    : 'ref'
    | 'ref' 'readonly'
    ;

ref_method_modifiers
    : ref_method_modifier*
    ;

method_header
    : member_name '(' parameter_list? ')'
    | member_name type_parameter_list '(' parameter_list? ')'
      type_parameter_constraints_clause*
    ;

method_modifier
    : ref_method_modifier
    | 'async'
    ;

ref_method_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

return_type
    : ref_return_type
    | 'void'
    ;

ref_return_type
    : type
    ;

member_name
    : identifier
    | interface_type '.' identifier
    ;

method_body
    : block
    | '=>' null_conditional_invocation_expression ';'
    | '=>' expression ';'
    | ';'
    ;

ref_method_body
    : block
    | '=>' 'ref' variable_reference ';'
    | ';'
    ;

Notes de grammaire :

  • unsafe_modifier (§23.2) est disponible uniquement dans le code non sécurisé (§23).
  • lors de la reconnaissance d’une method_body si les alternatives de null_conditional_invocation_expression et d’expression sont applicables, l’ancien doit être choisi.

Remarque : Le chevauchement et la priorité entre les solutions de remplacement ici sont uniquement à des fins descriptives ; les règles de grammaire peuvent être élaborées pour supprimer le chevauchement. ANTLR, et d’autres systèmes de grammaire, adoptent la même commodité et donc method_body a automatiquement la sémantique spécifiée. Note de fin

Un method_declaration peut inclure un ensemble d’attributs (§22) et l’un des types autorisés d’accessibilité déclarée (§15.3.6), le new (§15.3.5), static (§15.6.3), (§15.6.3), virtual (§15 .6.4), override (§15.6.5), sealed (§15.6.6), abstract (§15.6.7), extern (§15.6.8) et async (§15.15) modificateurs.

Une déclaration a une combinaison valide de modificateurs si toutes les valeurs suivantes sont vraies :

  • La déclaration inclut une combinaison valide de modificateurs d’accès (§15.3.6).
  • La déclaration n’inclut pas le même modificateur plusieurs fois.
  • La déclaration inclut au plus l’un des modificateurs suivants : static, virtual, et override.
  • La déclaration inclut au maximum l’un des modificateurs suivants : new et override.
  • Si la déclaration inclut le abstract modificateur, la déclaration n’inclut aucun des modificateurs suivants : static, , virtual, sealedou extern.
  • Si la déclaration inclut le private modificateur, la déclaration n’inclut aucun des modificateurs suivants : virtual, overrideou abstract.
  • Si la déclaration inclut le sealed modificateur, la déclaration inclut également le override modificateur.
  • Si la déclaration inclut le partial modificateur, elle n’inclut aucun des modificateurs suivants : new, , publicprotected, internal, , private, virtual, , , sealed, overrideabstractou extern.

Les méthodes sont classées selon ce qu’elles retournent, le cas échéant :

  • Si ref elle est présente, la méthode est renvoyée par ref et renvoie une référence de variable, qui est éventuellement en lecture seule ;
  • Sinon, si return_type est void, la méthode est renvoie-non-valeur et ne retourne pas de valeur ;
  • Sinon, la méthode est retournée par valeur et retourne une valeur.

La return_type d’une déclaration de méthode de retour par valeur ou de retour sans valeur spécifie le type du résultat, le cas échéant, retourné par la méthode. Seule une méthode de retour sans valeur peut inclure le partial modificateur (§15.6.9). Si la déclaration inclut le async modificateur, return_type doit être void ou la méthode renvoie par valeur et le type de retour est un type de tâche (§15.15.1).

La ref_return_type d’une déclaration de méthode returns-by-ref spécifie le type de la variable référencée par la variable_reference retournée par la méthode.

Une méthode générique est une méthode dont la déclaration inclut une type_parameter_list. Cela spécifie les paramètres de type de la méthode. Les type_parameter_constraints_clausefacultatifs spécifient les contraintes pour les paramètres de type.

Une method_declaration générique pour une implémentation de membre d’interface explicite n’a aucune type_parameter_constraints_clauses ; la déclaration hérite des contraintes des contraintes sur la méthode d’interface.

De même, une déclaration de méthode avec le override modificateur n’a pas de type_parameter_constraints_clauseet les contraintes des paramètres de type de la méthode sont héritées de la méthode virtuelle en cours de substitution.

La member_name spécifie le nom de la méthode. Sauf si la méthode est une implémentation explicite de membre d’interface (§18.6.2), le member_name est simplement un identificateur.

Pour une implémentation de membre d’interface explicite, la member_name se compose d’un interface_type suivi d’un «. » et d’un identificateur. Dans ce cas, la déclaration ne doit inclure aucun modificateur autre que (éventuellement) extern ou async.

Le parameter_list facultatif spécifie les paramètres de la méthode (§15.6.2).

Les return_type ou ref_return_type, et chacun des types référencés dans la parameter_list d’une méthode, doit être au moins aussi accessible que la méthode elle-même (§7.5.5).

La method_body d’une méthode returns-by-value ou returns-no-value est soit un point-virgule, un corps de bloc ou un corps d’expression. Un corps de bloc se compose d’un bloc, qui spécifie les instructions à exécuter lorsque la méthode est appelée. Un corps d’expression se compose d’un =>null_conditional_invocation_expression ou d’une expression, et d’un point-virgule, et indique une expression unique à effectuer lorsque la méthode est appelée.

Pour les méthodes abstraites et externes, la method_body se compose simplement d’un point-virgule. Pour les méthodes partielles, la method_body peut se composer d’un point-virgule, d’un corps de bloc ou d’un corps d’expression. Pour toutes les autres méthodes, la method_body est un corps de bloc ou un corps d’expression.

Si le method_body se compose d’un point-virgule, la déclaration n’inclut pas le async modificateur.

La ref_method_body d’une méthode returns-by-ref est soit un point-virgule, un corps de bloc ou un corps d’expression. Un corps de bloc se compose d’un bloc, qui spécifie les instructions à exécuter lorsque la méthode est appelée. Un corps d’expression se compose d’un =>variable_referenceref, d’un point-virgule et d’un seul variable_reference à évaluer quand la méthode est appelée.

Pour les méthodes abstraites et externes, le ref_method_body se compose simplement d’un point-virgule ; pour toutes les autres méthodes, le ref_method_body est soit un corps de bloc, soit un corps d’expression.

Le nom, le nombre de paramètres de type et la liste de paramètres d’une méthode définissent la signature (§7.6) de la méthode. Plus précisément, la signature d’une méthode se compose de son nom, du nombre de ses paramètres de type et du nombre, des parameter_mode_modifiers (§15.6.2.1) et des types de ses paramètres. Le type de retour ne fait pas partie de la signature d’une méthode, ni les noms des paramètres, les noms des paramètres de type ou les contraintes. Lorsqu’un type de paramètre fait référence à un paramètre de type de la méthode, la position ordinale du paramètre de type (et non le nom du paramètre de type) est utilisée pour l’équivalence de type.

Le nom d’une méthode diffère des noms de toutes les autres méthodes non déclarées dans la même classe. En outre, la signature d’une méthode diffère des signatures de toutes les autres méthodes déclarées dans la même classe, et deux méthodes déclarées dans la même classe ne doivent pas avoir de signatures qui diffèrent uniquement par in, outet ref.

Les type_parameter de la méthode sont dans l’étendue tout au long de la method_declaration et peuvent être utilisées pour former des types tout au long de cette étendue dans return_type ou ref_return_type, method_body ou ref_method_body et type_parameter_constraints_clause, mais pas dans les attributs.

Tous les paramètres et paramètres de type doivent avoir des noms différents.

15.6.2 Paramètres de méthode

15.6.2.1 Général

Les paramètres d’une méthode, le cas échéant, sont déclarés par la parameter_list de la méthode.

parameter_list
    : fixed_parameters
    | fixed_parameters ',' parameter_array
    | parameter_array
    ;

fixed_parameters
    : fixed_parameter (',' fixed_parameter)*
    ;

fixed_parameter
    : attributes? parameter_modifier? type identifier default_argument?
    ;

default_argument
    : '=' expression
    ;

parameter_modifier
    : parameter_mode_modifier
    | 'this'
    ;

parameter_mode_modifier
    : 'ref'
    | 'out'
    | 'in'
    ;

parameter_array
    : attributes? 'params' array_type identifier
    ;

La liste des paramètres se compose d’un ou plusieurs paramètres séparés par des virgules dont seuls les derniers peuvent être un parameter_array.

Un fixed_parameter se compose d’un ensemble facultatif d’attributs(§22), d’un type facultatif in, outou refthismodificateur ; d’un type ; d’un identificateur ; et d’un default_argument facultatif. Chaque fixed_parameter déclare un paramètre du type donné avec le nom donné. Le this modificateur désigne la méthode comme méthode d’extension et est autorisé uniquement sur le premier paramètre d’une méthode statique dans une classe statique non générique et non imbriquée. Si le paramètre est un struct type ou un paramètre de type limité à unstruct, le this modificateur peut être combiné avec le modificateur ou ref le in modificateur, mais pas le out modificateur. Les méthodes d’extension sont décrites plus loin dans le §15.6.10. Un fixed_parameter avec un default_argument est appelé paramètre facultatif, tandis qu’un fixed_parameter sans default_argument est un paramètre obligatoire. Un paramètre obligatoire n’apparaît pas après un paramètre facultatif dans un parameter_list.

Un paramètre avec un modificateur ou un refmodificateur ne peut pas avoir de out.this Un paramètre d’entrée peut avoir une default_argument. L’expression d’un default_argument doit être l’une des suivantes :

  • un constant_expression
  • expression du formulaire new S()S est un type valeur
  • expression du formulaire default(S)S est un type valeur

L’expression doit être implicitement convertible par une conversion d’identité ou nullable vers le type du paramètre.

Si des paramètres facultatifs se produisent dans une déclaration de méthode partielle (§15.6.9), une implémentation explicite de membre d’interface (§18.6.2), une déclaration d’indexeur à paramètre unique (§) 15.9), ou dans une déclaration d’opérateur (§15.10.1) un compilateur doit donner un avertissement, car ces membres ne peuvent jamais être appelés d’une manière qui autorise l’omission des arguments.

Un parameter_array se compose d’un ensemble facultatif d’attributs(§22), d’un modificateur, d’un params et d’un identificateur. Un tableau de paramètres déclare un paramètre unique du type de tableau donné avec le nom donné. La array_type d’un tableau de paramètres doit être un type de tableau unidimensionnel (§17.2). Dans un appel de méthode, un tableau de paramètres autorise la spécification d’un seul argument du type de tableau donné, ou permet de spécifier zéro ou plusieurs arguments du type d’élément de tableau. Les tableaux de paramètres sont décrits plus loin dans le §15.6.2.4.

Une parameter_array peut se produire après un paramètre facultatif, mais ne peut pas avoir de valeur par défaut : l’omission d’arguments pour un parameter_array entraînerait plutôt la création d’un tableau vide.

Exemple : L’exemple suivant illustre différents types de paramètres :

void M<T>(
    ref int i,
    decimal d,
    bool b = false,
    bool? n = false,
    string s = "Hello",
    object o = null,
    T t = default(T),
    params int[] a
) { }

Dans le parameter_list pour , est un paramètre obligatoire, M est un paramètre de valeur obligatoire, irefet db est des paramètres de valeur facultatifs et s est un tableau de paramètres. ota

exemple de fin

Une déclaration de méthode crée un espace de déclaration distinct (§7.3) pour les paramètres et les paramètres de type. Les noms sont introduits dans cet espace de déclaration par la liste des paramètres de type et la liste des paramètres de la méthode. Le corps de la méthode, le cas échéant, est considéré comme imbriqué dans cet espace de déclaration. Il s’agit d’une erreur pour que deux membres d’un espace de déclaration de méthode aient le même nom.

Un appel de méthode (§12.8.10.2) crée une copie, spécifique à cet appel, des paramètres et des variables locales de la méthode, et la liste d’arguments de l’appel affecte des valeurs ou des références de variables aux paramètres nouvellement créés. Dans le bloc d’une méthode, les paramètres peuvent être référencés par leurs identificateurs dans simple_name expressions (§12.8.4).

Les types de paramètres suivants existent :

Remarque : Comme décrit dans le §7.6, les inmodificateurs et out les refmodificateurs font partie de la signature d’une méthode, mais le params modificateur n’est pas. Note de fin

15.6.2.2 Paramètres de valeur

Un paramètre déclaré sans modificateur est un paramètre de valeur. Un paramètre valeur est une variable locale qui obtient sa valeur initiale à partir de l’argument correspondant fourni dans l’appel de méthode.

Pour connaître les règles d’affectation définitive, consultez le §9.2.5.

L’argument correspondant dans un appel de méthode doit être une expression implicitement convertible (§10.2) au type de paramètre.

Une méthode est autorisée à affecter de nouvelles valeurs à un paramètre de valeur. Ces affectations affectent uniquement l’emplacement de stockage local représenté par le paramètre valeur , ils n’ont aucun effet sur l’argument réel donné dans l’appel de méthode.

15.6.2.3 Paramètres de référence

15.6.2.3.1 Général

Les paramètres d’entrée, de sortie et de référence sont des paramètresde référence. Un paramètre de référence est une variable de référence locale (§9.7) ; le référentiel initial est obtenu à partir de l’argument correspondant fourni dans l’appel de méthode.

Remarque : Le référentiel d’un paramètre de référence peut être modifié à l’aide de l’opérateur ref assignment (= ref).

Lorsqu’un paramètre est un paramètre de référence, l’argument correspondant d’une invocation de méthode doit se composer du mot clé correspondant, inou , suivi refd’un out (§9.5) du même type que le paramètre. Toutefois, lorsque le paramètre est un in paramètre, l’argument peut être une expression pour laquelle une conversion implicite (§10.2) existe de cette expression d’argument au type du paramètre correspondant.

Les paramètres de référence ne sont pas autorisés sur les fonctions déclarées en tant qu’itérateur (§15.14) ou de fonction asynchrone (§15.15).

Dans une méthode qui accepte plusieurs paramètres de référence, il est possible que plusieurs noms représentent le même emplacement de stockage.

15.6.2.3.2 Paramètres d’entrée

Un paramètre déclaré avec un in modificateur est un paramètre d’entrée. L’argument correspondant à un paramètre d’entrée est soit une variable existante au point de l’appel de méthode, soit une variable créée par l’implémentation (§12.6.2.3) dans l’appel de méthode. Pour connaître les règles d’affectation définitive, consultez le §9.2.8.

Il s’agit d’une erreur au moment de la compilation pour modifier la valeur d’un paramètre d’entrée.

Remarque : L’objectif principal des paramètres d’entrée est d’améliorer l’efficacité. Lorsque le type d’un paramètre de méthode est un struct volumineux (en termes de mémoire requise), il est utile d’éviter de copier la valeur entière de l’argument lors de l’appel de la méthode. Les paramètres d’entrée permettent aux méthodes de faire référence à des valeurs existantes en mémoire, tout en offrant une protection contre les modifications indésirables apportées à ces valeurs. Note de fin

15.6.2.3.3 Paramètres de référence

Un paramètre déclaré avec un ref modificateur est un paramètre de référence. Pour connaître les règles d’affectation définitive, consultez le §9.2.6.

Exemple : l’exemple

class Test
{
    static void Swap(ref int x, ref int y)
    {
        int temp = x;
        x = y;
        y = temp;
    }

    static void Main()
    {
        int i = 1, j = 2;
        Swap(ref i, ref j);
        Console.WriteLine($"i = {i}, j = {j}");
    }
}

génère la sortie

i = 2, j = 1

Pour l’appel de Swap in Main, x représente i et y représente j. Ainsi, l’appel a pour effet d’échanger les valeurs de i et j.

exemple de fin

Exemple : dans le code suivant

class A
{
    string s;
    void F(ref string a, ref string b)
    {
        s = "One";
        a = "Two";
        b = "Three";
    }

    void G()
    {
        F(ref s, ref s);
    }
}

l’appel d’in FG passe une référence à la fois pour s les deux a et b. Ainsi, pour cet appel, les noms s, aet b tous font référence au même emplacement de stockage, et les trois affectations modifient tous le champ sd’instance .

exemple de fin

Pour un type, dans une struct méthode d’instance, l’accesseur d’instance (§12.2.1) ou le constructeur d’instance avec un initialiseur de constructeur, le this mot clé se comporte exactement comme un paramètre de référence du type de struct (§12.8.14).

Paramètres de sortie 15.6.2.3.4

Un paramètre déclaré avec un out modificateur est un paramètre de sortie. Pour connaître les règles d’affectation définitive, consultez le §9.2.7.

Une méthode déclarée comme méthode partielle (§15.6.9) ne doit pas avoir de paramètres de sortie.

Remarque : Les paramètres de sortie sont généralement utilisés dans les méthodes qui produisent plusieurs valeurs de retour. Note de fin

Exemple :

class Test
{
    static void SplitPath(string path, out string dir, out string name)
    {
        int i = path.Length;
        while (i > 0)
        {
            char ch = path[i - 1];
            if (ch == '\\' || ch == '/' || ch == ':')
            {
                break;
            }
            i--;
        }
        dir = path.Substring(0, i);
        name = path.Substring(i);
    }

    static void Main()
    {
        string dir, name;
        SplitPath(@"c:\Windows\System\hello.txt", out dir, out name);
        Console.WriteLine(dir);
        Console.WriteLine(name);
    }
}

L’exemple produit la sortie :

c:\Windows\System\
hello.txt

Notez que les variables et dir les name variables peuvent être non attribuées avant qu’elles ne soient passées à SplitPath, et qu’elles sont considérées comme définitivement affectées après l’appel.

exemple de fin

15.6.2.4 Tableaux de paramètres

Un paramètre déclaré avec un params modificateur est un tableau de paramètres. Si une liste de paramètres inclut un tableau de paramètres, il s’agit du dernier paramètre de la liste et il doit être d’un type de tableau unidimensionnel.

Exemple : les types string[] et string[][] peuvent être utilisés comme type d’un tableau de paramètres, mais le type string[,] ne peut pas. exemple de fin

Remarque : Il n’est pas possible de combiner le params modificateur avec les modificateurs in, outou ref. Note de fin

Un tableau de paramètres permet aux arguments d’être spécifiés de l’une des deux manières d’un appel de méthode :

  • L’argument donné pour un tableau de paramètres peut être une expression unique qui est implicitement convertible (§10.2) en type de tableau de paramètres. Dans ce cas, le tableau de paramètres agit exactement comme un paramètre de valeur.
  • L’appel peut également spécifier zéro ou plusieurs arguments pour le tableau de paramètres, où chaque argument est une expression implicitement convertible (§10.2) vers le type d’élément du tableau de paramètres. Dans ce cas, l’appel crée une instance du type de tableau de paramètres avec une longueur correspondant au nombre d’arguments, initialise les éléments de l’instance de tableau avec les valeurs d’argument données et utilise l’instance de tableau nouvellement créée comme argument réel.

À l’exception de l’autorisation d’un nombre variable d’arguments dans un appel, un tableau de paramètres équivaut précisément à un paramètre valeur (§15.6.2.2) du même type.

Exemple : l’exemple

class Test
{
    static void F(params int[] args)
    {
        Console.Write($"Array contains {args.Length} elements:");
        foreach (int i in args)
        {
            Console.Write($" {i}");
        }
        Console.WriteLine();
    }

    static void Main()
    {
        int[] arr = {1, 2, 3};
        F(arr);
        F(10, 20, 30, 40);
        F();
    }
}

génère la sortie

Array contains 3 elements: 1 2 3
Array contains 4 elements: 10 20 30 40
Array contains 0 elements:

Le premier appel de F simplement passer le tableau arr en tant que paramètre valeur. Le deuxième appel de F crée automatiquement un élément à quatre éléments avec les valeurs d’élément int[] données et transmet cette instance de tableau en tant que paramètre de valeur. De même, le troisième appel de F crée un élément int[] zéro et passe cette instance en tant que paramètre valeur. Les deuxième et troisième appels sont exactement équivalents à l’écriture :

F(new int[] {10, 20, 30, 40});
F(new int[] {});

exemple de fin

Lors de l’exécution d’une résolution de surcharge, une méthode avec un tableau de paramètres peut être applicable, sous sa forme normale ou dans sa forme développée (§12.6.4.2). La forme développée d’une méthode est disponible uniquement si la forme normale de la méthode n’est pas applicable et uniquement si une méthode applicable avec la même signature que le formulaire développé n’est pas déjà déclarée dans le même type.

Exemple : l’exemple

class Test
{
    static void F(params object[] a) =>
        Console.WriteLine("F(object[])");

    static void F() =>
        Console.WriteLine("F()");

    static void F(object a0, object a1) =>
        Console.WriteLine("F(object,object)");

    static void Main()
    {
        F();
        F(1);
        F(1, 2);
        F(1, 2, 3);
        F(1, 2, 3, 4);
    }
}

génère la sortie

F()
F(object[])
F(object,object)
F(object[])
F(object[])

Dans l’exemple, deux des formes développées possibles de la méthode avec un tableau de paramètres sont déjà incluses dans la classe en tant que méthodes régulières. Ces formulaires développés ne sont donc pas pris en compte lors de l’exécution d’une résolution de surcharge, et les appels de première et troisième méthode sélectionnent donc les méthodes régulières. Lorsqu’une classe déclare une méthode avec un tableau de paramètres, il n’est pas rare d’inclure également certaines des formes développées comme méthodes régulières. Ainsi, il est possible d’éviter l’allocation d’une instance de tableau qui se produit lorsqu’une forme développée d’une méthode avec un tableau de paramètres est appelée.

exemple de fin

Un tableau est un type de référence, de sorte que la valeur passée pour un tableau de paramètres peut être null.

Exemple : l’exemple :

class Test
{
    static void F(params string[] array) =>
        Console.WriteLine(array == null);

    static void Main()
    {
        F(null);
        F((string) null);
    }
}

génère cette sortie :

True
False

Le deuxième appel produit False , car il équivaut à F(new string[] { null }) passer un tableau contenant une référence null unique.

exemple de fin

Lorsque le type d’un tableau de paramètres est object[], une ambiguïté potentielle se produit entre la forme normale de la méthode et le formulaire développé pour un seul object paramètre. La raison de l’ambiguïté est qu’un object[] est lui-même implicitement convertible en type object. Toutefois, l’ambiguïté ne pose aucun problème, car elle peut être résolue en insérant un cast si nécessaire.

Exemple : l’exemple

class Test
{
    static void F(params object[] args)
    {
        foreach (object o in args)
        {
            Console.Write(o.GetType().FullName);
            Console.Write(" ");
        }
        Console.WriteLine();
    }

    static void Main()
    {
        object[] a = {1, "Hello", 123.456};
        object o = a;
        F(a);
        F((object)a);
        F(o);
        F((object[])o);
    }
}

génère la sortie

System.Int32 System.String System.Double
System.Object[]
System.Object[]
System.Int32 System.String System.Double

Dans les premiers et derniers appels de F, la forme normale est F applicable, car une conversion implicite existe du type d’argument au type de paramètre (les deux sont de type object[]). Par conséquent, la résolution de surcharge sélectionne la forme normale de F, et l’argument est passé en tant que paramètre de valeur régulière. Dans les deuxième et troisième appels, la forme normale n’est F pas applicable, car aucune conversion implicite n’existe du type d’argument vers le type de paramètre (le type object ne peut pas être converti implicitement en type object[]). Toutefois, la forme développée est F applicable, de sorte qu’elle est sélectionnée par la résolution de surcharge. Par conséquent, un élément object[] unique est créé par l’appel, et l’élément unique du tableau est initialisé avec la valeur d’argument donnée (qui est elle-même une référence à un object[]).

exemple de fin

15.6.3 Méthodes statiques et d’instance

Lorsqu’une déclaration de méthode inclut un static modificateur, cette méthode est considérée comme une méthode statique. Lorsqu’aucun modificateur n’est static présent, la méthode est considérée comme une méthode d’instance.

Une méthode statique ne fonctionne pas sur une instance spécifique, et il s’agit d’une erreur au moment de la compilation à this laquelle faire référence dans une méthode statique.

Une méthode d’instance fonctionne sur une instance donnée d’une classe, et cette instance est accessible en tant que this (§12.8.14).

Les différences entre les membres statiques et d’instance sont abordées plus loin dans le §15.3.8.

Méthodes virtuelles 15.6.4

Lorsqu’une déclaration de méthode d’instance inclut un modificateur virtuel, cette méthode est considérée comme une méthode virtuelle. Lorsqu’aucun modificateur virtuel n’est présent, la méthode est considérée comme une méthode non virtuelle.

L’implémentation d’une méthode non virtuelle est invariante : l’implémentation est identique si la méthode est appelée sur une instance de la classe dans laquelle elle est déclarée ou une instance d’une classe dérivée. En revanche, l’implémentation d’une méthode virtuelle peut être remplacée par des classes dérivées. Le processus de substitution de l’implémentation d’une méthode virtuelle héritée est appelé substitution de cette méthode (§15.6.5).

Dans un appel de méthode virtuelle, le type d’exécution de l’instance pour laquelle cet appel a lieu détermine l’implémentation réelle de la méthode à appeler. Dans un appel de méthode non virtuelle, le type de compilation de l’instance est le facteur déterminant. En termes précis, lorsqu’une méthode nommée N est appelée avec une liste A d’arguments sur une instance avec un type de compilation et un type CR d’exécution (où R est l’une C ou une classe dérivée), Cl’appel est traité comme suit :

  • Au moment de la liaison, la résolution de surcharge est appliquée à , et , pour sélectionner une méthode C spécifique à partir de l’ensemble de méthodes déclarées dans et héritées par N.AMC Ceci est décrit dans le §12.8.10.2.
  • Ensuite, au moment de l’exécution :
    • S’il M s’agit d’une méthode non virtuelle, M elle est appelée.
    • Sinon, M est une méthode virtuelle et l’implémentation la plus dérivée par M rapport à celle-ci R est appelée.

Pour chaque méthode virtuelle déclarée ou héritée par une classe, il existe une implémentation la plus dérivée de la méthode par rapport à cette classe. L’implémentation la plus dérivée d’une méthode M virtuelle par rapport à une classe R est déterminée comme suit :

  • Si R contient la déclaration Mvirtuelle d’introduction, il s’agit de l’implémentation la plus dérivée de MR.
  • Sinon, s’il R contient un remplacement, il s’agit de Ml’implémentation la plus dérivée de MR.
  • Sinon, la mise en œuvre la plus dérivée du M point de R vue est la même que l’implémentation la plus dérivée par M rapport à la classe de base directe de R.

Exemple : L’exemple suivant illustre les différences entre les méthodes virtuelles et non virtuelles :

class A
{
    public void F() => Console.WriteLine("A.F");
    public virtual void G() => Console.WriteLine("A.G");
}

class B : A
{
    public new void F() => Console.WriteLine("B.F");
    public override void G() => Console.WriteLine("B.G");
}

class Test
{
    static void Main()
    {
        B b = new B();
        A a = b;
        a.F();
        b.F();
        a.G();
        b.G();
    }
}

Dans l’exemple, A introduit une méthode F non virtuelle et une méthode Gvirtuelle . La classe B introduit une nouvelle méthode Fnon virtuelle, masquant ainsi l’héritageF, et substitue également la méthode Ghéritée. L’exemple produit la sortie :

A.F
B.F
B.G
B.G

Notez que l’instruction a.G() appelle B.G, et non A.G. Cela est dû au fait que le type d’exécution de l’instance (qui est B), et non le type de compilation de l’instance (autrement Adit), détermine l’implémentation de méthode réelle à appeler.

exemple de fin

Étant donné que les méthodes sont autorisées à masquer les méthodes héritées, il est possible qu’une classe contienne plusieurs méthodes virtuelles avec la même signature. Cela ne pose pas de problème d’ambiguïté, car toutes les méthodes les plus dérivées sont masquées.

Exemple : dans le code suivant

class A
{
    public virtual void F() => Console.WriteLine("A.F");
}

class B : A
{
    public override void F() => Console.WriteLine("B.F");
}

class C : B
{
    public new virtual void F() => Console.WriteLine("C.F");
}

class D : C
{
    public override void F() => Console.WriteLine("D.F");
}

class Test
{
    static void Main()
    {
        D d = new D();
        A a = d;
        B b = d;
        C c = d;
        a.F();
        b.F();
        c.F();
        d.F();
    }
}

les C classes et D contiennent deux méthodes virtuelles avec la même signature : celle introduite par A et celle introduite par C. La méthode introduite par C masque la méthode héritée de A. Par conséquent, la déclaration de remplacement dans D les remplacements de la méthode introduite par C, et il n’est pas possible de D remplacer la méthode introduite par A. L’exemple produit la sortie :

B.F
B.F
D.F
D.F

Notez qu’il est possible d’appeler la méthode virtuelle masquée en accédant à une instance d’un D type moins dérivé dans lequel la méthode n’est pas masquée.

exemple de fin

15.6.5 Méthodes de remplacement

Lorsqu’une déclaration de méthode d’instance inclut un override modificateur, la méthode est considérée comme une méthode de remplacement. Une méthode de remplacement remplace une méthode virtuelle héritée avec la même signature. Alors qu’une déclaration de méthode virtuelle introduit une nouvelle méthode, une déclaration de méthode de remplacement spécialise une méthode virtuelle héritée existante en fournissant une nouvelle implémentation de cette méthode.

La méthode remplacée par une déclaration de remplacement est appelée méthode de base substituée Pour une méthode M de substitution déclarée dans une classeC, la méthode de base substituée est déterminée en examinant chaque classe de base de C, en commençant par la classe de base directe de C 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 qu’après la M substitution d’arguments de type. Dans le cadre de la localisation de la méthode de base substituée, une méthode est considérée comme accessible s’il s’agit , s’il publics’agit , s’il est protectedou s’il est protected internal ou internal déclaré dans le même programme que private protected.C

Une erreur au moment de la compilation se produit, sauf si toutes les valeurs suivantes sont remplies pour une déclaration de remplacement :

  • Une méthode de base substituée peut être située comme décrit ci-dessus.
  • Il existe exactement une méthode de base substituée. Cette restriction n’a effet que si le type de classe de base est un type construit où la substitution d’arguments de type rend la signature de deux méthodes identiques.
  • La méthode de base substituée est une méthode virtuelle, abstraite ou substituée. En d’autres termes, la méthode de base substituée ne peut pas être statique ou non virtuelle.
  • La méthode de base substituée n’est pas une méthode scellée.
  • Il existe une conversion d’identité entre le type de retour de la méthode de base substituée et la méthode override.
  • La déclaration de remplacement et la méthode de base substituée ont la même accessibilité déclarée. En d’autres termes, une déclaration de remplacement ne peut pas modifier l’accessibilité de la méthode virtuelle. Toutefois, si la méthode de base substituée est protégée en interne et qu’elle est déclarée dans un assembly différent de l’assembly contenant la déclaration de remplacement, l’accessibilité déclarée de la déclaration de remplacement doit être protégée.
  • La déclaration de remplacement ne spécifie aucune type_parameter_constraints_clauses. Au lieu de cela, les contraintes sont héritées de la méthode de base substituée. Les contraintes qui sont des paramètres de type dans la méthode substituée peuvent être remplacées par des arguments de type dans la contrainte héritée. Cela peut entraîner des contraintes qui ne sont pas valides lorsqu’elles sont spécifiées explicitement, telles que les types valeur ou les types scellés.

Exemple : L’exemple suivant montre comment fonctionnent les règles de substitution pour les classes génériques :

abstract class C<T>
{
    public virtual T F() {...}
    public virtual C<T> G() {...}
    public virtual void H(C<T> x) {...}
}

class D : C<string>
{
    public override string F() {...}            // Ok
    public override C<string> G() {...}         // Ok
    public override void H(C<T> x) {...}        // Error, should be C<string>
}

class E<T,U> : C<U>
{
    public override U F() {...}                 // Ok
    public override C<U> G() {...}              // Ok
    public override void H(C<T> x) {...}        // Error, should be C<U>
}

exemple de fin

Une déclaration de remplacement peut accéder à la méthode de base substituée à l’aide d’un base_access (§12.8.15).

Exemple : dans le code suivant

class A
{
    int x;

    public virtual void PrintFields() => Console.WriteLine($"x = {x}");
}

class B : A
{
    int y;

    public override void PrintFields()
    {
        base.PrintFields();
        Console.WriteLine($"y = {y}");
    }
}

l’appel base.PrintFields() dans B appelle la méthode PrintFields déclarée dans A. Une base_access désactive le mécanisme d’appel virtuel et traite simplement la méthode de base comme une méthode non-méthodevirtual . Si l’appel a B été écrit((A)this).PrintFields(), il appelle de façon récursive la PrintFields méthode déclarée, Bet non celle déclarée dans A, car PrintFields est virtuelle et le type d’exécution de ((A)this) l’objet .B

exemple de fin

Seul l’inclusion d’un override modificateur peut remplacer une autre méthode. Dans tous les autres cas, une méthode avec la même signature qu’une méthode héritée masque simplement la méthode héritée.

Exemple : dans le code suivant

class A
{
    public virtual void F() {}
}

class B : A
{
    public virtual void F() {} // Warning, hiding inherited F()
}

la F méthode dans B n’inclut pas de override modificateur et ne remplace donc pas la F méthode dans A. Au lieu de cela, la F méthode dans B masque la méthode dans A, et un avertissement est signalé, car la déclaration n’inclut pas de nouveau modificateur.

exemple de fin

Exemple : dans le code suivant

class A
{
    public virtual void F() {}
}

class B : A
{
    private new void F() {} // Hides A.F within body of B
}

class C : B
{
    public override void F() {} // Ok, overrides A.F
}

la F méthode dans B masque la méthode virtuelle F héritée de A. Étant donné que le nouveau F dans B a un accès privé, sa portée inclut uniquement le corps de classe et B ne s’étend pas à C. Par conséquent, la déclaration d’in FC est autorisée à remplacer l’héritage F de A.

exemple de fin

15.6.6 Méthodes scellées

Lorsqu’une déclaration de méthode d’instance inclut un sealed modificateur, cette méthode est dite être une méthode scellée. Une méthode scellée remplace une méthode virtuelle héritée par la même signature. Une méthode scellée doit également être marquée avec le override modificateur. L’utilisation sealed du modificateur empêche une classe dérivée de remplacer la méthode.

Exemple : l’exemple

class A
{
    public virtual void F() => Console.WriteLine("A.F");
    public virtual void G() => Console.WriteLine("A.G");
}

class B : A
{
    public sealed override void F() => Console.WriteLine("B.F");
    public override void G()        => Console.WriteLine("B.G");
}

class C : B
{
    public override void G() => Console.WriteLine("C.G");
}

la classe B fournit deux méthodes de remplacement : une F méthode qui a le sealed modificateur et une G méthode qui ne le fait pas. Bl’utilisation sealed du modificateur empêche C la substitution Fsupplémentaire.

exemple de fin

15.6.7 Méthodes abstraites

Lorsqu’une déclaration de méthode d’instance inclut un abstract modificateur, cette méthode est dite comme une méthode abstraite. Bien qu’une méthode abstraite soit implicitement également une méthode virtuelle, elle ne peut pas avoir le modificateur virtual.

Une déclaration de méthode abstraite introduit une nouvelle méthode virtuelle, mais ne fournit pas d’implémentation de cette méthode. Au lieu de cela, les classes dérivées non abstraites sont requises pour fournir leur propre implémentation en substituant cette méthode. Étant donné qu’une méthode abstraite ne fournit aucune implémentation réelle, le corps de la méthode d’une méthode abstraite se compose simplement d’un point-virgule.

Les déclarations de méthode abstraites sont autorisées uniquement dans les classes abstraites (§15.2.2.2).

Exemple : dans le code suivant

public abstract class Shape
{
    public abstract void Paint(Graphics g, Rectangle r);
}

public class Ellipse : Shape
{
    public override void Paint(Graphics g, Rectangle r) => g.DrawEllipse(r);
}

public class Box : Shape
{
    public override void Paint(Graphics g, Rectangle r) => g.DrawRect(r);
}

la Shape classe définit la notion abstraite d’un objet de forme géométrique qui peut se peindre lui-même. La Paint méthode est abstraite, car il n’existe aucune implémentation par défaut significative. Les Ellipse classes et Box les classes sont des implémentations concrètes Shape . Étant donné que ces classes ne sont pas abstraites, elles sont nécessaires pour remplacer la Paint méthode et fournir une implémentation réelle.

exemple de fin

Il s’agit d’une erreur au moment de la compilation d’un base_access (§12.8.15) pour référencer une méthode abstraite.

Exemple : dans le code suivant

abstract class A
{
    public abstract void F();
}

class B : A
{
    // Error, base.F is abstract
    public override void F() => base.F();
}

une erreur au moment de la compilation est signalée pour l’appel base.F() , car elle fait référence à une méthode abstraite.

exemple de fin

Une déclaration de méthode abstraite est autorisée à remplacer une méthode virtuelle. Cela permet à une classe abstraite de forcer la re-implémentation de la méthode dans les classes dérivées et rend l’implémentation d’origine de la méthode indisponible.

Exemple : dans le code suivant

class A
{
    public virtual void F() => Console.WriteLine("A.F");
}

abstract class B: A
{
    public abstract override void F();
}

class C : B
{
    public override void F() => Console.WriteLine("C.F");
}

la classe A déclare une méthode virtuelle, la classe B remplace cette méthode par une méthode abstraite et la classe C remplace la méthode abstraite pour fournir sa propre implémentation.

exemple de fin

15.6.8 Méthodes externes

Lorsqu’une déclaration de méthode inclut un extern modificateur, la méthode est considérée comme une méthode externe. Les méthodes externes sont implémentées en externe, généralement à l’aide d’un langage autre que C#. Étant donné qu’une déclaration de méthode externe ne fournit aucune implémentation réelle, le corps de méthode d’une méthode externe se compose simplement d’un point-virgule. Une méthode externe ne doit pas être générique.

Le mécanisme par lequel la liaison à une méthode externe est obtenue est défini par l’implémentation.

Exemple : L’exemple suivant illustre l’utilisation du extern modificateur et de l’attribut DllImport :

class Path
{
    [DllImport("kernel32", SetLastError=true)]
    static extern bool CreateDirectory(string name, SecurityAttribute sa);

    [DllImport("kernel32", SetLastError=true)]
    static extern bool RemoveDirectory(string name);

    [DllImport("kernel32", SetLastError=true)]
    static extern int GetCurrentDirectory(int bufSize, StringBuilder buf);

    [DllImport("kernel32", SetLastError=true)]
    static extern bool SetCurrentDirectory(string name);
}

exemple de fin

15.6.9 Méthodes partielles

Lorsqu’une déclaration de méthode inclut un partial modificateur, cette méthode est considérée comme une méthode partielle. Les méthodes partielles peuvent uniquement être déclarées en tant que membres de types partiels (§15.2.7) et sont soumises à un certain nombre de restrictions.

Les méthodes partielles peuvent être définies dans une partie d’une déclaration de type et implémentées dans une autre. L’implémentation est facultative ; si aucune partie n’implémente la méthode partielle, la déclaration de méthode partielle et tous les appels à celui-ci sont supprimés de la déclaration de type résultant de la combinaison des parties.

Les méthodes partielles ne définissent pas les modificateurs d’accès ; elles sont implicitement privées. Leur type de retour doit être void, et leurs paramètres ne doivent pas être des paramètres de sortie. L’identificateur partiel est reconnu comme un mot clé contextuel (§6.4.4) dans une déclaration de méthode uniquement s’il apparaît immédiatement avant le void mot clé. Une méthode partielle ne peut pas implémenter explicitement les méthodes d’interface.

Il existe deux types de déclarations de méthode partielles : si le corps de la déclaration de méthode est un point-virgule, la déclaration est considérée comme une déclaration de méthode partielle définissante. Si le corps est autre qu’un point-virgule, la déclaration est considérée comme une déclaration de méthode partielle d’implémentation. Dans les parties d’une déclaration de type, il ne peut y avoir qu’une seule déclaration de méthode partielle avec une signature donnée, et il ne peut y avoir qu’une seule déclaration de méthode partielle avec une signature donnée. Si une déclaration de méthode partielle d’implémentation est donnée, une déclaration de méthode partielle correspondante existe et les déclarations doivent correspondre comme spécifié dans les éléments suivants :

  • Les déclarations doivent avoir les mêmes modificateurs (bien qu’ils ne soient pas nécessairement dans le même ordre), le nom de la méthode, le nombre de paramètres de type et le nombre de paramètres.
  • Les paramètres correspondants dans les déclarations doivent avoir les mêmes modificateurs (bien qu’ils ne soient pas nécessairement dans le même ordre) et les mêmes types ou types convertibles d’identité (différences modulo dans les noms de paramètres de type).
  • Les paramètres de type correspondants dans les déclarations doivent avoir les mêmes contraintes (différences modulo dans les noms de paramètres de type).

Une déclaration de méthode partielle implémente peut apparaître dans la même partie que la déclaration de méthode partielle correspondante.

Seule une méthode partielle définissant participe à la résolution de surcharge. Ainsi, si une déclaration d’implémentation est donnée ou non, les expressions d’appel peuvent résoudre les appels de la méthode partielle. Étant donné qu’une méthode partielle retourne voidtoujours, ces expressions d’appel sont toujours des instructions d’expression. En outre, étant donné qu’une méthode partielle est implicitement private, ces instructions se produisent toujours dans l’une des parties de la déclaration de type dans laquelle la méthode partielle est déclarée.

Remarque : La définition de la définition de la définition et de l’implémentation de déclarations de méthode partielles ne nécessite pas que les noms de paramètres correspondent. Cela peut produire un comportement surprenant, bien que bien défini, lorsque des arguments nommés (§12.6.2.1) sont utilisés. Par exemple, étant donné la définition de la déclaration de méthode partielle pour M un fichier et l’implémentation de la déclaration de méthode partielle dans un autre fichier :

// File P1.cs:
partial class P
{
    static partial void M(int x);
}

// File P2.cs:
partial class P
{
    static void Caller() => M(y: 0);
    static partial void M(int y) {}
}

n’est pas valide , car l’appel utilise le nom de l’argument de l’implémentation et non la déclaration de méthode partielle définie.

Note de fin

Si aucune partie d’une déclaration de type partiel ne contient une déclaration d’implémentation pour une méthode partielle donnée, toute instruction d’expression appelant celle-ci est simplement supprimée de la déclaration de type combinée. Ainsi, l’expression d’appel, y compris les sous-expressions, n’a aucun effet au moment de l’exécution. La méthode partielle elle-même est également supprimée et ne sera pas membre de la déclaration de type combiné.

Si une déclaration d’implémentation existe pour une méthode partielle donnée, les appels des méthodes partielles sont conservés. La méthode partielle donne lieu à une déclaration de méthode similaire à la déclaration de méthode partielle d’implémentation, à l’exception des éléments suivants :

  • Le partial modificateur n’est pas inclus.

  • Les attributs de la déclaration de méthode résultante sont les attributs combinés de la définition et de la déclaration de méthode partielle implémentée dans un ordre non spécifié. Les doublons ne sont pas supprimés.

  • Les attributs sur les paramètres de la déclaration de méthode résultante sont les attributs combinés des paramètres correspondants de la définition et de la déclaration de méthode partielle en ordre non spécifié. Les doublons ne sont pas supprimés.

Si une déclaration de définition mais pas une déclaration d’implémentation est donnée pour une méthode Mpartielle, les restrictions suivantes s’appliquent :

  • Il s’agit d’une erreur au moment de la compilation à partir de M laquelle créer un délégué (§12.8.17.6).

  • Il s’agit d’une erreur au moment de la compilation à faire référence à M l’intérieur d’une fonction anonyme convertie en type d’arborescence d’expressions (§8.6).

  • Les expressions qui se produisent dans le cadre d’un appel n’affectent M pas l’état d’affectation défini (§9.4), ce qui peut entraîner des erreurs de compilation.

  • M ne peut pas être le point d’entrée d’une application (§7.1).

Les méthodes partielles sont utiles pour permettre à une partie d’une déclaration de type de personnaliser le comportement d’une autre partie, par exemple un élément généré par un outil. Considérez la déclaration de classe partielle suivante :

partial class Customer
{
    string name;

    public string Name
    {
        get => name;
        set
        {
            OnNameChanging(value);
            name = value;
            OnNameChanged();
        }
    }

    partial void OnNameChanging(string newName);
    partial void OnNameChanged();
}

Si cette classe est compilée sans aucune autre partie, la définition des déclarations de méthode partielles et de leurs appels est supprimée, et la déclaration de classe combinée résultante équivaut à ce qui suit :

class Customer
{
    string name;

    public string Name
    {
        get => name;
        set => name = value;
    }
}

Supposons toutefois qu’une autre partie soit donnée, qui fournit des déclarations d’implémentation des méthodes partielles :

partial class Customer
{
    partial void OnNameChanging(string newName) =>
        Console.WriteLine($"Changing {name} to {newName}");

    partial void OnNameChanged() =>
        Console.WriteLine($"Changed to {name}");
}

Ensuite, la déclaration de classe combinée résultante équivaut à ce qui suit :

class Customer
{
    string name;

    public string Name
    {
        get => name;
        set
        {
            OnNameChanging(value);
            name = value;
            OnNameChanged();
        }
    }

    void OnNameChanging(string newName) =>
        Console.WriteLine($"Changing {name} to {newName}");

    void OnNameChanged() =>
        Console.WriteLine($"Changed to {name}");
}

Méthodes d’extension 15.6.10

Lorsque le premier paramètre d’une méthode inclut le this modificateur, cette méthode est considérée comme une méthode d’extension. Les méthodes d’extension ne doivent être déclarées que dans des classes statiques non génériques et non imbriquées. Le premier paramètre d’une méthode d’extension est restreint, comme suit :

  • Il peut uniquement s’agir d’un paramètre d’entrée s’il a un type valeur
  • Il peut s’agir uniquement d’un paramètre de référence s’il a un type valeur ou s’il a un type générique contraint à struct
  • Il ne doit pas s’agir d’un type de pointeur.

Exemple : Voici un exemple de classe statique qui déclare deux méthodes d’extension :

public static class Extensions
{
    public static int ToInt32(this string s) => Int32.Parse(s);

    public static T[] Slice<T>(this T[] source, int index, int count)
    {
        if (index < 0 || count < 0 || source.Length - index < count)
        {
            throw new ArgumentException();
        }
        T[] result = new T[count];
        Array.Copy(source, index, result, 0, count);
        return result;
    }
}

exemple de fin

Une méthode d’extension est une méthode statique régulière. En outre, lorsque sa classe statique englobante est dans l’étendue, une méthode d’extension peut être appelée à l’aide de la syntaxe d’appel de méthode d’instance (§12.8.10.3), à l’aide de l’expression de récepteur comme premier argument.

Exemple : Le programme suivant utilise les méthodes d’extension déclarées ci-dessus :

static class Program
{
    static void Main()
    {
        string[] strings = { "1", "22", "333", "4444" };
        foreach (string s in strings.Slice(1, 2))
        {
            Console.WriteLine(s.ToInt32());
        }
    }
}

La Slice méthode est disponible sur le string[], et la ToInt32 méthode est disponible sur string, car elles ont été déclarées comme méthodes d’extension. La signification du programme est la même que celle suivante, à l’aide d’appels de méthode statique ordinaires :

static class Program
{
    static void Main()
    {
        string[] strings = { "1", "22", "333", "4444" };
        foreach (string s in Extensions.Slice(strings, 1, 2))
        {
            Console.WriteLine(Extensions.ToInt32(s));
        }
    }
}

exemple de fin

15.6.11 Corps de la méthode

Le corps de méthode d’une déclaration de méthode se compose d’un corps de bloc, d’un corps d’expression ou d’un point-virgule.

Les déclarations de méthode abstraites et externes ne fournissent pas d’implémentation de méthode, de sorte que leurs corps de méthode se composent simplement d’un point-virgule. Pour toute autre méthode, le corps de la méthode est un bloc (§13.3) qui contient les instructions à exécuter quand cette méthode est appelée.

Le type de retour effectif d’une méthode est void si le type de retour est void, ou si la méthode est asynchrone et que le type de retour est «TaskType» (§15.15.1). Sinon, le type de retour effectif d’une méthode non asynchrone est son type de retour et le type de retour effectif d’une méthode asynchrone avec le type «TaskType»<T>de retour (§15.15.1) est T.

Lorsque le type de retour effectif d’une méthode est void et que la méthode a un corps de bloc, return les instructions (§13.10.5) dans le bloc ne spécifient pas d’expression. Si l’exécution du bloc d’une méthode void se termine normalement (autrement dit, le contrôle s’écoule hors de la fin du corps de la méthode), cette méthode retourne simplement à son appelant.

Lorsque le type de retour effectif d’une méthode est void et que la méthode a un corps d’expression, l’expression E doit être une statement_expression, et le corps est exactement équivalent à un corps de bloc du formulaire { E; }.

Pour une méthode de retour par valeur (§15.6.1), chaque instruction de retour dans le corps de cette méthode doit spécifier une expression implicitement convertible en type de retour effectif.

Pour une méthode return-by-ref (§15.6.1), chaque instruction de retour dans le corps de cette méthode doit spécifier une expression dont le type est celui du type de retour effectif et a un contexte ref-safe de caller-context (§9.7.2).

Pour les méthodes returns-by-value et returns-by-ref, le point de terminaison du corps de la méthode ne peut pas être accessible. En d’autres termes, le contrôle n’est pas autorisé à circuler hors de la fin du corps de la méthode.

Exemple : dans le code suivant

class A
{
    public int F() {} // Error, return value required

    public int G()
    {
        return 1;
    }

    public int H(bool b)
    {
        if (b)
        {
            return 1;
        }
        else
        {
            return 0;
        }
    }

    public int I(bool b) => b ? 1 : 0;
}

la méthode de retour F de valeur entraîne une erreur au moment de la compilation, car le contrôle peut s’écouler à la fin du corps de la méthode. Les G méthodes et H les méthodes sont correctes, car tous les chemins d’exécution possibles se terminent dans une instruction return qui spécifie une valeur de retour. La I méthode est correcte, car son corps équivaut à un bloc avec une seule instruction return dans celle-ci.

exemple de fin

15.7 Propriétés

15.7.1 Général

Une propriété est un membre qui fournit l’accès à une caractéristique d’un objet ou d’une classe. Les propriétés incluent la longueur d’une chaîne, la taille d’une police, la légende d’une fenêtre et le nom d’un client. Les propriétés sont une extension naturelle des champs : les deux sont des membres nommés avec des types associés, et la syntaxe d’accès aux champs et aux propriétés est la même. Toutefois, contrairement aux champs, les propriétés ne désignent pas des emplacements de stockage. Au lieu de cela, les propriétés ont des accesseurs qui spécifient les instructions à exécuter lorsque les valeurs sont lues ou écrites. Les propriétés fournissent donc un mécanisme permettant d’associer des actions à la lecture et à l’écriture des caractéristiques d’un objet ou d’une classe ; de plus, ils permettent de calculer ces caractéristiques.

Les propriétés sont déclarées à l’aide de property_declarations :

property_declaration
    : attributes? property_modifier* type member_name property_body
    | attributes? property_modifier* ref_kind type member_name ref_property_body
    ;    

property_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;
    
property_body
    : '{' accessor_declarations '}' property_initializer?
    | '=>' expression ';'
    ;

property_initializer
    : '=' variable_initializer ';'
    ;

ref_property_body
    : '{' ref_get_accessor_declaration '}'
    | '=>' 'ref' variable_reference ';'
    ;

unsafe_modifier (§23.2) est disponible uniquement dans le code non sécurisé (§23).

Il existe deux types de property_declaration :

  • Le premier déclare une propriété non-ref-valued. Sa valeur a un type de type. Ce type de propriété peut être lisible et/ou accessible en écriture.
  • La deuxième déclare une propriété ref-valued. Sa valeur est un variable_reference (§9.5readonly Ce type de propriété est lisible uniquement.

Un property_declaration peut inclure un ensemble d’attributs (§22) et l’un des types autorisés d’accessibilité déclarée (§15.3.6), le new (§15.3.5), static (§15.7.2), virtual (§15.6.4, §15.7.6), override (§15.6.5, §15.7.6), sealed (§15.6.6), abstract (§15.6.7, §15.7.6) et extern (§15.6.8) modificateurs.

Les déclarations de propriété sont soumises aux mêmes règles que les déclarations de méthode (§15.6) en ce qui concerne les combinaisons valides de modificateurs.

Le member_name (§15.6.1) spécifie le nom de la propriété. Sauf si la propriété est une implémentation de membre d’interface explicite, la member_name est simplement un identificateur. Pour une implémentation de membre d’interface explicite (§18.6.2), le member_name se compose d’un interface_type suivi d’un «. » et d’un identificateur.

Le type d’une propriété doit être au moins aussi accessible que la propriété elle-même (§7.5.5).

Une property_body peut être constituée d’un corps d’instruction ou d’un corps d’expression. Dans un corps d’instruction, accessor_declarations, qui doit être placé entre des jetons « » et «{} », déclarez les accesseurs (§15.7.3) de la propriété. Les accesseurs spécifient les instructions exécutables associées à la lecture et à l’écriture de la propriété.

Dans un property_body corps d’expression composé d’une => expressionE et d’un point-virgule équivaut exactement au corps { get { return E; } }de l’instruction, et peut donc être utilisé uniquement pour spécifier des propriétés en lecture seule où le résultat de l’accesseur get est donné par une seule expression.

Une property_initializer peut uniquement être donnée pour une propriété implémentée automatiquement (§15.7.4) et provoque l’initialisation du champ sous-jacent de ces propriétés avec la valeur donnée par l’expression.

Une ref_property_body peut être constituée d’un corps d’instruction ou d’un corps d’expression. Dans un corps d’instruction, un get_accessor_declaration déclare l’accesseur get (§15.7.3) de la propriété. L’accesseur spécifie les instructions exécutables associées à la lecture de la propriété.

Dans un ref_property_body un corps d’expression composé de =>refsuivi, d’un variable_referenceV et d’un point-virgule équivaut exactement au corps { get { return ref V; } }de l’instruction.

Remarque : Même si la syntaxe d’accès à une propriété est identique à celle d’un champ, une propriété n’est pas classifiée comme variable. Par conséquent, il n’est pas possible de passer une propriété en tant que in, outou ref argument, sauf si la propriété est ref-valued et retourne donc une référence de variable (§9.7). Note de fin

Lorsqu’une déclaration de propriété inclut un extern modificateur, la propriété est considérée comme une propriété externe. Étant donné qu’une déclaration de propriété externe ne fournit aucune implémentation réelle, chacun des accessor_bodydans son accessor_declarations doit être un point-virgule.

15.7.2 Propriétés statiques et d’instance

Lorsqu’une déclaration de propriété inclut un static modificateur, la propriété est considérée comme une propriété statique. Lorsqu’aucun modificateur n’est static présent, la propriété est considérée comme une propriété d’instance.

Une propriété statique n’est pas associée à une instance spécifique, et il s’agit d’une erreur au moment de la compilation à laquelle faire référence this dans les accesseurs d’une propriété statique.

Une propriété d’instance est associée à une instance donnée d’une classe, et cette instance est accessible en tant que this (§12.8.14) dans les accesseurs de cette propriété.

Les différences entre les membres statiques et d’instance sont abordées plus loin dans le §15.3.8.

15.7.3 Accesseurs

Remarque : Cette clause s’applique aux propriétés (§15.7) et aux indexeurs (§15.9). La clause est écrite en termes de propriétés, lors de la lecture pour les indexeurs remplacent l’indexeur/indexeur pour les propriétés/propriétés et consultent la liste des différences entre les propriétés et les indexeurs donnés dans le §15.9.2. Note de fin

La accessor_declarations d’une propriété spécifie les instructions exécutables associées à l’écriture et/ou à la lecture de cette propriété.

accessor_declarations
    : get_accessor_declaration set_accessor_declaration?
    | set_accessor_declaration get_accessor_declaration?
    ;

get_accessor_declaration
    : attributes? accessor_modifier? 'get' accessor_body
    ;

set_accessor_declaration
    : attributes? accessor_modifier? 'set' accessor_body
    ;

accessor_modifier
    : 'protected'
    | 'internal'
    | 'private'
    | 'protected' 'internal'
    | 'internal' 'protected'
    | 'protected' 'private'
    | 'private' 'protected'
    ;

accessor_body
    : block
    | '=>' expression ';'
    | ';' 
    ;

ref_get_accessor_declaration
    : attributes? accessor_modifier? 'get' ref_accessor_body
    ;
    
ref_accessor_body
    : block
    | '=>' 'ref' variable_reference ';'
    | ';'
    ;

Le accessor_declarations se compose d’un get_accessor_declaration, d’un set_accessor_declaration ou des deux. Chaque déclaration d’accesseur se compose d’attributs facultatifs, d’un accessor_modifier facultatif, du jeton ou getd’un set.

Pour une propriété ref valued, la ref_get_accessor_declaration se compose d’attributs facultatifs, d’un accessor_modifier facultatif, du jetonget, suivi d’un ref_accessor_body.

L’utilisation de accessor_modifiers est régie par les restrictions suivantes :

  • Une accessor_modifier ne doit pas être utilisée dans une interface ou dans une implémentation de membre d’interface explicite.
  • Pour une propriété ou un indexeur qui n’a aucun override modificateur, une accessor_modifier n’est autorisée que si la propriété ou l’indexeur a à la fois un accesseur get et set, et n’est autorisé que sur l’un de ces accesseurs.
  • Pour une propriété ou un indexeur qui inclut un override modificateur, un accesseur doit correspondre à la accessor_modifier, le cas échéant, de l’accesseur en cours de substitution.
  • Le accessor_modifier déclare une accessibilité strictement plus restrictive que l’accessibilité déclarée de la propriété ou de l’indexeur lui-même. Pour être précis :
    • Si la propriété ou l’indexeur a une accessibilité publicdéclarée, l’accessibilité déclarée par accessor_modifier peut être soit private protected, , protected internal, , internalou protectedprivate.
    • Si la propriété ou l’indexeur a une accessibilité protected internaldéclarée, l’accessibilité déclarée par accessor_modifier peut être soit private protected, , protected private, , internalou protectedprivate.
    • Si la propriété ou l’indexeur a une accessibilité déclarée ou internalprotectedsi l’accessibilité déclarée par accessor_modifier doit être ou private protectedprivate.
    • Si la propriété ou l’indexeur a une accessibilité private protecteddéclarée, l’accessibilité déclarée par accessor_modifier doit être private.
    • Si la propriété ou l’indexeur a une accessibilité privatedéclarée, aucune accessor_modifier ne peut être utilisée.

Pour abstract les propriétés non extern ref, toute accessor_body pour chaque accesseur spécifié est simplement un point-virgule. Une propriété non abstraite, non extern, mais pas un indexeur, peut également avoir la accessor_body pour tous les accesseurs spécifiés comme point-virgule, auquel cas il s’agit d’une propriété implémentée automatiquement (§15.7.4). Une propriété implémentée automatiquement doit avoir au moins un accesseur get. Pour les accesseurs d’une autre propriété non abstraite, non extern, la accessor_body est :

  • un bloc qui spécifie les instructions à exécuter lorsque l’accesseur correspondant est appelé ; ou
  • un corps d’expression, qui se compose d’une => expression et d’un point-virgule, et désigne une expression unique à exécuter lorsque l’accesseur correspondant est appelé.

Pour abstract et extern les propriétés ref, la ref_accessor_body est simplement un point-virgule. Pour l’accesseur d’une autre propriété non abstraite, non extern, la ref_accessor_body est :

  • un bloc qui spécifie les instructions à exécuter lorsque l’accesseur get est appelé ; ou
  • corps d’expression, qui se compose d’un =>refvariable_reference et d’un point-virgule. La référence de variable est évaluée lorsque l’accesseur get est appelé.

Un accesseur get pour une propriété non ref correspond à une méthode sans paramètre avec une valeur de retour du type de propriété. À l’exception de la cible d’une affectation, lorsqu’une telle propriété est référencée dans une expression, son accesseur get est appelée pour calculer la valeur de la propriété (§12.2.2).

Le corps d’un accesseur get pour une propriété non ref-value doit respecter les règles relatives aux méthodes de retour de valeur décrites dans le §15.6.11. En particulier, toutes les return instructions du corps d’un accesseur get doivent spécifier une expression implicitement convertible en type de propriété. En outre, le point de terminaison d’un accesseur get ne peut pas être accessible.

Un accesseur get pour une propriété ref-value correspond à une méthode sans paramètre avec une valeur de retour d’un variable_reference à une variable du type de propriété. Lorsqu’une telle propriété est référencée dans une expression, son accesseur get est appelé pour calculer la valeur variable_reference de la propriété. Cette référence de variable, comme toute autre, est ensuite utilisée pour lire ou, pour les variable_referencenon en lecture seule, écrivez la variable référencée comme requis par le contexte.

Exemple : L’exemple suivant illustre une propriété ref-valued comme cible d’une affectation :

class Program
{
    static int field;
    static ref int Property => ref field;

    static void Main()
    {
        field = 10;
        Console.WriteLine(Property); // Prints 10
        Property = 20;               // This invokes the get accessor, then assigns
                                     // via the resulting variable reference
        Console.WriteLine(field);    // Prints 20
    }
}

exemple de fin

Le corps d’un accesseur get pour une propriété ref-value doit être conforme aux règles des méthodes ref-valued décrites dans le §15.6.11.

Un accesseur set correspond à une méthode avec un paramètre valeur unique du type de propriété et un void type de retour. Le paramètre implicite d’un accesseur set est toujours nommé value. Lorsqu’une propriété est référencée comme cible d’une affectation (§12.21), ou en tant qu’opérande de ou ++ (–-, §12.9.6), l’accesseur set est appelé avec un argument qui fournit la nouvelle valeur (§12.21.2). Le corps d’un accesseur set doit respecter les règles des void méthodes décrites dans le §15.6.11. En particulier, les instructions de retour dans le corps de l’accesseur set ne sont pas autorisées à spécifier une expression. Étant donné qu’un accesseur set a implicitement un paramètre nommé value, il s’agit d’une erreur au moment de la compilation d’une variable locale ou d’une déclaration constante dans un accesseur set pour avoir ce nom.

En fonction de la présence ou de l’absence des accesseurs get et set, une propriété est classée comme suit :

  • Une propriété qui inclut à la fois un accesseur get et un accesseur set est considérée comme une propriété en lecture-écriture.
  • Une propriété qui n’a qu’un accesseur get est considérée comme une propriété en lecture seule. Il s’agit d’une erreur au moment de la compilation pour qu’une propriété en lecture seule soit la cible d’une affectation.
  • Une propriété qui n’a qu’un accesseur set est considérée comme une propriété en écriture seule. À l’exception de la cible d’une affectation, il s’agit d’une erreur au moment de la compilation pour référencer une propriété en écriture seule dans une expression.

Remarque : Les opérateurs et opérateurs de préfix et de postfix ++ et -- d’affectation composée ne peuvent pas être appliqués aux propriétés en écriture seule, car ces opérateurs lisent l’ancienne valeur de leur opérande avant d’écrire le nouveau. Note de fin

Exemple : dans le code suivant

public class Button : Control
{
    private string caption;

    public string Caption
    {
        get => caption;
        set
        {
            if (caption != value)
            {
                caption = value;
                Repaint();
            }
        }
    }

    public override void Paint(Graphics g, Rectangle r)
    {
        // Painting code goes here
    }
}

le Button contrôle déclare une propriété publique Caption . L’accesseur get de la propriété Caption retourne le string stocké dans le champ privé caption . L’accesseur set vérifie si la nouvelle valeur est différente de la valeur actuelle et, le cas échéant, elle stocke la nouvelle valeur et repeint le contrôle. Les propriétés suivent souvent le modèle ci-dessus : l’accesseur get retourne simplement une valeur stockée dans un private champ, et l’accesseur set modifie ce private champ, puis effectue toutes les actions supplémentaires requises pour mettre à jour entièrement l’état de l’objet. Étant donné la Button classe ci-dessus, voici un exemple d’utilisation de la Caption propriété :

Button okButton = new Button();
okButton.Caption = "OK"; // Invokes set accessor
string s = okButton.Caption; // Invokes get accessor

Ici, l’accesseur set est appelé en affectant une valeur à la propriété, et l’accesseur get est appelé en référençant la propriété dans une expression.

exemple de fin

Les accesseurs get et set d’une propriété ne sont pas des membres distincts, et il n’est pas possible de déclarer les accesseurs d’une propriété séparément.

Exemple : l’exemple

class A
{
    private string name;

    // Error, duplicate member name
    public string Name
    { 
        get => name;
    }

    // Error, duplicate member name
    public string Name
    { 
        set => name = value;
    }
}

ne déclare pas une propriété en lecture-écriture unique. Au lieu de cela, il déclare deux propriétés portant le même nom, une seule lecture seule et une seule écriture. Étant donné que deux membres déclarés dans la même classe ne peuvent pas avoir le même nom, l’exemple provoque une erreur au moment de la compilation.

exemple de fin

Lorsqu’une classe dérivée déclare une propriété du même nom qu’une propriété héritée, la propriété dérivée masque la propriété héritée en ce qui concerne la lecture et l’écriture.

Exemple : dans le code suivant

class A
{
    public int P
    {
        set {...}
    }
}

class B : A
{
    public new int P
    {
        get {...}
    }
}

la P propriété dans B masque la propriété en P ce qui concerne la lecture et l’écritureA. Ainsi, dans les instructions

B b = new B();
b.P = 1;       // Error, B.P is read-only
((A)b).P = 1;  // Ok, reference to A.P

l’affectation à b.P cause d’une erreur au moment de la compilation doit être signalée, car la propriété en lecture seule P dans B masque la propriété en écriture seule P dans A. Notez toutefois qu’un cast peut être utilisé pour accéder à la propriété masquée P .

exemple de fin

Contrairement aux champs publics, les propriétés fournissent une séparation entre l’état interne d’un objet et son interface publique.

Exemple : Considérez le code suivant, qui utilise un Point struct pour représenter un emplacement :

class Label
{
    private int x, y;
    private string caption;

    public Label(int x, int y, string caption)
    {
        this.x = x;
        this.y = y;
        this.caption = caption;
    }

    public int X => x;
    public int Y => y;
    public Point Location => new Point(x, y);
    public string Caption => caption;
}

Ici, la Label classe utilise deux int champs et xy, pour stocker son emplacement. L’emplacement est exposé publiquement sous la forme d’une XY propriété et d’une Location propriété de type Point. Si, dans une version ultérieure de Label, il devient plus pratique de stocker l’emplacement en interne Point , la modification peut être apportée sans affecter l’interface publique de la classe :

class Label
{
    private Point location;
    private string caption;

    public Label(int x, int y, string caption)
    {
        this.location = new Point(x, y);
        this.caption = caption;
    }

    public int X => location.X;
    public int Y => location.Y;
    public Point Location => location;
    public string Caption => caption;
}

x Au y lieu de celapublic readonly, il aurait été impossible d’apporter une telle modification à la Label classe.

exemple de fin

Remarque : L’exposition de l’état par le biais de propriétés n’est pas nécessairement moins efficace que l’exposition directe de champs. En particulier, lorsqu’une propriété n’est pas virtuelle et ne contient qu’une petite quantité de code, l’environnement d’exécution peut remplacer les appels aux accesseurs par le code réel des accesseurs. Ce processus est appelé inlining et rend l’accès aux propriétés aussi efficace que l’accès aux champs, mais conserve la flexibilité accrue des propriétés. Note de fin

Exemple : étant donné que l’appel d’un accesseur get est conceptuellement équivalent à la lecture de la valeur d’un champ, il est considéré comme un style de programmation incorrect pour que les accesseurs obtiennent des effets secondaires observables. Dans l’exemple

class Counter
{
    private int next;

    public int Next => next++;
}

la valeur de la Next propriété dépend du nombre de fois où la propriété a déjà été accédée. Ainsi, l’accès à la propriété produit un effet secondaire observable et la propriété doit être implémentée comme méthode à la place.

La convention « aucun effet secondaire » pour les accesseurs get ne signifie pas que les accesseurs get doivent toujours être écrits simplement pour retourner des valeurs stockées dans des champs. En effet, obtenir des accesseurs calcule souvent la valeur d’une propriété en accédant à plusieurs champs ou en appelant des méthodes. Toutefois, un accesseur get correctement conçu n’effectue aucune action qui provoque des modifications observables dans l’état de l’objet.

exemple de fin

Les propriétés peuvent être utilisées pour retarder l’initialisation d’une ressource jusqu’à ce qu’elle soit référencée pour la première fois.

Exemple :

public class Console
{
    private static TextReader reader;
    private static TextWriter writer;
    private static TextWriter error;

    public static TextReader In
    {
        get
        {
            if (reader == null)
            {
                reader = new StreamReader(Console.OpenStandardInput());
            }
            return reader;
        }
    }

    public static TextWriter Out
    {
        get
        {
            if (writer == null)
            {
                writer = new StreamWriter(Console.OpenStandardOutput());
            }
            return writer;
        }
    }

    public static TextWriter Error
    {
        get
        {
            if (error == null)
            {
                error = new StreamWriter(Console.OpenStandardError());
            }
            return error;
        }
    }
...
}

La Console classe contient trois propriétés, In, Outet Error, qui représentent respectivement les appareils d’entrée, de sortie et d’erreur standard. En exposant ces membres en tant que propriétés, la Console classe peut retarder leur initialisation jusqu’à ce qu’elles soient réellement utilisées. Par exemple, lors du premier référencement de la Out propriété, comme dans

Console.Out.WriteLine("hello, world");

le sous-jacent TextWriter de l’appareil de sortie est créé. Toutefois, si l’application ne fait aucune référence aux propriétés et In aux Error propriétés, aucun objet n’est créé pour ces appareils.

exemple de fin

15.7.4 Implémentation automatique des propriétés

Une propriété implémentée automatiquement (ou auto-propriété pour short) est une propriété non abstraite, non externe, non ref-valued avec des points-virgules uniquement accessor_bodys. Les propriétés automatiques doivent avoir un accesseur get et peuvent éventuellement avoir un accesseur défini.

Lorsqu’une propriété est spécifiée en tant que propriété implémentée automatiquement, un champ de stockage masqué est automatiquement disponible pour la propriété et les accesseurs sont implémentés pour lire et écrire dans ce champ de stockage. Le champ de stockage masqué n’est pas accessible, il peut être lu et écrit uniquement par le biais des accesseurs de propriétés implémentés automatiquement, même dans le type conteneur. Si la propriété automatique n’a pas d’accesseur défini, le champ de stockage est considéré readonly (§15.5.3). Tout comme un readonly champ, une propriété automatique en lecture seule peut également être affectée dans le corps d’un constructeur de la classe englobante. Une telle affectation affecte directement le champ de stockage en lecture seule de la propriété.

Une propriété automatique peut éventuellement avoir un property_initializer, qui est appliqué directement au champ de stockage en tant que variable_initializer (§17.7).

Exemple :

public class Point
{
    public int X { get; set; } // Automatically implemented
    public int Y { get; set; } // Automatically implemented
}

équivaut à la déclaration suivante :

public class Point
{
    private int x;
    private int y;

    public int X { get { return x; } set { x = value; } }
    public int Y { get { return y; } set { y = value; } }
}

exemple de fin

Exemple : dans les éléments suivants

public class ReadOnlyPoint
{
    public int X { get; }
    public int Y { get; }

    public ReadOnlyPoint(int x, int y)
    {
        X = x;
        Y = y;
    }
}

équivaut à la déclaration suivante :

public class ReadOnlyPoint
{
    private readonly int __x;
    private readonly int __y;
    public int X { get { return __x; } }
    public int Y { get { return __y; } }

    public ReadOnlyPoint(int x, int y)
    {
        __x = x;
        __y = y;
    }
}

Les affectations au champ en lecture seule sont valides, car elles se produisent dans le constructeur.

exemple de fin

Bien que le champ de stockage soit masqué, ce champ peut avoir des attributs ciblés par champ appliqués directement à celui-ci via le property_declaration de la propriété implémentée automatiquement (§15.7.1).

Exemple : le code suivant

[Serializable]
public class Foo
{
    [field: NonSerialized]
    public string MySecret { get; set; }
}

entraîne l’application de l’attribut NonSerialized ciblé par champ au champ de stockage généré par le compilateur, comme si le code avait été écrit comme suit :

[Serializable]
public class Foo
{
    [NonSerialized]
    private string _mySecretBackingField;
    public string MySecret
    {
        get { return _mySecretBackingField; }
        set { _mySecretBackingField = value; }
    }
}

exemple de fin

15.7.5 Accessibilité

Si un accesseur a un accessor_modifier, le domaine d’accessibilité (§7.5.3) de l’accesseur est déterminé à l’aide de l’accessibilité déclarée du accessor_modifier. Si un accesseur n’a pas de accessor_modifier, le domaine d’accessibilité de l’accesseur est déterminé à partir de l’accessibilité déclarée de la propriété ou de l’indexeur.

La présence d’un accessor_modifier n’affecte jamais la recherche de membre (§12.5) ou la résolution de surcharge (§12.6.4). Les modificateurs de la propriété ou de l’indexeur déterminent toujours quelle propriété ou indexeur est liée, quel que soit le contexte de l’accès.

Une fois qu’une propriété ou un indexeur non ref-valued particulier a été sélectionné, les domaines d’accessibilité des accesseurs spécifiques impliqués sont utilisés pour déterminer si cette utilisation est valide :

  • Si l’utilisation est en tant que valeur (§12.2.2), l’accesseur get doit exister et être accessible.
  • Si l’utilisation est la cible d’une affectation simple (§12.21.2), l’accesseur set doit exister et être accessible.
  • Si l’utilisation est la cible de l’affectation composée (§12.21.4), ou comme cible des ++ ou -- opérateurs (§12.8.16, §12.9.6), les accesseurs get et l’accesseur défini doivent exister et être accessibles.

Exemple : Dans l’exemple suivant, la propriété A.Text est masquée par la propriété B.Text, même dans les contextes où seul l’accesseur set est appelé. En revanche, la propriété B.Count n’est pas accessible à la classe M. Par conséquent, la propriété A.Count accessible est utilisée à la place.

class A
{
    public string Text
    {
        get => "hello";
        set { }
    }

    public int Count
    {
        get => 5;
        set { }
    }
}

class B : A
{
    private string text = "goodbye";
    private int count = 0;

    public new string Text
    {
        get => text;
        protected set => text = value;
    }

    protected new int Count
    {
        get => count;
        set => count = value;
    }
}

class M
{
    static void Main()
    {
        B b = new B();
        b.Count = 12;       // Calls A.Count set accessor
        int i = b.Count;    // Calls A.Count get accessor
        b.Text = "howdy";   // Error, B.Text set accessor not accessible
        string s = b.Text;  // Calls B.Text get accessor
    }
}

exemple de fin

Une fois qu’une propriété ref-valued particulière ou un indexeur ref-valued a été sélectionné ; si l’utilisation est une valeur, la cible d’une affectation simple ou la cible d’une affectation composée ; le domaine d’accessibilité de l’accesseur get impliqué est utilisé pour déterminer si cette utilisation est valide.

Un accesseur utilisé pour implémenter une interface ne doit pas avoir de accessor_modifier. Si un seul accesseur est utilisé pour implémenter une interface, l’autre accesseur peut être déclaré avec un accessor_modifier :

Exemple :

public interface I
{
    string Prop { get; }
}

public class C : I
{
    public string Prop
    {
        get => "April";     // Must not have a modifier here
        internal set {...}  // Ok, because I.Prop has no set accessor
    }
}

exemple de fin

15.7.6 Accesseurs virtuels, scellés, substitués et abstraits

Remarque : Cette clause s’applique aux propriétés (§15.7) et aux indexeurs (§15.9). La clause est écrite en termes de propriétés, lors de la lecture pour les indexeurs remplacent l’indexeur/indexeur pour les propriétés/propriétés et consultent la liste des différences entre les propriétés et les indexeurs donnés dans le §15.9.2. Note de fin

Une déclaration de propriété virtuelle spécifie que les accesseurs de la propriété sont virtuels. Le virtual modificateur s’applique à tous les accesseurs non privés d’une propriété. Lorsqu’un accesseur d’une propriété virtuelle a la privateaccessor_modifier, l’accesseur privé n’est implicitement pas virtuel.

Une déclaration de propriété abstraite spécifie que les accesseurs de la propriété sont virtuels, mais ne fournit pas d’implémentation réelle des accesseurs. Au lieu de cela, les classes dérivées non abstraites sont requises pour fournir leur propre implémentation aux accesseurs en substituant la propriété. Étant donné qu’un accesseur pour une déclaration de propriété abstraite ne fournit aucune implémentation réelle, son accessor_body se compose simplement d’un point-virgule. Une propriété abstraite ne doit pas avoir d’accesseur private .

Une déclaration de propriété qui inclut à la fois les modificateurs et abstract les override modificateurs spécifie que la propriété est abstraite et remplace une propriété de base. Les accesseurs d’une telle propriété sont également abstraits.

Les déclarations de propriété abstraites sont autorisées uniquement dans les classes abstraites (§15.2.2.2). Les accesseurs d’une propriété virtuelle héritée peuvent être substitués dans une classe dérivée en incluant une déclaration de propriété qui spécifie une override directive. Il s’agit d’une déclaration de propriété substituée. Une déclaration de propriété substituée ne déclare pas de nouvelle propriété. Au lieu de cela, il se spécialise simplement dans les implémentations des accesseurs d’une propriété virtuelle existante.

La déclaration de remplacement et la propriété de base substituée doivent avoir la même accessibilité déclarée. En d’autres termes, une déclaration de remplacement ne modifie pas l’accessibilité de la propriété de base. Toutefois, si la propriété de base substituée est protégée interne et qu’elle est déclarée dans un assembly différent de l’assembly contenant la déclaration de remplacement, l’accessibilité déclarée de la déclaration de remplacement doit être protégé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. Il doit y avoir une conversion d’identité entre le type de substitution et la propriété héritée.

Une déclaration de propriété substituée peut inclure le sealed modificateur. L’utilisation de ce modificateur empêche une classe dérivée de remplacer davantage la propriété. Les accesseurs d’une propriété scellée sont également scellés.

À l’exception des différences dans la syntaxe de déclaration et d’appel, les accesseurs virtuels, scellés, substitués et abstraits se comportent exactement comme les méthodes virtuelles, scellées, substituées et abstraites. Plus précisément, les règles décrites dans le §15.6.4, §15.6.5, §15.6.6 et §15.6.7 s’appliquent comme si les accesseurs étaient des méthodes d’un formulaire correspondant :

  • Un accesseur get correspond à une méthode sans paramètre avec une valeur de retour du type de propriété et les mêmes modificateurs que la propriété conteneur.
  • Un accesseur set correspond à une méthode avec un paramètre valeur unique du type de propriété, un type de retour void et les mêmes modificateurs que la propriété conteneur.

Exemple : dans le code suivant

abstract class A
{
    int y;

    public virtual int X
    {
        get => 0;
    }

    public virtual int Y
    {
        get => y;
        set => y = value;
    }

    public abstract int Z { get; set; }
}

X est une propriété en lecture seule virtuelle, Y est une propriété en lecture-écriture virtuelle et Z est une propriété en lecture-écriture abstraite. Étant donné qu’il Z s’agit d’un résumé, la classe A conteneur doit également être déclarée abstraite.

Une classe dérivée de A celle-ci est indiquée ci-dessous :

class B : A
{
    int z;

    public override int X
    {
        get => base.X + 1;
    }

    public override int Y
    {
        set => base.Y = value < 0 ? 0: value;
    }

    public override int Z
    {
        get => z;
        set => z = value;
    }
}

Ici, les déclarations de X, Yet Z substituent les déclarations de propriété. Chaque déclaration de propriété correspond exactement aux modificateurs d’accessibilité, au type et au nom de la propriété héritée correspondante. Accesseur get et X accesseur set d’utilisation du mot clé de Y base pour accéder aux accesseurs hérités. La déclaration des Z remplacements des deux accesseurs abstraits , par conséquent, il n’existe aucun membre de fonction en attente abstract dans B, et B est autorisée à être une classe non abstraite.

exemple de fin

Lorsqu’une propriété est déclarée comme remplacement, tous les accesseurs substitués sont accessibles au code substitué. En outre, l’accessibilité déclarée de la propriété ou de l’indexeur lui-même et des accesseurs doit correspondre à celle du membre et des accesseurs substitués.

Exemple :

public class B
{
    public virtual int P
    {
        get {...}
        protected set {...}
    }
}

public class D: B
{
    public override int P
    {
        get {...}            // Must not have a modifier here
        protected set {...}  // Must specify protected here
    }
}

exemple de fin

15.8 Événements

15.8.1 Général

Un événement est un membre qui permet à un objet ou une classe de fournir des notifications. Les clients peuvent attacher du code exécutable aux événements en fournissant des gestionnaires d’événements.

Les événements sont déclarés à l’aide de event_declarations :

event_declaration
    : attributes? event_modifier* 'event' type variable_declarators ';'
    | attributes? event_modifier* 'event' type member_name
        '{' event_accessor_declarations '}'
    ;

event_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

event_accessor_declarations
    : add_accessor_declaration remove_accessor_declaration
    | remove_accessor_declaration add_accessor_declaration
    ;

add_accessor_declaration
    : attributes? 'add' block
    ;

remove_accessor_declaration
    : attributes? 'remove' block
    ;

unsafe_modifier (§23.2) est disponible uniquement dans le code non sécurisé (§23).

Un event_declaration peut inclure un ensemble d’attributs (§22) et l’un des types autorisés d’accessibilité déclarée (§15.3.6), le new (§15.3.5virtual§15.6.4, §15.8.5), override (§15.6.5, §15.8.5), sealed (§15.6.6), abstract (§15.6.7, §15.8.5) et extern (§15.6.8) modificateurs.

Les déclarations d’événement sont soumises aux mêmes règles que les déclarations de méthode (§15.6) en ce qui concerne les combinaisons valides de modificateurs.

Le type d’une déclaration d’événement doit être un delegate_type (§8.2.8), et que delegate_type doit être au moins aussi accessible que l’événement lui-même (§7.5.5).

Une déclaration d’événement peut inclure des event_accessor_declarations. Toutefois, s’il ne le fait pas, pour les événements non externes et non abstraits, un compilateur doit les fournir automatiquement (§15.8.2) ; pour les événements extern, les accesseurs sont fournis en externe.

Une déclaration d’événement qui omet event_accessor_declarations définit un ou plusieurs événements, un pour chacun des variable_declarators. Les attributs et les modificateurs s’appliquent à tous les membres déclarés par une telle event_declaration.

Il s’agit d’une erreur au moment de la compilation pour une event_declaration d’inclure à la fois le abstract modificateur et les event_accessor_declarations.

Lorsqu’une déclaration d’événement inclut un extern modificateur, l’événement est considéré comme un événement externe. Étant donné qu’une déclaration d’événement externe ne fournit aucune implémentation réelle, il s’agit d’une erreur pour inclure à la fois le extern modificateur et les event_accessor_declarations.

Il s’agit d’une erreur au moment de la compilation d’une variable_declarator d’une déclaration d’événement avec un abstract ou external un modificateur pour inclure un variable_initializer.

Un événement peut être utilisé comme opérande gauche des += opérateurs et -= des opérateurs. Ces opérateurs sont utilisés, respectivement, pour attacher des gestionnaires d’événements à, ou pour supprimer des gestionnaires d’événements d’un événement, et les modificateurs d’accès du contrôle d’événement les contextes dans lesquels ces opérations sont autorisées.

Les seules opérations autorisées sur un événement par code qui se trouve en dehors du type dans lequel cet événement est déclaré, sont += et -=. Par conséquent, bien que ce code puisse ajouter et supprimer des gestionnaires pour un événement, il ne peut pas obtenir ou modifier directement la liste sous-jacente des gestionnaires d’événements.

Dans une opération du formulaire x += y oux –= y, lorsqu’il s’agit x d’un événement, le résultat de l’opération a le type void (§12.21.5) (au lieu d’avoir le type de x, avec la valeur d’après x l’affectation, comme pour les autres +=-= opérateurs définis sur les types non-événement). Cela empêche le code externe d’examiner indirectement le délégué sous-jacent d’un événement.

Exemple : L’exemple suivant montre comment les gestionnaires d’événements sont attachés aux instances de la Button classe :

public delegate void EventHandler(object sender, EventArgs e);

public class Button : Control
{
    public event EventHandler Click;
}

public class LoginDialog : Form
{
    Button okButton;
    Button cancelButton;

    public LoginDialog()
    {
        okButton = new Button(...);
        okButton.Click += new EventHandler(OkButtonClick);
        cancelButton = new Button(...);
        cancelButton.Click += new EventHandler(CancelButtonClick);
    }

    void OkButtonClick(object sender, EventArgs e)
    {
        // Handle okButton.Click event
    }

    void CancelButtonClick(object sender, EventArgs e)
    {
        // Handle cancelButton.Click event
    }
}

Ici, le LoginDialog constructeur d’instance crée deux Button instances et attache des gestionnaires d’événements aux Click événements.

exemple de fin

15.8.2 Événements de type champ

Dans le texte du programme de la classe ou du struct qui contient la déclaration d’un événement, certains événements peuvent être utilisés comme des champs. Pour être utilisé de cette façon, un événement ne doit pas être abstrait ou extern, et n’inclut pas explicitement les event_accessor_declarations. Un tel événement peut être utilisé dans n’importe quel contexte qui autorise un champ. Le champ contient un délégué (§20), qui fait référence à la liste des gestionnaires d’événements qui ont été ajoutés à l’événement. Si aucun gestionnaire d’événements n’a été ajouté, le champ contient null.

Exemple : dans le code suivant

public delegate void EventHandler(object sender, EventArgs e);

public class Button : Control
{
    public event EventHandler Click;

    protected void OnClick(EventArgs e)
    {
        EventHandler handler = Click;
        if (handler != null)
        {
            handler(this, e);
        }
    }

    public void Reset() => Click = null;
}

Click est utilisé comme champ dans la Button classe. Comme l’illustre l’exemple, le champ peut être examiné, modifié et utilisé dans les expressions d’appel de délégué. Méthode OnClick de la Button classe « déclenche » l’événement Click . La notion de déclenchement d’un événement est équivalente à l’appel de délégué représenté par l’événement. Par conséquent, il n’existe aucune construction de langage particulière pour déclencher des événements. Notez que l’appel de délégué est précédé d’une vérification qui garantit que le délégué n’est pas null et que la vérification est effectuée sur une copie locale pour garantir la sécurité des threads.

En dehors de la déclaration de la Button classe, le Click membre ne peut être utilisé que sur le côté gauche des += opérateurs, –= comme dans

b.Click += new EventHandler(...);

qui ajoute un délégué à la liste d’appel de l’événement Click et

Click –= new EventHandler(...);

qui supprime un délégué de la liste d’appel de l’événement Click .

exemple de fin

Lors de la compilation d’un événement de type champ, un compilateur crée automatiquement un stockage pour contenir le délégué et crée des accesseurs pour l’événement qui ajoute ou supprime des gestionnaires d’événements au champ délégué. Les opérations d’ajout et de suppression sont thread safe et peuvent (mais ne sont pas nécessaires) être effectuées tout en maintenant le verrou (§13.13) sur l’objet conteneur d’un événement d’instance, ou l’objet System.Type (§12.8.18) pour un événement statique.

Remarque : Par conséquent, une déclaration d’événement d’instance du formulaire :

class X
{
    public event D Ev;
}

doit être compilé en quelque chose d’équivalent à :

class X
{
    private D __Ev; // field to hold the delegate

    public event D Ev
    {
        add
        {
            /* Add the delegate in a thread safe way */
        }
        remove
        {
            /* Remove the delegate in a thread safe way */
        }
    }
}

Dans la classe X, les références au Ev côté gauche des += opérateurs entraînent –= l’appel des accesseurs d’ajout et de suppression. Toutes les autres références à Ev compiler pour référencer le champ __Ev masqué à la place (§12.8.7). Le nom «__Ev » est arbitraire ; le champ masqué peut avoir n’importe quel nom ou aucun nom du tout.

Note de fin

15.8.3 Accesseurs d’événements

Remarque : Les déclarations d’événements omettent généralement event_accessor_declarations, comme dans l’exemple Button ci-dessus. Par exemple, elles peuvent être incluses si le coût de stockage d’un champ par événement n’est pas acceptable. Dans ce cas, une classe peut inclure des event_accessor_declarationet utiliser un mécanisme privé pour stocker la liste des gestionnaires d’événements. Note de fin

Les event_accessor_declarations d’un événement spécifient les instructions exécutables associées à l’ajout et à la suppression de gestionnaires d’événements.

Les déclarations d’accesseur se composent d’un add_accessor_declaration et d’un remove_accessor_declaration. Chaque déclaration d’accesseur se compose de l’ajout ou de la suppression du jeton suivi d’un bloc. Le bloc associé à un add_accessor_declaration spécifie les instructions à exécuter lorsqu’un gestionnaire d’événements est ajouté, et le bloc associé à un remove_accessor_declaration spécifie les instructions à exécuter lorsqu’un gestionnaire d’événements est supprimé.

Chaque add_accessor_declaration et remove_accessor_declaration correspond à une méthode avec un paramètre valeur unique du type d’événement et un void type de retour. Le paramètre implicite d’un accesseur d’événement est nommé value. Lorsqu’un événement est utilisé dans une attribution d’événement, l’accesseur d’événement approprié est utilisé. Plus précisément, si l’opérateur d’affectation est += alors l’accesseur d’ajout est utilisé, et si l’opérateur d’affectation est –= alors l’accesseur de suppression est utilisé. Dans les deux cas, l’opérande droit de l’opérateur d’affectation est utilisé comme argument de l’accesseur d’événement. Le bloc d’une add_accessor_declaration ou d’un remove_accessor_declaration doit respecter les règles des méthodes décrites dans le void. En particulier, return les instructions d’un tel bloc ne sont pas autorisées à spécifier une expression.

Étant donné qu’un accesseur d’événements a implicitement un paramètre nommé value, il s’agit d’une erreur au moment de la compilation d’une variable locale ou d’une constante déclarée dans un accesseur d’événements pour avoir ce nom.

Exemple : dans le code suivant


class Control : Component
{
    // Unique keys for events
    static readonly object mouseDownEventKey = new object();
    static readonly object mouseUpEventKey = new object();

    // Return event handler associated with key
    protected Delegate GetEventHandler(object key) {...}

    // Add event handler associated with key
    protected void AddEventHandler(object key, Delegate handler) {...}

    // Remove event handler associated with key
    protected void RemoveEventHandler(object key, Delegate handler) {...}

    // MouseDown event
    public event MouseEventHandler MouseDown
    {
        add { AddEventHandler(mouseDownEventKey, value); }
        remove { RemoveEventHandler(mouseDownEventKey, value); }
    }

    // MouseUp event
    public event MouseEventHandler MouseUp
    {
        add { AddEventHandler(mouseUpEventKey, value); }
        remove { RemoveEventHandler(mouseUpEventKey, value); }
    }

    // Invoke the MouseUp event
    protected void OnMouseUp(MouseEventArgs args)
    {
        MouseEventHandler handler;
        handler = (MouseEventHandler)GetEventHandler(mouseUpEventKey);
        if (handler != null)
        {
            handler(this, args);
        }
    }
}

la Control classe implémente un mécanisme de stockage interne pour les événements. La AddEventHandler méthode associe une valeur de délégué à une clé, la GetEventHandler méthode retourne le délégué actuellement associé à une clé et la RemoveEventHandler méthode supprime un délégué en tant que gestionnaire d’événements pour l’événement spécifié. Probablement, le mécanisme de stockage sous-jacent est conçu de telle sorte qu’il n’y a aucun coût pour associer une valeur de délégué Null à une clé, et par conséquent, les événements non gérés ne consomment aucun stockage.

exemple de fin

15.8.4 Événements statiques et d’instance

Lorsqu’une déclaration d’événement inclut un static modificateur, l’événement est considéré comme un événement statique. Lorsqu’aucun modificateur n’est static présent, l’événement est considéré comme un événement d’instance.

Un événement statique n’est pas associé à une instance spécifique, et il s’agit d’une erreur au moment de la compilation à this laquelle faire référence dans les accesseurs d’un événement statique.

Un événement d’instance est associé à une instance donnée d’une classe, et cette instance est accessible en tant que this (§12.8.14) dans les accesseurs de cet événement.

Les différences entre les membres statiques et d’instance sont abordées plus loin dans le §15.3.8.

15.8.5 Accesseurs virtuels, scellés, substitués et abstraits

Une déclaration d’événement virtuel spécifie que les accesseurs de cet événement sont virtuels. Le virtual modificateur s’applique aux deux accesseurs d’un événement.

Une déclaration d’événement abstraite spécifie que les accesseurs de l’événement sont virtuels, mais ne fournit pas d’implémentation réelle des accesseurs. Au lieu de cela, les classes dérivées non abstraites sont requises pour fournir leur propre implémentation aux accesseurs en substituant l’événement. Étant donné qu’un accesseur pour une déclaration d’événement abstraite ne fournit aucune implémentation réelle, il ne fournit pas de event_accessor_declarations.

Une déclaration d’événement qui inclut à la fois les modificateurs et abstract les override modificateurs spécifie que l’événement est abstrait et remplace un événement de base. Les accesseurs d’un tel événement sont également abstraits.

Les déclarations d’événement abstraites sont autorisées uniquement dans les classes abstraites (§15.2.2.2).

Les accesseurs d’un événement virtuel hérité peuvent être substitués dans une classe dérivée en incluant une déclaration d’événement qui spécifie un override modificateur. Il s’agit d’une déclaration d’événement de substitution. Une déclaration d’événement de substitution ne déclare pas un nouvel événement. Au lieu de cela, il se spécialise simplement dans les implémentations des accesseurs d’un événement virtuel existant.

Une déclaration d’événement de substitution spécifie exactement les mêmes modificateurs d’accessibilité et le même nom que l’événement substitué, il doit y avoir une conversion d’identité entre le type de substitution et l’événement substitué, et les accesseurs d’ajout et de suppression doivent être spécifiés dans la déclaration.

Une déclaration d’événement de substitution peut inclure le sealed modificateur. L’utilisation du this modificateur empêche une classe dérivée de remplacer davantage l’événement. Les accesseurs d’un événement scellé sont également scellés.

Il s’agit d’une erreur au moment de la compilation pour qu’une déclaration d’événement de substitution inclue un new modificateur.

À l’exception des différences dans la syntaxe de déclaration et d’appel, les accesseurs virtuels, scellés, substitués et abstraits se comportent exactement comme les méthodes virtuelles, scellées, substituées et abstraites. Plus précisément, les règles décrites dans le §15.6.4, §15.6.5, §15.6.6 et §15.6.7 s’appliquent comme si les accesseurs étaient des méthodes d’un formulaire correspondant. Chaque accesseur correspond à une méthode avec un paramètre valeur unique du type d’événement, un void type de retour et les mêmes modificateurs que l’événement conteneur.

15.9 Indexeurs

15.9.1 Général

Un indexeur est un membre qui permet à un objet d’être indexé de la même façon qu’un tableau. Les indexeurs sont déclarés à l’aide de indexer_declarations :

indexer_declaration
    : attributes? indexer_modifier* indexer_declarator indexer_body
    | attributes? indexer_modifier* ref_kind indexer_declarator ref_indexer_body
    ;

indexer_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

indexer_declarator
    : type 'this' '[' parameter_list ']'
    | type interface_type '.' 'this' '[' parameter_list ']'
    ;

indexer_body
    : '{' accessor_declarations '}' 
    | '=>' expression ';'
    ;  

ref_indexer_body
    : '{' ref_get_accessor_declaration '}'
    | '=>' 'ref' variable_reference ';'
    ;

unsafe_modifier (§23.2) est disponible uniquement dans le code non sécurisé (§23).

Il existe deux types de indexer_declaration :

  • Le premier déclare un indexeur non-ref-valued. Sa valeur a un type de type. Ce type d’indexeur peut être lisible et/ou accessible en écriture.
  • La seconde déclare un indexeur ref-valued. Sa valeur est un variable_reference (§9.5readonly Ce type d’indexeur est lisible uniquement.

Un indexer_declaration peut inclure un ensemble d’attributs (§22) et l’un des types autorisés d’accessibilité déclarée (§15.3.6), le new (§15.3.5), (§15.15) virtual .6.4), override (§15.6.5), sealed (§15.6.6), abstract (§15.6.7) et extern (§15.6.8) modificateurs.

Les déclarations d’indexeur sont soumises aux mêmes règles que les déclarations de méthode (§15.6) en ce qui concerne les combinaisons valides de modificateurs, à l’exception du fait que le static modificateur n’est pas autorisé sur une déclaration d’indexeur.

Le type d’une déclaration d’indexeur spécifie le type d’élément de l’indexeur introduit par la déclaration.

Remarque : à mesure que les indexeurs sont conçus pour être utilisés dans des contextes de type élément de tableau, le type d’élément de terme tel que défini pour un tableau est également utilisé avec un indexeur. Note de fin

Sauf si l’indexeur est une implémentation de membre d’interface explicite, le type est suivi du mot clé this. Pour une implémentation de membre d’interface explicite, le type est suivi d’un interface_type, d’un «. » et du mot clé this. Contrairement aux autres membres, les indexeurs n’ont pas de noms définis par l’utilisateur.

Le parameter_list spécifie les paramètres de l’indexeur. La liste des paramètres d’un indexeur correspond à celle d’une méthode (§15.6.2), sauf qu’au moins un paramètre doit être spécifié et que les thismodificateurs , refet out les modificateurs de paramètres ne sont pas autorisés.

Le type d’indexeur et chacun des types référencés dans la parameter_list doit être au moins aussi accessible que l’indexeur lui-même (§7.5.5).

Une indexer_body peut être constituée d’un corps d’instruction (§15.7.1) ou d’un corps d’expression (§15.6.1). Dans un corps d’instruction, accessor_declarations, qui doit être placé dans des jetons «{ » et «} », déclarez les accesseurs (§15.7.3) de l’indexeur. Les accesseurs spécifient les instructions exécutables associées à la lecture et à l’écriture d’éléments d’indexeur.

Dans un indexer_body un corps d’expression composé de «=> » suivi d’une expression E et d’un point-virgule équivaut exactement au corps { get { return E; } }de l’instruction, et ne peut donc être utilisé que pour spécifier des indexeurs en lecture seule où le résultat de l’accesseur get est donné par une seule expression.

Une ref_indexer_body peut être constituée d’un corps d’instruction ou d’un corps d’expression. Dans un corps d’instruction, un get_accessor_declaration déclare l’accesseur get (§15.7.3) de l’indexeur. L’accesseur spécifie les instructions exécutables associées à la lecture de l’indexeur.

Dans un ref_indexer_body un corps d’expression composé de =>ref suivi, un variable_referenceV et un point-virgule équivaut exactement au corps { get { return ref V; } }de l’instruction .

Remarque : même si la syntaxe d’accès à un élément d’indexeur est identique à celle d’un élément de tableau, un élément d’indexeur n’est pas classé comme variable. Par conséquent, il n’est pas possible de passer un élément d’indexeur en tant qu’argument in, outsauf ref si l’indexeur est ref-valued et retourne donc une référence (§9.7). Note de fin

La parameter_list d’un indexeur définit la signature (§7.6) de l’indexeur. Plus précisément, la signature d’un indexeur se compose du nombre et des types de ses paramètres. Le type d’élément et les noms des paramètres ne font pas partie de la signature d’un indexeur.

La signature d’un indexeur diffère des signatures de tous les autres indexeurs déclarés dans la même classe.

Lorsqu’une déclaration d’indexeur inclut un extern modificateur, l’indexeur est dit être un indexeur externe. Étant donné qu’une déclaration d’indexeur externe ne fournit aucune implémentation réelle, chacun des accessor_bodys dans son accessor_declarations doit être un point-virgule.

Exemple : L’exemple ci-dessous déclare une BitArray classe qui implémente un indexeur pour accéder aux bits individuels dans le tableau de bits.

class BitArray
{
    int[] bits;
    int length;

    public BitArray(int length)
    {
        if (length < 0)
        {
            throw new ArgumentException();
        }
        bits = new int[((length - 1) >> 5) + 1];
        this.length = length;
    }

    public int Length => length;

    public bool this[int index]
    {
        get
        {
            if (index < 0 || index >= length)
            {
                throw new IndexOutOfRangeException();
            }
            return (bits[index >> 5] & 1 << index) != 0;
        }
        set
        {
            if (index < 0 || index >= length)
            {
                throw new IndexOutOfRangeException();
            }
            if (value)
            {
                bits[index >> 5] |= 1 << index;
            }
            else
            {
                bits[index >> 5] &= ~(1 << index);
            }
        }
    }
}

Une instance de la BitArray classe consomme considérablement moins de mémoire qu’une valeur correspondante bool[] (étant donné que chaque valeur de l’ancien occupe un seul bit au lieu de celle de bytece dernier), mais elle autorise les mêmes opérations qu’un bool[].

La classe suivante CountPrimes utilise un BitArray algorithme classique « sieve » pour calculer le nombre de primes comprises entre 2 et un maximum donné :

class CountPrimes
{
    static int Count(int max)
    {
        BitArray flags = new BitArray(max + 1);
        int count = 0;
        for (int i = 2; i <= max; i++)
        {
            if (!flags[i])
            {
                for (int j = i * 2; j <= max; j += i)
                {
                    flags[j] = true;
                }
                count++;
            }
        }
        return count;
    }

    static void Main(string[] args)
    {
        int max = int.Parse(args[0]);
        int count = Count(max);
        Console.WriteLine($"Found {count} primes between 2 and {max}");
    }
}

Notez que la syntaxe d’accès aux éléments du fichier BitArray est exactement la même que pour un bool[].

L’exemple suivant montre une classe grid 26×10 qui a un indexeur avec deux paramètres. Le premier paramètre doit être une lettre majuscule ou minuscule dans la plage A-Z, et la seconde doit être un entier de la plage 0 à 9.

class Grid
{
    const int NumRows = 26;
    const int NumCols = 10;
    int[,] cells = new int[NumRows, NumCols];

    public int this[char row, int col]
    {
        get
        {
            row = Char.ToUpper(row);
            if (row < 'A' || row > 'Z')
            {
                throw new ArgumentOutOfRangeException("row");
            }
            if (col < 0 || col >= NumCols)
            {
                throw new ArgumentOutOfRangeException ("col");
            }
            return cells[row - 'A', col];
        }
        set
        {
            row = Char.ToUpper(row);
            if (row < 'A' || row > 'Z')
            {
                throw new ArgumentOutOfRangeException ("row");
            }
            if (col < 0 || col >= NumCols)
            {
                throw new ArgumentOutOfRangeException ("col");
            }
            cells[row - 'A', col] = value;
        }
    }
}

exemple de fin

15.9.2 Indexeur et différences de propriété

Les indexeurs et les propriétés sont très similaires dans le concept, mais diffèrent de la manière suivante :

  • Une propriété est identifiée par son nom, tandis qu’un indexeur est identifié par sa signature.
  • Une propriété est accessible via un simple_name (§12.8.4) ou un member_access (§12.8.7), tandis qu’un élément d’indexeur est accessible via un element_access (§12.8.12.3).
  • Une propriété peut être un membre statique, tandis qu’un indexeur est toujours un membre d’instance.
  • Un accesseur get d’une propriété correspond à une méthode sans paramètres, tandis qu’un accesseur get d’un indexeur correspond à une méthode avec la même liste de paramètres que l’indexeur.
  • Un accesseur set d’une propriété correspond à une méthode avec un paramètre unique nommé value, tandis qu’un accesseur set d’un indexeur correspond à une méthode avec la même liste de paramètres que l’indexeur, ainsi qu’un paramètre supplémentaire nommé value.
  • Il s’agit d’une erreur au moment de la compilation pour qu’un accesseur d’indexeur déclare une variable locale ou une constante locale portant le même nom qu’un paramètre d’indexeur.
  • Dans une déclaration de propriété substituée, la propriété héritée est accessible à l’aide de la syntaxe base.P, où P se trouve le nom de la propriété. Dans une déclaration d’indexeur substituée, l’indexeur hérité est accessible à l’aide de la syntaxe base[E], où E est une liste séparée par des virgules d’expressions.
  • Il n’existe aucun concept d’indexeur « implémenté automatiquement ». Il s’agit d’une erreur d’avoir un indexeur non abstrait et non externe avec des points-virgules accessor_bodys.

Outre ces différences, toutes les règles définies au §15.7.3, §15.7.5 et §15.7.6 s’appliquent aux accesseurs d’indexeur ainsi qu’aux accesseurs de propriété.

Ce remplacement des propriétés/propriétés par indexeur/indexeurs lors de la lecture de §15.7.3, §15.7.5 et §15.7.6 s’applique également aux termes définis. Plus précisément, la propriété en lecture-écriture devient un indexeur en lecture seule, la propriété en lecture seule devient indexeur en lecture seule et la propriété en écriture seule devient indexeur en écriture seule.

15.10 Opérateurs

15.10.1 Général

Un opérateur est un membre qui définit la signification d’un opérateur d’expression qui peut être appliqué aux instances de la classe. Les opérateurs sont déclarés à l’aide de operator_declarations :

operator_declaration
    : attributes? operator_modifier+ operator_declarator operator_body
    ;

operator_modifier
    : 'public'
    | 'static'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

operator_declarator
    : unary_operator_declarator
    | binary_operator_declarator
    | conversion_operator_declarator
    ;

unary_operator_declarator
    : type 'operator' overloadable_unary_operator '(' fixed_parameter ')'
    ;

logical_negation_operator
    : '!'
    ;

overloadable_unary_operator
    : '+' | '-' | logical_negation_operator | '~' | '++' | '--' | 'true' | 'false'
    ;

binary_operator_declarator
    : type 'operator' overloadable_binary_operator
        '(' fixed_parameter ',' fixed_parameter ')'
    ;

overloadable_binary_operator
    : '+'  | '-'  | '*'  | '/'  | '%'  | '&' | '|' | '^'  | '<<' 
    | right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
    ;

conversion_operator_declarator
    : 'implicit' 'operator' type '(' fixed_parameter ')'
    | 'explicit' 'operator' type '(' fixed_parameter ')'
    ;

operator_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier (§23.2) est disponible uniquement dans le code non sécurisé (§23).

Remarque : Les opérateurs de négation logique de préfixe (§12.9.4) et de postfix null-forgiving (§12.8.9), représentés par le même jeton lexical (!), sont distincts. Ce dernier n’est pas un opérateur surchargé. Note de fin

Il existe trois catégories d’opérateurs surchargés : opérateurs unaires (§15.10.2), opérateurs binaires (§15.10.3) et opérateurs de conversion (§15.10.4).

Le operator_body est un point-virgule, un corps de bloc (§15.6.1) ou un corps d’expression (§15.6.1). Un corps de bloc se compose d’un bloc, qui spécifie les instructions à exécuter lorsque l’opérateur est appelé. Le bloc doit respecter les règles relatives aux méthodes de retour de valeur décrites dans le §15.6.11. Un corps d’expression se compose d’une => expression et d’un point-virgule, et désigne une expression unique à effectuer lorsque l’opérateur est appelé.

Pour extern les opérateurs, le operator_body se compose simplement d’un point-virgule. Pour tous les autres opérateurs, la operator_body est un corps de bloc ou un corps d’expression.

Les règles suivantes s’appliquent à toutes les déclarations d’opérateur :

  • Une déclaration d’opérateur doit inclure à la fois un modificateur et un publicstatic modificateur.
  • Le ou les paramètres d’un opérateur ne doivent pas avoir de modificateurs autres que in.
  • La signature d’un opérateur (§15.10.2, §15.10.3, §15.10.4) diffère des signatures de tous les autres opérateurs déclarés dans la même classe.
  • Tous les types référencés dans une déclaration d’opérateur doivent être au moins aussi accessibles que l’opérateur lui-même (§7.5.5).
  • Il s’agit d’une erreur pour que le même modificateur apparaisse plusieurs fois dans une déclaration d’opérateur.

Chaque catégorie d’opérateur impose des restrictions supplémentaires, comme décrit dans les sous-sections suivantes.

Comme d’autres membres, les opérateurs déclarés dans une classe de base sont hérités par des classes dérivées. Étant donné que les déclarations d’opérateur nécessitent toujours la classe ou le struct dans lequel l’opérateur est déclaré pour participer à la signature de l’opérateur, il n’est pas possible qu’un opérateur déclaré dans une classe dérivée masque un opérateur déclaré dans une classe de base. Ainsi, le new modificateur n’est jamais requis et n’est donc jamais autorisé, dans une déclaration d’opérateur.

Des informations supplémentaires sur les opérateurs unaires et binaires sont disponibles dans le §12.4.

Des informations supplémentaires sur les opérateurs de conversion sont disponibles dans le §10.5.

15.10.2 Opérateurs unaires

Les règles suivantes s’appliquent aux déclarations d’opérateur unaire, où T désigne le type d’instance de la classe ou du struct qui contient la déclaration d’opérateur :

  • Un unaire +, , -!(négation logique uniquement), ou ~ opérateur doit prendre un seul paramètre de type T ou T? peut retourner n’importe quel type.
  • Un opérateur ou unaire ++ doit prendre un seul paramètre de type -- ou T retourner ce même type ou un type dérivé de celui-ci.T?
  • Un opérateur ou unaire true doit prendre un seul paramètre de type false ou T doit retourner un type T?.bool

La signature d’un opérateur unaire se compose du jeton d’opérateur (+, , -!, ~, ++, --, true, ou false) et du type du paramètre unique. Le type de retour ne fait pas partie de la signature d’un opérateur unaire, ni le nom du paramètre.

Les true opérateurs unaires et false les opérateurs nécessitent une déclaration appairée. Une erreur au moment de la compilation se produit si une classe déclare l’un de ces opérateurs sans également déclarer l’autre. Les true opérateurs et false les opérateurs sont décrits plus loin dans le §12.24.

Exemple : L’exemple suivant montre une implémentation et une utilisation ultérieure d’opérateur++ pour une classe de vecteur entier :

public class IntVector
{
    public IntVector(int length) {...}
    public int Length { get { ... } }                      // Read-only property
    public int this[int index] { get { ... } set { ... } } // Read-write indexer

    public static IntVector operator++(IntVector iv)
    {
        IntVector temp = new IntVector(iv.Length);
        for (int i = 0; i < iv.Length; i++)
        {
            temp[i] = iv[i] + 1;
        }
        return temp;
    }
}

class Test
{
    static void Main()
    {
        IntVector iv1 = new IntVector(4); // Vector of 4 x 0
        IntVector iv2;
        iv2 = iv1++;              // iv2 contains 4 x 0, iv1 contains 4 x 1
        iv2 = ++iv1;              // iv2 contains 4 x 2, iv1 contains 4 x 2
    }
}

Notez comment la méthode d’opérateur retourne la valeur produite en ajoutant 1 à l’opérande, tout comme les opérateurs d’incrémentation et de décrémentation postfix (§12.8.16), ainsi que les opérateurs d’incrémentation et de décrémentation de préfixe (§12.9.6). Contrairement à C++, cette méthode ne doit pas modifier la valeur de son opérande directement, car elle enfreint la sémantique standard de l’opérateur d’incrémentation postfix (§12.8.16).

exemple de fin

15.10.3 Opérateurs binaires

Les règles suivantes s’appliquent aux déclarations d’opérateur binaire, où T désigne le type d’instance de la classe ou du struct qui contient la déclaration d’opérateur :

  • Un opérateur binaire sans décalage doit prendre deux paramètres, dont au moins un doit avoir un type T ou T?, et peut retourner n’importe quel type.
  • Un binaire << ou >> opérateur (§12.11) doit prendre deux paramètres, dont le premier doit avoir un type T ou T ? et le deuxième doit avoir un type int ou int?, et peut retourner n’importe quel type.

La signature d’un opérateur binaire se compose du jeton d’opérateur (+, -, *, /, %, &, |^<<>>==, !=, >, <>=ou <=) et des types des deux paramètres. Le type de retour et les noms des paramètres ne font pas partie de la signature d’un opérateur binaire.

Certains opérateurs binaires nécessitent une déclaration appairée. Pour chaque déclaration d’un opérateur d’une paire, il doit y avoir une déclaration correspondante de l’autre opérateur de la paire. Deux déclarations d’opérateur correspondent si les conversions d’identité existent entre leurs types de retour et leurs types de paramètres correspondants. Les opérateurs suivants nécessitent une déclaration appairée :

  • opérateur et opérateur ==!=
  • opérateur et opérateur ><
  • opérateur et opérateur >=<=

15.10.4 Opérateurs de conversion

Une déclaration d’opérateur de conversion introduit une conversion définie par l’utilisateur (§10.5), qui augmente les conversions implicites et explicites prédéfinies.

Une déclaration d’opérateur de conversion qui inclut le mot clé introduit une conversion implicite définie par l’utilisateur implicit . Les conversions implicites peuvent se produire dans diverses situations, notamment les appels de membres de fonction, les expressions de cast et les affectations. Cette procédure est décrite plus loin dans le §10.2.

Une déclaration d’opérateur de conversion qui inclut le mot clé introduit une conversion explicite définie par l’utilisateur explicit . Les conversions explicites peuvent se produire dans les expressions de cast et sont décrites plus loin dans le §10.3.

Un opérateur de conversion convertit à partir d’un type source, indiqué par le type de paramètre de l’opérateur de conversion, en type cible, indiqué par le type de retour de l’opérateur de conversion.

Pour un type source et un type STcible donnés, s’il S s’agit de T types valeur nullables, laissez S₀T₀ et faites référence à leurs types sous-jacents ; sinon, S₀ et sont égaux T₀S et T respectivement. Une classe ou un struct est autorisé à déclarer une conversion d’un type source vers un type ST cible uniquement si toutes les valeurs suivantes sont vraies :

  • S₀ et T₀ sont des types différents.

  • S₀ Ou T₀ est le type d’instance de la classe ou du struct qui contient la déclaration d’opérateur.

  • Ni aucun S₀ n’est T₀ un interface_type.

  • À l’exclusion des conversions définies par l’utilisateur, une conversion n’existe pas depuis S ou T depuis T vers S.

Pour les besoins de ces règles, tous les paramètres de type associés S ou T sont considérés comme des types uniques qui n’ont aucune relation d’héritage avec d’autres types, et toutes les contraintes sur ces paramètres de type sont ignorées.

Exemple : Dans les éléments suivants :

class C<T> {...}

class D<T> : C<T>
{
    public static implicit operator C<int>(D<T> value) {...}     // Ok
    public static implicit operator C<string>(D<T> value) {...}  // Ok
    public static implicit operator C<T>(D<T> value) {...}       // Error
}

les deux premières déclarations d’opérateur sont autorisées, car T et intstring, respectivement, sont considérées comme des types uniques sans relation. Toutefois, le troisième opérateur est une erreur, car C<T> il s’agit de la classe de base de D<T>.

exemple de fin

À partir de la deuxième règle, il suit qu’un opérateur de conversion doit effectuer une conversion vers ou à partir du type de classe ou de struct dans lequel l’opérateur est déclaré.

Exemple : il est possible pour une classe ou un type C de struct de définir une conversion de vers et de Cint vers intC, mais pas de int vers bool. exemple de fin

Il n’est pas possible de redéfinir directement une conversion prédéfinie. Par conséquent, les opérateurs de conversion ne sont pas autorisés à effectuer une conversion depuis ou vers, object car des conversions implicites et explicites existent déjà entre object et tous les autres types. De même, ni la source ni les types cibles d’une conversion ne peuvent être un type de base de l’autre, car une conversion existe déjà. Toutefois, il est possible de déclarer des opérateurs sur des types génériques qui, pour des arguments de type particuliers, spécifient des conversions qui existent déjà en tant que conversions prédéfinies.

Exemple :

struct Convertible<T>
{
    public static implicit operator Convertible<T>(T value) {...}
    public static explicit operator T(Convertible<T> value) {...}
}

lorsque le type object est spécifié comme argument de type pour T, le deuxième opérateur déclare une conversion qui existe déjà (implicite, et par conséquent, une conversion explicite existe de n’importe quel type en objet de type).

exemple de fin

Dans les cas où une conversion prédéfinie existe entre deux types, toutes les conversions définies par l’utilisateur entre ces types sont ignorées. Plus précisément :

  • Si une conversion implicite prédéfinie (§10.2) existe d’un type à un type ST, toutes les conversions définies par l’utilisateur (implicites ou explicites) sont ST ignorées.
  • Si une conversion explicite prédéfinie (§10.3) existe d’un type à un type ST, toutes les conversions explicites définies par l’utilisateur sont ST ignorées. En outre:
    • Si l’un S ou T l’autre est un type d’interface, les conversions implicites définies par l’utilisateur sont ST ignorées.
    • Sinon, les conversions implicites définies par l’utilisateur sont ST toujours prises en compte.

Pour tous les types, mais object, les opérateurs déclarés par le Convertible<T> type ci-dessus ne sont pas en conflit avec les conversions prédéfinies.

Exemple :

void F(int i, Convertible<int> n)
{
    i = n;                    // Error
    i = (int)n;               // User-defined explicit conversion
    n = i;                    // User-defined implicit conversion
    n = (Convertible<int>)i;  // User-defined implicit conversion
}

Toutefois, pour le type object, les conversions prédéfinies masquent les conversions définies par l’utilisateur dans tous les cas, mais un :

void F(object o, Convertible<object> n)
{
    o = n;                       // Pre-defined boxing conversion
    o = (object)n;               // Pre-defined boxing conversion
    n = o;                       // User-defined implicit conversion
    n = (Convertible<object>)o;  // Pre-defined unboxing conversion
}

exemple de fin

Les conversions définies par l’utilisateur ne sont pas autorisées à effectuer une conversion depuis ou vers interface_types. En particulier, cette restriction garantit qu’aucune transformation définie par l’utilisateur ne se produit lors de la conversion en interface_type, et qu’une conversion vers un interface_type réussit uniquement si la conversion en cours implémente réellement le object interface_type spécifié.

La signature d’un opérateur de conversion se compose du type source et du type cible. (Il s’agit de la seule forme de membre pour laquelle le type de retour participe à la signature.) La classification implicite ou explicite d’un opérateur de conversion ne fait pas partie de la signature de l’opérateur. Par conséquent, une classe ou un struct ne peut pas déclarer à la fois un opérateur de conversion implicite et explicite avec les mêmes types source et cible.

Remarque : En général, les conversions implicites définies par l’utilisateur doivent être conçues pour ne jamais lever d’exceptions et ne jamais perdre des informations. Si une conversion définie par l’utilisateur peut donner lieu à des exceptions (par exemple, parce que l’argument source est hors limites) ou à une perte d’informations (comme l’abandon de bits de commande élevée), cette conversion doit être définie comme une conversion explicite. Note de fin

Exemple : dans le code suivant

public struct Digit
{
    byte value;

    public Digit(byte value)
    {
        if (value < 0 || value > 9)
        {
            throw new ArgumentException();
        }
        this.value = value;
    }

    public static implicit operator byte(Digit d) => d.value;
    public static explicit operator Digit(byte b) => new Digit(b);
}

la conversion de Digit vers byte est implicite, car elle ne lève jamais d’exceptions ou perd des informations, mais la conversion en byte est Digit explicite, car Digit elle ne peut représenter qu’un sous-ensemble des valeurs possibles d’un byte.

exemple de fin

15.11 Constructeurs d’instance

15.11.1 Général

Un constructeur d’instance est un membre qui implémente les actions requises pour initialiser une instance d’une classe. Les constructeurs d’instance sont déclarés à l’aide de constructor_declarations :

constructor_declaration
    : attributes? constructor_modifier* constructor_declarator constructor_body
    ;

constructor_modifier
    : 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

constructor_declarator
    : identifier '(' parameter_list? ')' constructor_initializer?
    ;

constructor_initializer
    : ':' 'base' '(' argument_list? ')'
    | ':' 'this' '(' argument_list? ')'
    ;

constructor_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier (§23.2) est disponible uniquement dans le code non sécurisé (§23).

Un constructor_declaration peut inclure un ensemble d’attributs (§22), l’un des types autorisés d’accessibilité déclarée (§15.3.6) et un extern modificateur (§15.6.8). Une déclaration de constructeur n’est pas autorisée à inclure le même modificateur plusieurs fois.

L’identificateur d’un constructor_declarator nomme la classe dans laquelle le constructeur d’instance est déclaré. Si un autre nom est spécifié, une erreur au moment de la compilation se produit.

La parameter_list facultative d’un constructeur d’instance est soumise aux mêmes règles que les parameter_list d’une méthode (§15.6). Comme le this modificateur pour les paramètres s’applique uniquement aux méthodes d’extension (§15.6.10), aucun paramètre dans le parameter_list d’un constructeur ne doit contenir le this modificateur. La liste des paramètres définit la signature (§7.6) d’un constructeur d’instance et régit le processus par lequel la résolution de surcharge (§12.6.4) sélectionne un constructeur d’instance particulier dans un appel.

Chacun des types référencés dans la parameter_list d’un constructeur d’instance doit être au moins aussi accessible que le constructeur lui-même (§7.5.5).

Le constructor_initializer facultatif spécifie un autre constructeur d’instance à appeler avant d’exécuter les instructions fournies dans le constructor_body de ce constructeur d’instance. Cette procédure est décrite plus loin dans le §15.11.2.

Lorsqu’une déclaration de constructeur inclut un extern modificateur, le constructeur est dit être un constructeur externe. Étant donné qu’une déclaration de constructeur externe ne fournit aucune implémentation réelle, son constructor_body se compose d’un point-virgule. Pour tous les autres constructeurs, la constructor_body se compose de l’une ou l’autre

  • un bloc, qui spécifie les instructions à initialiser une nouvelle instance de la classe ; ou
  • un corps d’expression, qui se compose d’une => expression et d’un point-virgule, et désigne une expression unique pour initialiser une nouvelle instance de la classe.

Un constructor_body qui est un corps de bloc ou d’expression correspond exactement au bloc d’une méthode d’instance avec un void type de retour (§15.6.11).

Les constructeurs d’instance ne sont pas hérités. Par conséquent, une classe n’a aucun constructeur d’instance autre que ceux réellement déclarés dans la classe, à l’exception qu’une classe ne contient aucune déclaration de constructeur d’instance, un constructeur d’instance par défaut est automatiquement fourni (§15.11.5).

Les constructeurs d’instance sont appelés par object_creation_expression s (§12.8.17.2) et par constructor_initializers.

15.11.2 Initialiseurs de constructeur

Tous les constructeurs d’instance (à l’exception de ceux de la classe object) incluent implicitement un appel d’un autre constructeur d’instance juste avant le constructor_body. Le constructeur à appeler implicitement est déterminé par l’constructor_initializer :

  • Un initialiseur de constructeur d’instance du formulaire base(argument_list) (où argument_list est facultatif) entraîne l’appel d’un constructeur d’instance à partir de la classe de base directe. Ce constructeur est sélectionné à l’aide de argument_list et des règles de résolution de surcharge de §12.6.4. L’ensemble de constructeurs d’instances candidates se compose de tous les constructeurs d’instance accessibles de la classe de base directe. Si ce jeu est vide ou si un constructeur d’instance unique ne peut pas être identifié, une erreur au moment de la compilation se produit.
  • Un initialiseur de constructeur d’instance du formulaire this(argument_list) (où argument_list est facultatif) appelle un autre constructeur d’instance à partir de la même classe. Le constructeur est sélectionné à l’aide de argument_list et des règles de résolution de surcharge de §12.6.4. L’ensemble de constructeurs d’instances candidates se compose de tous les constructeurs d’instance déclarés dans la classe elle-même. Si l’ensemble obtenu de constructeurs d’instances applicables est vide ou si un seul constructeur d’instance ne peut pas être identifié, une erreur au moment de la compilation se produit. Si une déclaration de constructeur d’instance s’appelle par le biais d’une chaîne d’initialiseurs d’un ou plusieurs constructeurs, une erreur au moment de la compilation se produit.

Si un constructeur d’instance n’a pas d’initialiseur de constructeur, un initialiseur de constructeur du formulaire base() est implicitement fourni.

Remarque : Par conséquent, une déclaration de constructeur d’instance du formulaire

C(...) {...}

est exactement équivalent à

C(...) : base() {...}

Note de fin

L’étendue des paramètres donnés par la parameter_list d’une déclaration de constructeur d’instance inclut l’initialiseur du constructeur de cette déclaration. Par conséquent, un initialiseur de constructeur est autorisé à accéder aux paramètres du constructeur.

Exemple :

class A
{
    public A(int x, int y) {}
}

class B: A
{
    public B(int x, int y) : base(x + y, x - y) {}
}

exemple de fin

Un initialiseur de constructeur d’instance ne peut pas accéder à l’instance en cours de création. Par conséquent, il s’agit d’une erreur au moment de la compilation pour référencer ceci dans une expression d’argument de l’initialiseur du constructeur, car il s’agit d’une erreur au moment de la compilation d’une expression d’argument pour référencer n’importe quel membre d’instance via un simple_name.

15.11.3 Initialiseurs de variables d’instance

Lorsqu’un constructeur d’instance n’a pas d’initialiseur de constructeur ou qu’il a un initialiseur de constructeur du formulaire base(...), ce constructeur effectue implicitement les initialisations spécifiées par les variable_initializerdes champs d’instance déclarés dans sa classe. Cela correspond à une séquence d’affectations exécutées immédiatement lors de l’entrée au constructeur et avant l’appel implicite du constructeur de classe de base directe. Les initialiseurs de variables sont exécutés dans l’ordre textuel dans lequel ils apparaissent dans la déclaration de classe (§15.5.6).

15.11.4 Exécution du constructeur

Les initialiseurs de variables sont transformés en instructions d’affectation, et ces instructions d’affectation sont exécutées avant l’appel du constructeur d’instance de classe de base. Cet ordre garantit que tous les champs d’instance sont initialisés par leurs initialiseurs de variables avant toute instruction ayant accès à cette instance.

Exemple : Étant donné les éléments suivants :

class A
{
    public A()
    {
        PrintFields();
    }

    public virtual void PrintFields() {}
}
class B: A
{
    int x = 1;
    int y;

    public B()
    {
        y = -1;
    }

    public override void PrintFields() =>
        Console.WriteLine($"x = {x}, y = {y}");
}

lorsque la nouvelle B() est utilisée pour créer une instance de B, la sortie suivante est générée :

x = 1, y = 0

La valeur est x 1, car l’initialiseur de variable est exécuté avant l’appel du constructeur d’instance de classe de base. Toutefois, la valeur de y 0 (valeur par défaut d’un int) car l’affectation à y n’est exécutée qu’après le retour du constructeur de classe de base. Il est utile de considérer les initialiseurs de variables d’instance et les initialiseurs de constructeur comme des instructions qui sont automatiquement insérées avant le constructor_body. L’exemple

class A
{
    int x = 1, y = -1, count;

    public A()
    {
        count = 0;
    }

    public A(int n)
    {
        count = n;
    }
}

class B : A
{
    double sqrt2 = Math.Sqrt(2.0);
    ArrayList items = new ArrayList(100);
    int max;

    public B(): this(100)
    {
        items.Add("default");
    }

    public B(int n) : base(n - 1)
    {
        max = n;
    }
}

contient plusieurs initialiseurs de variables ; il contient également des initialiseurs de constructeur des deux formes (base et this). L’exemple correspond au code ci-dessous, où chaque commentaire indique une instruction insérée automatiquement (la syntaxe utilisée pour les appels de constructeur insérés automatiquement n’est pas valide, mais sert simplement à illustrer le mécanisme).

class A
{
    int x, y, count;
    public A()
    {
        x = 1;      // Variable initializer
        y = -1;     // Variable initializer
        object();   // Invoke object() constructor
        count = 0;
    }

    public A(int n)
    {
        x = 1;      // Variable initializer
        y = -1;     // Variable initializer
        object();   // Invoke object() constructor
        count = n;
    }
}

class B : A
{
    double sqrt2;
    ArrayList items;
    int max;
    public B() : this(100)
    {
        B(100);                      // Invoke B(int) constructor
        items.Add("default");
    }

    public B(int n) : base(n - 1)
    {
        sqrt2 = Math.Sqrt(2.0);      // Variable initializer
        items = new ArrayList(100);  // Variable initializer
        A(n - 1);                    // Invoke A(int) constructor
        max = n;
    }
}

exemple de fin

15.11.5 Constructeurs par défaut

Si une classe ne contient aucune déclaration de constructeur d’instance, un constructeur d’instance par défaut est automatiquement fourni. Ce constructeur par défaut appelle simplement un constructeur de la classe de base directe, comme s’il avait un initialiseur de constructeur du formulaire base(). Si la classe est abstraite, l’accessibilité déclarée pour le constructeur par défaut est protégée. Sinon, l’accessibilité déclarée pour le constructeur par défaut est publique.

Remarque : Par conséquent, le constructeur par défaut est toujours du formulaire

protected C(): base() {}

or

public C(): base() {}

C est le nom de la classe.

Note de fin

Si la résolution de surcharge ne peut pas déterminer un meilleur candidat unique pour l’initialiseur du constructeur de classe de base, une erreur au moment de la compilation se produit.

Exemple : dans le code suivant

class Message
{
    object sender;
    string text;
}

un constructeur par défaut est fourni, car la classe ne contient aucune déclaration de constructeur d’instance. Ainsi, l’exemple est précisément équivalent à

class Message
{
    object sender;
    string text;

    public Message() : base() {}
}

exemple de fin

15.12 Constructeurs statiques

Un constructeur statique est un membre qui implémente les actions requises pour initialiser une classe fermée. Les constructeurs statiques sont déclarés à l’aide de static_constructor_declarations :

static_constructor_declaration
    : attributes? static_constructor_modifiers identifier '(' ')'
        static_constructor_body
    ;

static_constructor_modifiers
    : 'static'
    | 'static' 'extern' unsafe_modifier?
    | 'static' unsafe_modifier 'extern'?
    | 'extern' 'static' unsafe_modifier?
    | 'extern' unsafe_modifier 'static'
    | unsafe_modifier 'static' 'extern'?
    | unsafe_modifier 'extern' 'static'
    ;

static_constructor_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier (§23.2) est disponible uniquement dans le code non sécurisé (§23).

Un static_constructor_declaration peut inclure un ensemble d’attributs (§22) et un extern modificateur (§15.6.8).

L’identificateur d’un static_constructor_declaration nomme la classe dans laquelle le constructeur statique est déclaré. Si un autre nom est spécifié, une erreur au moment de la compilation se produit.

Lorsqu’une déclaration de constructeur statique inclut un extern modificateur, le constructeur statique est considéré comme un constructeur statique externe. Étant donné qu’une déclaration de constructeur statique externe ne fournit aucune implémentation réelle, son static_constructor_body se compose d’un point-virgule. Pour toutes les autres déclarations de constructeur statiques, la static_constructor_body se compose de l’une ou l’autre des déclarations de constructeur statique

  • un bloc, qui spécifie les instructions à exécuter pour initialiser la classe ; ou
  • un corps d’expression, qui se compose d’une => expression et d’un point-virgule, et désigne une seule expression à exécuter afin d’initialiser la classe.

Un static_constructor_body qui est un corps de bloc ou d’expression correspond exactement à la method_body d’une méthode statique avec un void type de retour (§15.6.11).

Les constructeurs statiques ne sont pas hérités et ne peuvent pas être appelés directement.

Le constructeur statique d’une classe fermée s’exécute au plus une fois dans un domaine d’application donné. L’exécution d’un constructeur statique est déclenchée par le premier des événements suivants à exécuter dans un domaine d’application :

  • Une instance de la classe est créée.
  • Tous les membres statiques de la classe sont référencés.

Si une classe contient la Main méthode (§7.1) dans laquelle l’exécution commence, le constructeur statique de cette classe s’exécute avant l’appel de la Main méthode.

Pour initialiser un nouveau type de classe fermée, commencez par créer un nouvel ensemble de champs statiques (§15.5.2) pour ce type fermé particulier. Chacun des champs statiques est initialisé à sa valeur par défaut (§15.5.5). Ensuite, les initialiseurs de champs statiques (§15.5.6.2) sont exécutés pour ces champs statiques. Enfin, le constructeur statique est exécuté.

Exemple : l’exemple

class Test
{
    static void Main()
    {
        A.F();
        B.F();
    }
}

class A
{
    static A()
    {
        Console.WriteLine("Init A");
    }

    public static void F()
    {
        Console.WriteLine("A.F");
    }
}

class B
{
    static B()
    {
        Console.WriteLine("Init B");
    }

    public static void F()
    {
        Console.WriteLine("B.F");
    }
}

doit produire la sortie :

Init A
A.F
Init B
B.F

étant donné que l’exécution du Aconstructeur statique est déclenchée par l’appel à A.F, et que l’exécution du Bconstructeur statique est déclenchée par l’appel à B.F.

exemple de fin

Il est possible de construire des dépendances circulaires qui permettent aux champs statiques avec des initialiseurs de variables d’être observés dans leur état de valeur par défaut.

Exemple : l’exemple

class A
{
    public static int X;

    static A()
    {
        X = B.Y + 1;
    }
}

class B
{
    public static int Y = A.X + 1;

    static B() {}

    static void Main()
    {
        Console.WriteLine($"X = {A.X}, Y = {B.Y}");
    }
}

génère la sortie

X = 1, Y = 2

Pour exécuter la Main méthode, le système exécute d’abord l’initialiseur pour B.Y, avant le constructeur statique de la classe B. Yl’initialiseur provoque Al’exécution du constructeur du static constructeur, car la valeur de celle-ci A.X est référencée. Le constructeur statique de A son tour procède au calcul de la valeur de X, et, dans ce cas, extrait la valeur par défaut de Y, qui est zéro. A.X est donc initialisé à 1. Le processus d’initialiseurs de champs statiques en cours d’exécution Aet de constructeur statique se termine ensuite, en retournant au calcul de la valeur initiale de Y, le résultat qui devient 2.

exemple de fin

Étant donné que le constructeur statique est exécuté exactement une fois pour chaque type de classe construit fermé, il est pratique d’appliquer des vérifications au moment de l’exécution sur le paramètre de type qui ne peuvent pas être vérifiées au moment de la compilation via des contraintes (§15.2.5).

Exemple : Le type suivant utilise un constructeur statique pour appliquer que l’argument de type est une énumération :

class Gen<T> where T : struct
{
    static Gen()
    {
        if (!typeof(T).IsEnum)
        {
            throw new ArgumentException("T must be an enum");
        }
    }
}

exemple de fin

15.13 Finaliseurs

Remarque : Dans une version antérieure de cette spécification, ce qui est maintenant appelé « finaliseur » a été appelé « destructeur ». L’expérience a montré que le terme « destructeur » a provoqué la confusion et a souvent entraîné des attentes incorrectes, en particulier pour les programmeurs connaissant C++. En C++, un destructeur est appelé de manière déterminatrice, alors que, en C#, un finaliseur n’est pas. Pour déterminer le comportement à partir de C#, il convient d’utiliser Dispose. Note de fin

Un finaliseur est un membre qui implémente les actions requises pour finaliser une instance d’une classe. Un finaliseur est déclaré à l’aide d’un finalizer_declaration :

finalizer_declaration
    : attributes? '~' identifier '(' ')' finalizer_body
    | attributes? 'extern' unsafe_modifier? '~' identifier '(' ')'
      finalizer_body
    | attributes? unsafe_modifier 'extern'? '~' identifier '(' ')'
      finalizer_body
    ;

finalizer_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier (§23.2) est disponible uniquement dans le code non sécurisé (§23).

Un finalizer_declaration peut inclure un ensemble d’attributs (§22).

L’identificateur d’un finalizer_declarator nomme la classe dans laquelle le finaliseur est déclaré. Si un autre nom est spécifié, une erreur au moment de la compilation se produit.

Lorsqu’une déclaration de finaliseur inclut un extern modificateur, le finaliseur est dit être un finaliseur externe. Étant donné qu’une déclaration de finaliseur externe ne fournit aucune implémentation réelle, sa finalizer_body se compose d’un point-virgule. Pour tous les autres finaliseurs, le finalizer_body se compose de l’une ou l’autre

  • un bloc, qui spécifie les instructions à exécuter afin de finaliser une instance de la classe.
  • ou un corps d’expression, qui se compose d’une => expression et d’un point-virgule, et désigne une seule expression à exécuter afin de finaliser une instance de la classe.

Un finalizer_body qui est un corps de bloc ou d’expression correspond exactement à la method_body d’une méthode d’instance avec un void type de retour (§15.6.11).

Les finaliseurs ne sont pas hérités. Par conséquent, une classe n’a aucun finaliseur autre que celui qui peut être déclaré dans cette classe.

Remarque : Étant donné qu’un finaliseur n’a pas de paramètres, il ne peut pas être surchargé, afin qu’une classe puisse avoir, au plus, un finaliseur. Note de fin

Les finaliseurs sont appelés automatiquement et ne peuvent pas être appelés explicitement. Une instance devient éligible à la finalisation lorsqu’il n’est plus possible pour un code d’utiliser cette instance. L’exécution du finaliseur pour l’instance peut se produire à tout moment après la finalisation de l’instance (§7.9). Lorsqu’une instance est finalisée, les finaliseurs de la chaîne d’héritage de cette instance sont appelés, dans l’ordre, de la plupart des dérivés au moins dérivé. Un finaliseur peut être exécuté sur n’importe quel thread. Pour plus d’informations sur les règles qui régissent quand et comment un finaliseur est exécuté, consultez le §7.9.

Exemple : sortie de l’exemple

class A
{
    ~A()
    {
        Console.WriteLine("A's finalizer");
    }
}

class B : A
{
    ~B()
    {
        Console.WriteLine("B's finalizer");
    }
}

class Test
{
    static void Main()
    {
        B b = new B();
        b = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

is

B's finalizer
A's finalizer

étant donné que les finaliseurs d’une chaîne d’héritage sont appelés dans l’ordre, de la plupart dérivés au moins dérivé.

exemple de fin

Les finaliseurs sont implémentés en remplaçant la méthode Finalize virtuelle sur System.Object. Les programmes C# ne sont pas autorisés à remplacer cette méthode ou à l’appeler (ou les remplacements) directement.

Exemple : Par exemple, le programme

class A
{
    override protected void Finalize() {}  // Error
    public void F()
    {
        this.Finalize();                   // Error
    }
}

contient deux erreurs.

exemple de fin

Un compilateur doit se comporter comme si cette méthode, et les remplacements de celui-ci, n’existent pas du tout.

Exemple : Ainsi, ce programme :

class A
{
    void Finalize() {}  // Permitted
}

est valide et la méthode affichée masque System.Objectla méthode.Finalize

exemple de fin

Pour une discussion sur le comportement lorsqu’une exception est levée à partir d’un finaliseur, consultez le §21.4.

15.14 Itérateurs

15.14.1 Général

Un membre de fonction (§12.6) implémenté à l’aide d’un bloc itérateur (§13.3) est appelé itérateur.

Un bloc d’itérateur peut être utilisé comme corps d’un membre de fonction tant que le type de retour du membre de fonction correspondant est l’une des interfaces d’énumérateur (§15.14.2) ou l’une des interfaces énumérables (§15.14.3). Il peut se produire en tant que method_body, operator_body ou accessor_body, tandis que les événements, les constructeurs d’instances, les constructeurs statiques et le finaliseur ne doivent pas être implémentés en tant qu’itérateurs.

Lorsqu’un membre de fonction est implémenté à l’aide d’un bloc d’itérateur, il s’agit d’une erreur au moment de la compilation pour la liste des paramètres du membre de fonction afin de spécifier inles paramètres, outou ref les paramètres d’un ref struct type.

Interfaces d’énumérateur 15.14.2

Les interfaces d’énumérateur sont l’interface System.Collections.IEnumerator non générique et toutes les instanciations de l’interface System.Collections.Generic.IEnumerator<T>générique . Par souci de concision, dans ce sous-volet et ses frères, ces interfaces sont référencées respectivement comme IEnumerator et IEnumerator<T>, respectivement.

Interfaces énumérables 15.14.3

Les interfaces énumérables sont l’interface System.Collections.IEnumerable non générique et toutes les instanciations de l’interface System.Collections.Generic.IEnumerable<T>générique. Par souci de concision, dans ce sous-volet et ses frères, ces interfaces sont référencées respectivement comme IEnumerable et IEnumerable<T>, respectivement.

15.14.4 Type de rendement

Un itérateur produit une séquence de valeurs, tout le même type. Ce type est appelé type de rendement de l’itérateur.

  • Type de rendement d’un itérateur qui retourne IEnumerator ou IEnumerable est object.
  • Type de rendement d’un itérateur qui retourne IEnumerator<T> ou IEnumerable<T> est T.

15.14.5 Objets énumérateurs

15.14.5.1 Général

Lorsqu’un membre de fonction retournant un type d’interface d’énumérateur est implémenté à l’aide d’un bloc d’itérateur, l’appel du membre de fonction n’exécute pas immédiatement le code dans le bloc itérateur. Au lieu de cela, un objet énumérateur est créé et retourné. Cet objet encapsule le code spécifié dans le bloc d’itérateur et l’exécution du code dans le bloc d’itérateur se produit lorsque la méthode de MoveNext l’objet énumérateur est appelée. Un objet énumérateur présente les caractéristiques suivantes :

  • Il implémente IEnumerator et IEnumerator<T>, où T est le type de rendement de l’itérateur.
  • Elle implémente System.IDisposable.
  • Il est initialisé avec une copie des valeurs d’argument (le cas échéant) et de la valeur d’instance passée au membre de la fonction.
  • Il a quatre états potentiels, avant, en cours d’exécution, suspendus et après, et est initialement dans l’état précédent .

Un objet énumérateur est généralement une instance d’une classe d’énumérateur générée par le compilateur qui encapsule le code dans le bloc d’itérateur et implémente les interfaces d’énumérateur, mais d’autres méthodes d’implémentation sont possibles. Si une classe d’énumérateur est générée par le compilateur, cette classe sera imbriquée, directement ou indirectement, dans la classe contenant le membre de la fonction, elle aura une accessibilité privée et aura un nom réservé pour l’utilisation du compilateur (§6.4.3).

Un objet énumérateur peut implémenter plus d’interfaces que celles spécifiées ci-dessus.

Les sous-sections suivantes décrivent le comportement requis des MoveNextimplémentations Current, ainsi Dispose que les membres des IEnumerator implémentations et IEnumerator<T> des implémentations d’interface fournies par un objet énumérateur.

Les objets énumérateurs ne prennent pas en charge la IEnumerator.Reset méthode. L’appel de cette méthode entraîne une System.NotSupportedException levée.

15.14.5.2 La méthode MoveNext

La MoveNext méthode d’un objet énumérateur encapsule le code d’un bloc d’itérateur. L’appel de la méthode exécute du MoveNext code dans le bloc itérateur et définit la Current propriété de l’objet énumérateur comme il convient. L’action précise effectuée par MoveNext dépend de l’état de l’objet énumérateur lorsqu’il MoveNext est appelé :

  • Si l’état de l’objet énumérateur est antérieur, appelez MoveNext:
    • Modifie l’état en cours d’exécution.
    • Initialise les paramètres (y compris this) du bloc itérateur sur les valeurs d’argument et la valeur d’instance enregistrées lorsque l’objet énumérateur a été initialisé.
    • Exécute le bloc d’itérateur à partir du début jusqu’à ce que l’exécution soit interrompue (comme décrit ci-dessous).
  • Si l’état de l’objet énumérateur est en cours d’exécution, le résultat de l’appel MoveNext n’est pas spécifié.
  • Si l’état de l’objet énumérateur est suspendu, appelez MoveNext :
    • Modifie l’état en cours d’exécution.
    • Restaure les valeurs de toutes les variables et paramètres locaux (y compris this) dans les valeurs enregistrées lors de l’exécution du bloc d’itérateur a été suspendue pour la dernière fois.

      Remarque : Le contenu des objets référencés par ces variables peut avoir changé depuis l’appel précédent à MoveNext. Note de fin

    • Reprend l’exécution du bloc itérateur immédiatement après l’instruction de retour de rendement qui a provoqué la suspension de l’exécution et se poursuit jusqu’à ce que l’exécution soit interrompue (comme décrit ci-dessous).
  • Si l’état de l’objet énumérateur est après, l’appel MoveNext retourne false.

Lorsque MoveNext vous exécutez le bloc d’itérateur, l’exécution peut être interrompue de quatre façons : par une yield return instruction, par une yield break instruction, en rencontrant la fin du bloc d’itérateur, et par une exception levée et propagée hors du bloc itérateur.

  • Lorsqu’une yield return instruction est rencontrée (§9.4.4.20) :
    • L’expression donnée dans l’instruction est évaluée, convertie implicitement en type de rendement et affectée à la Current propriété de l’objet énumérateur.
    • L’exécution du corps de l’itérateur est suspendue. Les valeurs de toutes les variables et paramètres locaux (y compris this) sont enregistrées, car il s’agit de l’emplacement de cette yield return instruction. Si l’instruction yield return se trouve dans un ou plusieurs try blocs, les blocs finals associés ne sont pas exécutés pour l’instant.
    • L’état de l’objet énumérateur est modifié pour être suspendu.
    • La MoveNext méthode retourne true à son appelant, indiquant que l’itération a réussi à atteindre la valeur suivante.
  • Lorsqu’une yield break instruction est rencontrée (§9.4.4.20) :
    • Si l’instruction yield break se trouve dans un ou plusieurs try blocs, les blocs associés finally sont exécutés.
    • L’état de l’objet énumérateur est remplacé par après.
    • La MoveNext méthode retourne false à son appelant, indiquant que l’itération est terminée.
  • Lorsque la fin du corps de l’itérateur est rencontrée :
    • L’état de l’objet énumérateur est remplacé par après.
    • La MoveNext méthode retourne false à son appelant, indiquant que l’itération est terminée.
  • Lorsqu’une exception est levée et propagée hors du bloc d’itérateur :
    • Les blocs appropriés finally dans le corps de l’itérateur ont été exécutés par la propagation d’exception.
    • L’état de l’objet énumérateur est remplacé par après.
    • La propagation de l’exception continue à l’appelant de la MoveNext méthode.

15.14.5.3 La propriété actuelle

La propriété d’un Current objet énumérateur est affectée par yield return les instructions du bloc d’itérateur.

Lorsqu’un objet énumérateur est dans l’état suspendu , la valeur est Current la valeur définie par l’appel précédent à MoveNext. Lorsqu’un objet énumérateur se trouve dans les états antérieurs, en cours d’exécution ou après , le résultat de l’accès Current n’est pas spécifié.

Pour un itérateur avec un type de rendement autre que object, le résultat de l’accès Current via l’implémentation de IEnumerable l’objet énumérateur correspond à l’accès Current via l’implémentation de IEnumerator<T> l’objet énumérateur et à la conversion du résultat sur object.

15.14.5.4 Méthode Dispose

La Dispose méthode est utilisée pour nettoyer l’itération en apportant l’objet énumérateur à l’état après .

  • Si l’état de l’objet énumérateur est antérieur, l’appel Dispose modifie l’état après.
  • Si l’état de l’objet énumérateur est en cours d’exécution, le résultat de l’appel Dispose n’est pas spécifié.
  • Si l’état de l’objet énumérateur est suspendu, appelez Dispose:
    • Modifie l’état en cours d’exécution.
    • Exécute tous les blocs finals comme si la dernière instruction exécutée yield return était une yield break instruction. Si cela entraîne la levée et la propagation d’une exception hors du corps de l’itérateur, l’état de l’objet énumérateur est défini sur après et l’exception est propagée à l’appelant de la Dispose méthode.
    • Modifie l’état après.
  • Si l’état de l’objet énumérateur est après, l’appel Dispose n’a aucun impact.

15.14.6 Objets énumérables

15.14.6.1 Général

Lorsqu’un membre de fonction retournant un type d’interface énumérable est implémenté à l’aide d’un bloc d’itérateur, l’appel du membre de fonction n’exécute pas immédiatement le code dans le bloc itérateur. Au lieu de cela, un objet énumérable est créé et retourné. La méthode de l’objet GetEnumerator énumérable retourne un objet énumérateur qui encapsule le code spécifié dans le bloc d’itérateur et l’exécution du code dans le bloc itérateur se produit lorsque la méthode de MoveNext l’objet énumérateur est appelée. Un objet énumérable présente les caractéristiques suivantes :

  • Il implémente IEnumerable et IEnumerable<T>, où T est le type de rendement de l’itérateur.
  • Il est initialisé avec une copie des valeurs d’argument (le cas échéant) et de la valeur d’instance passée au membre de la fonction.

Un objet énumérable est généralement une instance d’une classe énumérable générée par le compilateur qui encapsule le code dans le bloc itérateur et implémente les interfaces énumérables, mais d’autres méthodes d’implémentation sont possibles. Si une classe énumérable est générée par le compilateur, cette classe sera imbriquée, directement ou indirectement, dans la classe contenant le membre de la fonction, elle aura une accessibilité privée et aura un nom réservé pour l’utilisation du compilateur (§6.4.3).

Un objet énumérable peut implémenter plus d’interfaces que celles spécifiées ci-dessus.

Remarque : Par exemple, un objet énumérable peut également implémenter IEnumerator et IEnumerator<T>, en lui permettant de servir à la fois d’énumérateur et d’énumérateur. En règle générale, une telle implémentation retourne sa propre instance (pour enregistrer les allocations) du premier appel vers GetEnumerator. Les appels suivants de GetEnumerator, le cas échéant, retournent une nouvelle instance de classe, généralement de la même classe, afin que les appels à différentes instances d’énumérateur n’affectent pas les uns les autres. Elle ne peut pas retourner la même instance même si l’énumérateur précédent a déjà énuméré au-delà de la fin de la séquence, car tous les appels futurs à un énumérateur épuisé doivent lever des exceptions. Note de fin

15.14.6.2 La méthode GetEnumerator

Un objet énumérable fournit une implémentation des GetEnumerator méthodes des interfaces et IEnumerable des IEnumerable<T> méthodes. Les deux GetEnumerator méthodes partagent une implémentation commune qui acquiert et retourne un objet d’énumérateur disponible. L’objet énumérateur est initialisé avec les valeurs d’argument et la valeur d’instance enregistrées lorsque l’objet énumérable a été initialisé, mais sinon, les fonctions d’objet d’énumérateur, comme décrit dans §15.14.5.

Fonctions asynchrones 15.15

15.15.1 Général

Une méthode (§15.6) ou une fonction anonyme (§12.19) avec le async modificateur est appelée fonction asynchrone. En règle générale, le terme async est utilisé pour décrire tout type de fonction qui a le async modificateur.

Il s’agit d’une erreur au moment de la compilation pour la liste des paramètres d’une fonction asynchrone pour spécifier n’importe quel inparamètre, outou ref paramètre d’un ref struct type.

La return_type d’une méthode asynchrone doit être soit void un type de tâche. Pour une méthode asynchrone qui produit une valeur de résultat, un type de tâche doit être générique. Pour une méthode asynchrone qui ne produit pas de valeur de résultat, un type de tâche ne doit pas être générique. Ces types sont appelés dans cette spécification, respectivement «TaskType»<T>«TaskType». Le type System.Threading.Tasks.Task de bibliothèque standard et les types construits à partir de System.Threading.Tasks.Task<TResult> sont des types de tâches, ainsi qu’une classe, un struct ou un type d’interface associé à un type de générateur de tâches via l’attribut System.Runtime.CompilerServices.AsyncMethodBuilderAttribute. Ces types sont appelés dans cette spécification comme «TaskBuilderType»<T> et «TaskBuilderType». Un type de tâche peut avoir au plus un paramètre de type et ne peut pas être imbriqué dans un type générique.

Une méthode asynchrone retournant un type de tâche est considérée comme un retour de tâche.

Les types de tâches peuvent varier dans leur définition exacte, mais du point de vue du langage, un type de tâche se trouve dans l’un des états incomplets, réussis ou défectueux. Une tâche défaillante enregistre une exception pertinente. Un enregistrement réussi«TaskType»<T> enregistre un résultat de type T. Les types de tâches sont attendus et les tâches peuvent donc être les opérandes des expressions await (§12.9.8).

Exemple : le type MyTask<T> de tâche est associé au type MyTaskMethodBuilder<T> du générateur de tâches et au type Awaiter<T>awaiter :

using System.Runtime.CompilerServices; 
[AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))]
class MyTask<T>
{
    public Awaiter<T> GetAwaiter() { ... }
}

class Awaiter<T> : INotifyCompletion
{
    public void OnCompleted(Action completion) { ... }
    public bool IsCompleted { get; }
    public T GetResult() { ... }
}

exemple de fin

Un type de générateur de tâches est un type de classe ou de struct qui correspond à un type de tâche spécifique (§15.15.2). Le type du générateur de tâches doit correspondre exactement à l’accessibilité déclarée de son type de tâche correspondant.

Remarque : Si le type de tâche est déclaré internal, le type de générateur correspondant doit également être déclaré internal et défini dans le même assembly. Si le type de tâche est imbriqué à l’intérieur d’un autre type, le type de buider de tâche doit également être imbriqué dans ce même type. Note de fin

Une fonction asynchrone a la possibilité de suspendre l’évaluation à l’aide d’expressions await (§12.9.8) dans son corps. L’évaluation peut être reprise ultérieurement au moment de la suspension de l’expression await par le biais d’un délégué de reprise. Le délégué de reprise est de type System.Actionet, lorsqu’il est appelé, l’évaluation de l’appel de fonction asynchrone reprend à partir de l’expression await où elle s’est arrêtée. L’appelant actuel d’un appel de fonction asynchrone est l’appelant d’origine si l’appel de fonction n’a jamais été suspendu ou que l’appelant le plus récent du délégué de reprise n’a jamais été interrompu.

15.15.2 Modèle de générateur de types de tâches

Un type de générateur de tâches peut avoir au plus un paramètre de type et ne peut pas être imbriqué dans un type générique. Un type de générateur de tâches doit avoir les membres suivants (pour les types de générateur de tâches non génériques, SetResult n’a aucun paramètre) avec l’accessibilité déclarée public :

class «TaskBuilderType»<T>
{
    public static «TaskBuilderType»<T> Create();
    public void Start<TStateMachine>(ref TStateMachine stateMachine)
                where TStateMachine : IAsyncStateMachine;
    public void SetStateMachine(IAsyncStateMachine stateMachine);
    public void SetException(Exception exception);
    public void SetResult(T result);
    public void AwaitOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine;
    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine;
    public «TaskType»<T> Task { get; }
}

Un compilateur génère du code qui utilise le « TaskBuilderType » pour implémenter la sémantique de suspension et de reprise de l’évaluation de la fonction asynchrone. Un compilateur doit utiliser le « TaskBuilderType » comme suit :

  • «TaskBuilderType».Create() est appelé pour créer une instance du « TaskBuilderType », nommée builder dans cette liste.
  • builder.Start(ref stateMachine)est appelé pour associer le générateur à une instance de machine d’état générée par le compilateur. stateMachine
    • Le générateur doit appeler stateMachine.MoveNext() soit dans Start() ou après Start() soit retourné pour avancer la machine d’état.
  • Une fois Start() retournée, la async méthode appelle builder.Task la tâche à retourner à partir de la méthode asynchrone.
  • Chaque appel pour stateMachine.MoveNext() faire avancer l’ordinateur d’état.
  • Si l’ordinateur d’état se termine correctement, builder.SetResult() est appelé, avec la valeur de retour de méthode, le cas échéant.
  • Sinon, si une exception est e levée dans l’ordinateur d’état, builder.SetException(e) elle est appelée.
  • Si l’ordinateur d’état atteint une await expr expression, expr.GetAwaiter() est appelé.
  • Si l’awaiter implémente ICriticalNotifyCompletion et IsCompleted a la valeur false, l’ordinateur d’état appelle builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine).
    • AwaitUnsafeOnCompleted() doit appeler awaiter.UnsafeOnCompleted(action) avec un Action appel qui stateMachine.MoveNext() se termine lorsque l’awaiter est terminé.
  • Sinon, l’ordinateur d’état appelle builder.AwaitOnCompleted(ref awaiter, ref stateMachine).
    • AwaitOnCompleted() doit appeler awaiter.OnCompleted(action) avec un Action appel qui stateMachine.MoveNext() se termine lorsque l’awaiter est terminé.
  • SetStateMachine(IAsyncStateMachine) peut être appelé par l’implémentation générée IAsyncStateMachine par le compilateur pour identifier l’instance du générateur associée à une instance d’ordinateur d’état, en particulier dans les cas où l’ordinateur d’état est implémenté en tant que type valeur.
    • Si le générateur appelle stateMachine.SetStateMachine(stateMachine), l’appel stateMachinebuilder.SetStateMachine(stateMachine) sur l’instance du générateur associéestateMachineà .

Remarque : Pour les deux SetResult(T result) et «TaskType»<T> Task { get; }, respectivement, le paramètre et l’argument doivent être convertibles Ten identité. Cela permet à un générateur de types de tâches de prendre en charge des types tels que des tuples, où deux types qui ne sont pas les mêmes sont convertibles d’identité. Note de fin

15.15.3 Évaluation d’une fonction asynchrone de retour de tâches

L’appel d’une fonction asynchrone qui retourne une tâche entraîne la génération d’une instance du type de tâche retourné. Il s’agit de la tâche de retour de la fonction asynchrone. La tâche est initialement dans un état incomplet .

Le corps de la fonction asynchrone est ensuite évalué jusqu’à ce qu’il soit suspendu (en atteignant une expression await) ou se termine, à quel point le contrôle est retourné à l’appelant, ainsi que la tâche de retour.

Lorsque le corps de la fonction asynchrone se termine, la tâche de retour est déplacée hors de l’état incomplet :

  • Si le corps de la fonction se termine par l’atteinte d’une instruction return ou de la fin du corps, toute valeur de résultat est enregistrée dans la tâche de retour, qui est placée dans un état réussi .
  • Si le corps de la fonction se termine en raison d’une erreur OperationCanceledException, l’exception est enregistrée dans la tâche de retour qui est placée dans l’état annulé .
  • Si le corps de la fonction se termine à la suite d’une autre exception non interceptée (§13.10.6), l’exception est enregistrée dans la tâche de retour qui est placée dans un état d’erreur .

15.15.4 Évaluation d’une fonction asynchrone de retour vide

Si le type de retour de la fonction asynchrone est void, l’évaluation diffère de ce qui précède de la façon suivante : étant donné qu’aucune tâche n’est retournée, la fonction communique plutôt l’achèvement et les exceptions au contexte de synchronisation du thread actuel. La définition exacte du contexte de synchronisation dépend de l’implémentation, mais est une représentation de « où » le thread actuel est en cours d’exécution. Le contexte de synchronisation est averti lorsque l’évaluation d’une voidfonction asynchrone de retour commence, se termine correctement ou provoque la levée d’une exception non interceptée.

Cela permet au contexte de suivre le nombre voidde fonctions asynchrones retournées en cours d’exécution, et de décider comment propager des exceptions sortantes.