Partager via


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 publicmodificateurs , et internalprivate les modificateurs protectedcontrô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 voidsoit , 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 et D2 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 et M ont le même nombre de paramètres, et chaque paramètre dans D a le même modificateur de paramètre par référence que le paramètre correspondant dans M.
  • 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 dans M.
  • Pour chaque paramètre de référence, le type de paramètre dans D est identique au type de paramètre dans M.
  • L’une des valeurs suivantes est vraie :
    • D et M sont à la fois retour-no-value
    • D et M 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 du M type de retour de D.
    • D et M sont les deux retours par réf, une conversion d’identité existe entre le type de retour et M le type de retour de D, 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 et B.M1 sont compatibles avec les types D1 délégués et D2, car elles ont le même type de retour et la même liste de paramètres. Les méthodes B.M2, B.M3et B.M4 sont incompatibles avec les types D1 délégués et D2, car elles ont différents types de retour ou listes de paramètres. Les méthodes B.M5 et B.M6 sont toutes deux compatibles avec le type D3dé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 type Predicate<int> délégué et la méthode X.G est compatible avec le type Predicate<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 Print méthode est compatible avec le Action<string> type de délégué, car tout appel d’un Action<string> délégué serait également un appel valide de la Print méthode.

Si la signature de la Print méthode ci-dessus a été modifiée Print(object value, bool prependTimestamp = false) par exemple, la Print méthode n’est plus compatible avec Action<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 et cd2 sont instanciés, ils encapsulent chacune une méthode. Lorsqu’il cd3 est instancié, il a une liste d’appel de deux méthodes et M1 M2, dans cet ordre. cd4la liste d’appels contient M1, M2et M1, dans cet ordre. Pour cd5, la liste d’appel contient M1, M2, M1, M1et M2, 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 de cd3 sa liste d’appel n’a qu’un seul membre, mais ce membre est une liste des méthodes M1 et M2 ces méthodes sont appelées td3 dans le même ordre qu’elles sont appelées par cd3. De même, lorsqu’elle td4 est instanciée, sa liste d’appel n’a que deux entrées, mais elle appelle les trois méthodes M1, M2et M1, dans cet ordre tout comme cd4 cela.

La structure de la liste d’appel affecte la soustraction des délégués. Délégué cd6, créé en soustrayant cd2 (qui appelle ) de cd4 (qui appelle M2, M2et M1) appelle M1M1 et M1. Toutefois, délégué td6, créé en soustrayant cd2 (qui appelle ) de td4 (qui appelle M1M2, M2et M1) appelle M1toujours , M2 et M1, dans cet ordre, comme M2 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