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 répertorie une ou plusieurs méthodes, appelées chacune entité joignable. Par exemple, une entité joignable se compose d’une instance et d’une méthode sur cette instance. Pour les méthodes statiques, une entité joignable ne se compose que 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 joignables 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’elle encapsule. Tout ce qui importe est que ces méthodes soient compatibles (§ 20.4) avec le type du délégué. Les délégués sont ainsi parfaitement adaptés à l’appel « anonyme ». fin de la remarque

20.2 Déclarations de délégués

Une déclaration de délégué, ou delegate_declaration, est une déclaration de type, ou 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 § 23.2.

L’affichage du même modificateur plusieurs fois dans une déclaration de délégué est une erreur de compilation.

Une déclaration de délégué qui fournit un objet variant_type_parameter_list est une déclaration de délégué générique. En outre, tout délégué imbriqué dans une déclaration de classe générique ou 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 modificateur new n’est autorisé que sur les délégués déclarés dans un autre type, auquel cas il spécifie que ce délégué masque un membre hérité du même nom, comme décrit dans le § 15.3.5.

Les modificateurs public, protected, internal et private contrôlent l’accessibilité du type de délégué. Selon le contexte dans lequel la déclaration de délégué a lieu, certains de ces modificateurs peuvent ne pas être autorisés (§ 7.5.2).

Le nom du type du délégué est l’identificateur.

Comme pour les méthodes (§ 15.6.1), si ref est présent, la méthode de délégué returns-by-ref est utilisée. Sinon, si return_type est void, la méthode de délégué returns-no-value est utilisée. Sinon, la méthode de délégué returns-by-value est utilisée.

L’attribut parameter_list facultatif spécifie les paramètres du délégué.

L'attribut return_type d’une déclaration de délégué returns-by-value ou returns-no-value spécifie le type du résultat, le cas échéant, retourné par le délégué.

L'attribut ref_return_type d’une déclaration de délégué returns-by-ref spécifie le type de la variable référencée par l'attribut variable_reference (§ 9.5) retourné par le délégué.

L'attribut facultatif variant_type_parameter_list (§ 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 de délégué doit être void ou sécurisé en sortie (§ 18.2.3.2).

Tous les types de paramètres d’un type de 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 en sortie.

Remarque : les paramètres de sortie doivent être sécurisés en entée en raison de restrictions d’implémentation courantes. fin de la remarque

En outre, chaque contrainte de type de classe, de type d’interface et de paramètre de type appliquée à un paramètre de type d’un délégué doit être sécurisée en entrée.

Les types de délégués en C# ont des noms équivalents, mais ne sont pas équivalents structurellement.

Exemple :

delegate int D1(int i, double d);
delegate int D2(int c, double d);

Les types de délégués D1 et D2 sont différents et ne sont donc 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 de délégué construit. Les types de paramètres et le type de retour d’un type de délégué construit sont déterminés en substituant, pour chaque paramètre de type défini dans la déclaration du délégué, l’argument de type correspondant du type de délégué construit.

La seule façon de déclarer un type de délégué est via un delegate_declaration. Chaque type de 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 de délégués sont implicitement sealed, il n’est donc pas possible de dériver un type à partir d'un type de délégué. Il n’est pas non plus possible de déclarer un type de classe non délégué dérivé de System.Delegate. System.Delegate n’est en soi un type de délégué ; il s’agit d’un type de classe à partir duquel tous les types de 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 classe Delegate, comme décrit dans le § 15.3.4. En outre, chaque type de délégué doit fournir une méthode Invoke non générique dont la liste de paramètres correspond à celle spécifiée dans la déclaration du délégué (parameter_list) dont le type de retour correspond à l'attribut return_type ou ref_return_type de cette déclaration, et, pour les délégués returns-by-ref, dont l'attribut ref_kind correspond également à celui indiqué dans la déclaration. La méthode Invoke doit être au moins aussi accessible que le type de délégué qui la contient. Appeler la méthode Invoke sur un type de délégué équivaut sémantiquement à utiliser 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 l'être à une classe ou une instance déléguée, respectivement. En particulier, il est possible d’accéder aux membres du type System.Delegate via la syntaxe d’accès aux membres habituelle.

20.4 Compatibilité des délégués

Une méthode ou un type de délégué M est compatible avec un type de délégué D si toutes les conditions 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, il existe une conversion d’identité (§ 10.2.2) ou une conversion de référence implicite (§ 10.2.8) entre le type de paramètre dans D et le type de paramètre correspondant dans M.
  • Pour chaque paramètre par référence, le type de paramètre dans D est identique au type de paramètre dans M.
  • Une des conditions suivantes est remplie :
    • D et M sont tous deux de type returns-no-value.
    • D et M sont des returns-by-value (§ 15.6.1, § 20.2), et il existe une conversion d'identité ou une conversion de référence implicite entre le type de retour de M et le type de retour de D.
    • D et M sont des returns-by-ref, il existe une conversion d'identité entre le type de retour de M et le type de retour de D, et les deux ont le même ref_kind.

