20 délégués
20.1 Général
Une déclaration de délégué définit une classe dérivée de la classe System.Delegate
. Une instance de délégué encapsule une liste d’appel, qui est une liste d’une ou plusieurs méthodes, chacune appelée entité pouvant être appelée. Par exemple, une entité pouvant être appelée se compose d’une instance et d’une méthode sur cette instance. Pour les méthodes statiques, une entité pouvant être appelée se compose simplement d’une méthode. L’appel d’une instance de délégué avec un jeu d’arguments approprié entraîne l’appel de chacune des entités pouvant être appelées par le délégué avec le jeu d’arguments donné.
Remarque : Une propriété intéressante et utile d’une instance de délégué est qu’elle ne connaît pas ou ne s’intéresse pas aux classes des méthodes qu’il encapsule ; tout ce qui importe est que ces méthodes soient compatibles (§20.4) avec le type du délégué. Cela rend les délégués parfaitement adaptés à l’appel « anonyme ». Note de fin
20.2 Déclarations de délégué
Un delegate_declaration est un type_declaration (§14.7) qui déclare un nouveau type de délégué.
delegate_declaration
: attributes? delegate_modifier* 'delegate' return_type delegate_header
| attributes? delegate_modifier* 'delegate' ref_kind ref_return_type
delegate_header
;
delegate_header
: identifier '(' parameter_list? ')' ';'
| identifier variant_type_parameter_list '(' parameter_list? ')'
type_parameter_constraints_clause* ';'
;
delegate_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| unsafe_modifier // unsafe code support
;
unsafe_modifier est défini dans le §23.2.
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 délégué.
Une déclaration de délégué qui fournit un variant_type_parameter_list est une déclaration de délégué générique. En outre, tout délégué imbriqué à l’intérieur d’une déclaration de classe générique ou d’une déclaration de struct générique est lui-même une déclaration de délégué générique, car les arguments de type pour le type conteneur doivent être fournis pour créer un type construit (§8.4).
Le new
modificateur est autorisé uniquement sur les délégués déclarés dans un autre type, auquel cas il spécifie qu’un tel délégué masque un membre hérité du même nom, comme décrit dans le §15.3.5.
Les public
modificateurs , et internal
private
les modificateurs protected
contrôlent l’accessibilité du type délégué. Selon le contexte dans lequel la déclaration de délégué se produit, certains de ces modificateurs peuvent ne pas être autorisés (§7.5.2).
Le nom de type du délégué est l’identificateur.
Comme pour les méthodes (§15.6.1), s’il ref
est présent, le délégué retourne par référence ; sinon, si return_type est void
, le délégué retourne sans valeur ; sinon, le délégué retourne par valeur.
Le parameter_list facultatif spécifie les paramètres du délégué.
La return_type d’une déclaration de délégué de retour par valeur ou de retour sans valeur spécifie le type du résultat, le cas échéant, retourné par le délégué.
La ref_return_type d’une déclaration de délégué de retour par réf spécifie le type de la variable référencée par le variable_reference (§9.5) retourné par le délégué.
Le variant_type_parameter_list facultatif (§18.2.3) spécifie les paramètres de type pour le délégué lui-même.
Le type de retour d’un type délégué doit être void
soit , soit sortie-safe (§18.2.3.2).
Tous les types de paramètres d’un type délégué doivent être input-safe (§18.2.3.2). En outre, tous les types de paramètres de sortie ou de référence doivent également être sécurisés.
Remarque : Les paramètres de sortie doivent être sécurisés en entrée en raison de restrictions d’implémentation courantes. Note de fin
En outre, chaque contrainte de type de classe, contrainte de type d’interface et contrainte de paramètre de type sur tous les paramètres de type du délégué doit être entrée-safe.
Les types délégués en C# sont équivalents de nom, et non structurellement équivalents.
Exemple :
delegate int D1(int i, double d); delegate int D2(int c, double d);
Les types
D1
délégués etD2
deux types différents, de sorte qu’ils ne sont pas interchangeables, malgré leurs signatures identiques.exemple de fin
Comme d’autres déclarations de type générique, les arguments de type doivent être donnés pour créer un type délégué construit. Les types de paramètres et le type de retour d’un type délégué construit sont créés en remplaçant, pour chaque paramètre de type dans la déclaration de délégué, l’argument de type correspondant du type délégué construit.
La seule façon de déclarer un type délégué est via un delegate_declaration. Chaque type délégué est un type de référence dérivé de System.Delegate
. Les membres requis pour chaque type de délégué sont détaillés dans le §20.3. Les types délégués sont implicitement sealed
, il n’est donc pas autorisé à dériver n’importe quel type d’un type délégué. Il n’est pas non plus permis de déclarer un type de classe non délégué dérivant de System.Delegate
. System.Delegate
n’est pas lui-même un type délégué ; il s’agit d’un type de classe à partir duquel tous les types délégués sont dérivés.
20.3 Membres délégués
Chaque type de délégué hérite des membres de la Delegate
classe, comme décrit dans le §15.3.4. En outre, chaque type délégué doit fournir une méthode non générique Invoke
dont la liste de paramètres correspond à la parameter_list dans la déclaration de délégué, dont le type de retour correspond à la return_type ou ref_return_type dans la déclaration de délégué, et pour les délégués de retour par réf dont ref_kind correspond à celui dans la déclaration de délégué. La Invoke
méthode doit être au moins aussi accessible que le type délégué contenant. L’appel de la Invoke
méthode sur un type délégué équivaut sémantiquement à l’utilisation de la syntaxe d’appel de délégué (§20.6).
Les implémentations peuvent définir des membres supplémentaires dans le type de délégué.
À l’exception de l’instanciation, toute opération qui peut être appliquée à une classe ou une instance de classe peut également être appliquée à une classe ou une instance déléguée, respectivement. En particulier, il est possible d’accéder aux membres du System.Delegate
type via la syntaxe d’accès de membre habituelle.
Compatibilité des délégués 20.4
Une méthode ou un type M
délégué est compatible avec un type D
délégué si toutes les valeurs suivantes sont remplies :
D
etM
ont le même nombre de paramètres, et chaque paramètre dansD
a le même modificateur de paramètre par référence que le paramètre correspondant dansM
.- Pour chaque paramètre de valeur, une conversion d’identité (§10.2.2) ou une conversion de référence implicite (§10.2.8) existe du type de paramètre dans
D
le type de paramètre dans le type de paramètre correspondant dansM
. - Pour chaque paramètre de référence, le type de paramètre dans
D
est identique au type de paramètre dansM
. - L’une des valeurs suivantes est vraie :
D
etM
sont à la fois retour-no-valueD
etM
sont des retours par valeur (§15.6.1, §20.2) et une conversion d’identité ou de référence implicite existe du type de retour duM
type de retour deD
.D
etM
sont les deux retours par réf, une conversion d’identité existe entre le type de retour etM
le type de retour deD
, et les deux ont les mêmes ref_kind.
Cette définition de compatibilité permet la covariance dans le type de retour et la contravariance dans les types de paramètres.
Exemple :
delegate int D1(int i, double d); delegate int D2(int c, double d); delegate object D3(string s); class A { public static int M1(int a, double b) {...} } class B { public static int M1(int f, double g) {...} public static void M2(int k, double l) {...} public static int M3(int g) {...} public static void M4(int g) {...} public static object M5(string s) {...} public static int[] M6(object o) {...} }
Les méthodes
A.M1
etB.M1
sont compatibles avec les typesD1
délégués etD2
, car elles ont le même type de retour et la même liste de paramètres. Les méthodesB.M2
,B.M3
etB.M4
sont incompatibles avec les typesD1
délégués etD2
, car elles ont différents types de retour ou listes de paramètres. Les méthodesB.M5
etB.M6
sont toutes deux compatibles avec le typeD3
délégué .exemple de fin
Exemple :
delegate bool Predicate<T>(T value); class X { static bool F(int i) {...} static bool G(string s) {...} }
La méthode
X.F
est compatible avec le typePredicate<int>
délégué et la méthodeX.G
est compatible avec le typePredicate<string>
délégué.exemple de fin
Remarque : La signification intuitive de la compatibilité des délégués est qu’une méthode est compatible avec un type délégué si chaque appel du délégué peut être remplacé par un appel de la méthode sans violer la sécurité du type, en traitant les paramètres facultatifs et les tableaux de paramètres comme des paramètres explicites. Par exemple, dans le code suivant :
delegate void Action<T>(T arg); class Test { static void Print(object value) => Console.WriteLine(value); static void Main() { Action<string> log = Print; log("text"); } }
La
Action<string>
type de délégué, car tout appel d’unAction<string>
délégué serait également un appel valide de laSi la signature de la
Print(object value, bool prependTimestamp = false)
par exemple, laAction<string>
les règles de cette clause.Note de fin
20.5 Instanciation du délégué
Une instance d’un délégué est créée par un delegate_creation_expression (§12.8.16.6), une conversion en type délégué, combinaison de délégués ou suppression de délégué. L’instance de délégué nouvellement créée fait ensuite référence à un ou plusieurs des éléments suivants :
- Méthode statique référencée dans le delegate_creation_expression ou
- Objet cible (qui ne peut pas être
null
) et méthode d’instance référencé dans le delegate_creation_expression, ou - Autre délégué (§12.8.16.6).
Exemple :
delegate void D(int x); class C { public static void M1(int i) {...} public void M2(int i) {...} } class Test { static void Main() { D cd1 = new D(C.M1); // Static method C t = new C(); D cd2 = new D(t.M2); // Instance method D cd3 = new D(cd2); // Another delegate } }
exemple de fin
L’ensemble de méthodes encapsulées par une instance de délégué est appelé liste d’appel. Lorsqu’une instance de délégué est créée à partir d’une seule méthode, elle encapsule cette méthode et sa liste d’appel ne contient qu’une seule entrée. Toutefois, lorsque deux instances nonnull
déléguées sont combinées, leurs listes d’appel sont concaténées( dans l’ordre d’opérande gauche, puis opérande droit) pour former une nouvelle liste d’appel, qui contient deux entrées ou plus.
Lorsqu’un délégué est créé à partir d’un seul délégué, la liste d’appel résultante n’a qu’une seule entrée, qui est le délégué source (§12.8.16.6).
Les délégués sont combinés à l’aide du binaire +
(§12.10.5) et +=
des opérateurs (§12.21.4). Un délégué peut être supprimé d’une combinaison de délégués, à l’aide du binaire -
(§12.10.6) et -=
des opérateurs (§12.21.4). Les délégués peuvent être comparés pour l’égalité (§12.12.9).
Exemple : L’exemple suivant montre l’instanciation d’un certain nombre de délégués et leurs listes d’appel correspondantes :
delegate void D(int x); class C { public static void M1(int i) {...} public static void M2(int i) {...} } class Test { static void Main() { D cd1 = new D(C.M1); // M1 - one entry in invocation list D cd2 = new D(C.M2); // M2 - one entry D cd3 = cd1 + cd2; // M1 + M2 - two entries D cd4 = cd3 + cd1; // M1 + M2 + M1 - three entries D cd5 = cd4 + cd3; // M1 + M2 + M1 + M1 + M2 - five entries D td3 = new D(cd3); // [M1 + M2] - ONE entry in invocation // list, which is itself a list of two methods. D td4 = td3 + cd1; // [M1 + M2] + M1 - two entries D cd6 = cd4 - cd2; // M1 + M1 - two entries in invocation list D td6 = td4 - cd2; // [M1 + M2] + M1 - two entries in invocation list, // but still three methods called, M2 not removed. } }
Quand
cd1
etcd2
sont instanciés, ils encapsulent chacune une méthode. Lorsqu’ilcd3
est instancié, il a une liste d’appel de deux méthodes etM1
M2
, dans cet ordre.cd4
la liste d’appels contientM1
,M2
etM1
, dans cet ordre. Pourcd5
, la liste d’appel contientM1
,M2
,M1
,M1
etM2
, dans cet ordre.Lors de la création d’un délégué à partir d’un autre délégué avec un delegate_creation_expression le résultat a une liste d’appel avec une structure différente de l’original, mais qui entraîne l’appel des mêmes méthodes dans le même ordre. Lorsqu’elle
td3
est créée à partir decd3
sa liste d’appel n’a qu’un seul membre, mais ce membre est une liste des méthodesM1
etM2
ces méthodes sont appeléestd3
dans le même ordre qu’elles sont appelées parcd3
. De même, lorsqu’elletd4
est instanciée, sa liste d’appel n’a que deux entrées, mais elle appelle les trois méthodesM1
,M2
etM1
, dans cet ordre tout commecd4
cela.La structure de la liste d’appel affecte la soustraction des délégués. Délégué
cd6
, créé en soustrayantcd2
(qui appelle ) decd4
(qui appelleM2
,M2
etM1
) appelleM1
M1
etM1
. Toutefois, déléguétd6
, créé en soustrayantcd2
(qui appelle ) detd4
(qui appelleM1
M2
,M2
etM1
) appelleM1
toujours ,M2
etM1
, dans cet ordre, commeM2
n’est pas une seule entrée dans la liste mais un membre d’une liste imbriquée. Pour obtenir d’autres exemples de combinaison (ainsi que de suppression) de délégués, consultez le §20.6.exemple de fin
Une fois instanciée, une instance de délégué fait toujours référence à la même liste d’appels.
Remarque : N’oubliez pas que lorsque deux délégués sont combinés, ou qu’un délégué est supprimé d’un autre, un nouveau résultat de délégué avec sa propre liste d’appel ; les listes d’appel des délégués combinés ou supprimés restent inchangés. Note de fin
20.6 Appel délégué
C# fournit une syntaxe spéciale pour appeler un délégué. Lorsqu’une instance nonnull
déléguée dont la liste d’appel contient une entrée est appelée, elle appelle la méthode unique avec les mêmes arguments qu’elle a été donnée et retourne la même valeur que la méthode appelée. (Consultez le §12.8.9.4 pour obtenir des informations détaillées sur l’appel de délégué.) Si une exception se produit pendant l’appel d’un tel délégué et que cette exception n’est pas interceptée dans la méthode appelée, la recherche d’une clause catch d’exception se poursuit dans la méthode qui a appelé le délégué, comme si cette méthode avait directement appelé la méthode à laquelle ce délégué a fait référence.
L’appel d’une instance de délégué dont la liste d’appel contient plusieurs entrées, procède en appelant chacune des méthodes de la liste d’appel, de façon synchrone, dans l’ordre. Chaque méthode appelée est passée au même jeu d’arguments que celui fourni à l’instance de délégué. Si un tel appel de délégué inclut des paramètres de référence (§15.6.2.3.3), chaque appel de méthode se produit avec une référence à la même variable ; les modifications apportées à cette variable par une méthode de la liste d’appel seront visibles par les méthodes plus loin dans la liste d’appel. Si l’appel de délégué inclut des paramètres de sortie ou une valeur de retour, leur valeur finale provient de l’appel du dernier délégué de la liste. Si une exception se produit pendant le traitement de l’appel d’un tel délégué et que cette exception n’est pas interceptée dans la méthode appelée, la recherche d’une clause catch d’exception se poursuit dans la méthode qui a appelé le délégué, et toutes les méthodes plus bas dans la liste d’appels ne sont pas appelées.
Toute tentative d’appel d’une instance de délégué dont la valeur entraîne null
une exception de type System.NullReferenceException
.
Exemple : L’exemple suivant montre comment instancier, combiner, supprimer et appeler des délégués :
delegate void D(int x); class C { public static void M1(int i) => Console.WriteLine("C.M1: " + i); public static void M2(int i) => Console.WriteLine("C.M2: " + i); public void M3(int i) => Console.WriteLine("C.M3: " + i); } class Test { static void Main() { D cd1 = new D(C.M1); cd1(-1); // call M1 D cd2 = new D(C.M2); cd2(-2); // call M2 D cd3 = cd1 + cd2; cd3(10); // call M1 then M2 cd3 += cd1; cd3(20); // call M1, M2, then M1 C c = new C(); D cd4 = new D(c.M3); cd3 += cd4; cd3(30); // call M1, M2, M1, then M3 cd3 -= cd1; // remove last M1 cd3(40); // call M1, M2, then M3 cd3 -= cd4; cd3(50); // call M1 then M2 cd3 -= cd2; cd3(60); // call M1 cd3 -= cd2; // impossible removal is benign cd3(60); // call M1 cd3 -= cd1; // invocation list is empty so cd3 is null // cd3(70); // System.NullReferenceException thrown cd3 -= cd1; // impossible removal is benign } }
Comme indiqué dans l’instruction
cd3 += cd1;
, un délégué peut être présent dans une liste d’appel plusieurs fois. Dans ce cas, il est simplement appelé une fois par occurrence. Dans une liste d’appel telle que celle-ci, lorsque ce délégué est supprimé, la dernière occurrence de la liste d’appel est celle qui a été réellement supprimée.Immédiatement avant l’exécution de l’instruction finale,
cd3 -= cd1
;, le déléguécd3
fait référence à une liste d’appels vide. La tentative de suppression d’un délégué d’une liste vide (ou pour supprimer un délégué inexistant d’une liste non vide) n’est pas une erreur.La sortie produite est la suivante :
C.M1: -1 C.M2: -2 C.M1: 10 C.M2: 10 C.M1: 20 C.M2: 20 C.M1: 20 C.M1: 30 C.M2: 30 C.M1: 30 C.M3: 30 C.M1: 40 C.M2: 40 C.M3: 40 C.M1: 50 C.M2: 50 C.M1: 60 C.M1: 60
exemple de fin
ECMA C# draft specification