Cette définition de la compatibilité autorise 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 deux types de délégués D1 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.M3 et B.M4 sont incompatibles avec les types de délégués D1 et D2, car elles ont des types de retour ou des listes de paramètres différents. Les méthodes B.M5 et B.M6 sont compatibles avec le type de délégué D3.

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 de délégué Predicate<int>, et la méthode X.G est compatible avec le type de délégué Predicate<string>.

exemple de fin

Remarque : la compatibilité des délégués signifie intuitivement qu'une méthode est compatible avec un type de délégué si chaque appel du délégué peut être remplacée par un appel de la méthode sans enfreindre la sécurité des types, 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 méthode Print est compatible avec le type de délégué Action<string>, car tout appel d’un délégué Action<string> serait également un appel valide de la méthode Print.

Si la signature de la méthode Print ci-dessus était changée, par exemple, en Print(object value, bool prependTimestamp = false), la méthode Print ne serait plus compatible avec Action<string> conformément aux règles de cette sous-clause.

fin de la remarque

20.5 Instanciation des délégués

Une instance d’un délégué est créée par un delegate_creation_expression (§12.8.17.5), une conversion en type délégué, combinaison de délégués ou suppression de délégué. La nouvelle instance de délégué fait alors référence à un ou plusieurs des éléments suivants :

  • La méthode statique référencée dans l'expression delegate_creation_expression, ou
  • L'objet cible (qui ne peut pas être null) et la méthode d’instance référencés dans l'expression delegate_creation_expression, ou
  • Autre délégué (§12.8.17.5).

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 de délégués non-null sont combinées, leurs listes d’appel sont concaténées (dans l’ordre suivant : opérande de gauche, puis opérande de droite) 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.17.5).

Les délégués sont combinés à l’aide des opérateurs binaires + (§ 12.10.5) et += (§ 12.21.4). Un délégué peut être supprimé d’une combinaison de délégués à l’aide des opérateurs binaires - (§ 12.10.6) et -= (§ 12.21.4). Les délégués peuvent être comparés pour vérifier leur é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.
   }
}

Lorsque cd1 et cd2 sont instanciés, ils encapsulent chacun une méthode. Lorsque cd3 est instancié, il dispose d'une liste d'appel de deux méthodes, M1 et M2, dans cet ordre. La liste d'appel de cd4 contient M1, M2 et M1, dans cet ordre. Pour cd5, la liste d’appel contient M1, M2, M1, M1 et M2, dans cet ordre.

Lorsque vous créez un délégué à partir d'un autre délégué avec une expression delegate_creation_expression, le résultat est une liste d'appels avec une structure différente de l'originale, mais qui aboutit aux mêmes méthodes appelées dans le même ordre. Lorsque td3 est créé à partir de cd3, sa liste d'appel ne comporte qu'un seul membre, mais ce membre est une liste des méthodes M1 et M2, et ces méthodes sont appelées par td3 dans le même ordre que celles appelées par cd3. De même, lorsque td4 est instancié, sa liste d'appels ne comporte que deux entrées, mais il appelle les trois méthodes M1, M2 et M1, dans cet ordre, tout comme cd4.

La structure de la liste d’appel affecte la soustraction des délégués. Le délégué cd6, créé en soustrayant cd2 (qui appelle M2) de cd4 (qui appelle M1, M2 et M1), appelle M1 et M1. Toutefois, le délégué td6, créé en soustrayant cd2 (qui appelle M2) de td4 (qui appelle M1, M2 et M1), appelle toujours M1, M2 et M1, dans cet ordre, car M2 entrée unique 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’appel.

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 délégué est créé avec sa propre liste d'appel ; les listes d'appel des délégués combinés ou supprimés restent inchangées. fin de la remarque

20.6 Appel de délégué

C# fournit une syntaxe spéciale pour appeler un délégué. Lorsqu'une instance de délégué non-null dont la liste d'invocation contient une entrée est appelée, elle appelle la méthode unique avec les mêmes arguments qui lui ont été donnés et retourne la même valeur que la méthode référencée. (Consultez §12.8.10.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 se poursuit en appelant chacune des méthodes de la liste d’appel de manière synchrone, dans l’ordre. Chaque méthode ainsi nommée reçoit le même ensemble d'arguments que celui qui a été fourni à l'instance déléguée. Si cet appel de délégué inclut des paramètres de référence (§ 15.6.2.3.3), chaque appel de méthode se produira avec une référence à la même variable ; les modifications apportées à cette variable par une méthode dans la liste d'appel seront visibles pour les méthodes situées plus bas 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 cet appel de délégué et que cette exception n’est pas interceptée dans la méthode appelée, la recherche d'une clause d'interception d'exception se poursuit dans la méthode qui a appelé le délégué, et aucune autre méthode située plus bas dans la liste d'appel n'est appelée.

Toute tentative d’appel d’une instance de délégué dont la valeur est null entraîne 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 apparaître plusieurs fois dans une liste d’appel. 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é, c'est la dernière occurrence dans la liste d'appel qui est 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’appel vide. Tenter de supprimer un délégué d'une liste vide (ou de 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