2.5. Classes délégations, délégués



Plan général:   ...........retour au plan général


1. Les classes de délégations

    1.1 Définition classe de délégation - délégué
    1.2 Délégué et méthodes de classe - définition
    1.3 Délégué et méthodes de classe - informations pendant l'exécution
    1.4 Délégué et méthodes d'instance - définition
    1.5 Plusieurs méthodes pour le même délégué
    1.6 Exécution des méthodes d'un délégué multicast - Exemple de code




Il existe en Delphi la notion de pointeur de méthode utilisée pour implanter la notion de gestionnaire d"événements. C# a repris cette idée en l'encapsulant dans un concept objet plus abstrait : la notion de classes de délégations. Dans la machine virtuelle CLR, la gestion des événements est fondée sur les classes de délégations, il est donc essentiel de comprendre le modèle du délégué pour comprendre le fonctionnement des événements en C#.


1. Les classes de délégations

Note de microsoft à l'attention des développeurs :
La classe Delegate est la classe de base pour les types délégué. Toutefois, seuls le système et les compilateurs peuvent dériver de manière explicite de la classe Delegate ou MulticastDelegate. En outre, il n'est pas possible de dériver un nouveau type d'un type délégué. La classe Delegate n'est pas considérée comme un type délégué. Il s'agit d'une classe utilisée pour dériver des types délégué.


1.1 Définition classe de délégation - délégué

Le langage C# contient un mot clé delegate , permettant au compilateur de construire une classe dérivée de la classe MulticastDelegate dérivée elle-même de la classe Delegate. Nous ne pouvons pas instancier une objet de classe Delegate, car le constructeur est spécifié protected et n'est donc pas accessible :



C'est en fait via ce mot clé delegate que nous allons construire des classes  qui ont pour nom : classes de délégations. Ces classes sont des classes du genre référence, elles sont instanciables et un objet de classe délégation est appelé un délégué. Un objet de classe délégation permet de référencer (pointer vers) une ou plusieurs méthodes. Il s'agit donc de l'extension de la notion de pointeur de méthode de Delphi. Selon les auteurs une classe de délégation peut être ausi nommée classe déléguée, type délégué voir même tout simplement délégué. Il est essentiel dans le texte lu de bien distinguer la classe et l'objet instancié.

Lorsque nous utiliserons le vocable classe délégué ou type délégué nous parlerons parlerons de la classe de délégation d’un objet délégué.



Un objet de classe délégation permet de référencer (pointer vers) une ou plusieurs méthodes.

Il donc possible de créer un nouveau type de délégué (une nouvelle classe) dans un programme, ceci d'une seule manière : en utilisant le qualificateur delegate. Ci-dessous nous déclarons un type délégué (une nouvelle classe particulière) nommé NomTypeDelegue  :
 
delegate string  NomTypeDelegue ( int parametre) ;


Un objet délégué peut donc être instancé à partir de cette "classe" comme n'importe quel autre objet :

NomTypeDelegue Obj = new NomTypeDelegue ( <paramètre> )

Il ne doit y avoir qu'un seul paramètre <paramètre> et c'est obligatoirement un nom de méthode. Soit MethodeXYZ le nom de la méthode passé en paramètre, nous dirons alors  que le délégué (l'objet délégué) référence la méthode MethodeXYZ .



Les méthodes référencées par un délégué peuvent être :
des méthodes de classe ( static )
            ou
des méthodes d'instance
Toutes les méthodes référencées par un même délégué ont la même signature partielle :
  • même type de retour du résultat,
  • même nombre de paramètres,
  • même ordre et type des paramètres,
  • seul leur nom diffère.



1.2 Délégué et méthodes de classe - définition

Un type commençant par le mot clef delegate est une classe délégation.

ci-dessous la syntaxe de 2 exemples de déclaration de classe délégation :
  • delegate string Deleguer1( int x ) ;
  • delegate void Deleguer2( string s ) ;
Un objet instancié à partir de la classe Deleguer1 est appelé un délégué de classe Deleguer1 :
  • Deleguer1  FoncDeleg1 = new Deleguer1 ( Fonc1 ) ;
  • où Fonc1 est une méthode  :
  • static string Fonc1 ( int x ) { ... }
Un objet instancié à partir de la classe Deleguer2 est appelé un délégué de classe Deleguer2 :
  • Deleguer2  FoncDeleg2 = new Deleguer2 ( Fonc2 ) ;
  • où Fonc2 est une autre méthode  :
  • static void  Fonc2 ( string x ) { ... }


Nous avons créé deux types délégations nommés Deleguer1 et Deleguer2 :
  • Le type Deleguer1 permet de référencer des méthodes ayant un paramètre de type int et renvoyant un string.
  • Le type Deleguer2 permet de référencer des méthodes ayant un paramètre de type string et ne renvoyant rien.

Les fonctions de classe Fonc1 et Fonc11 répondent à la signature partielle du type Deleguer1
  • static string Fonc1 ( int x ) { ... }
  • static string Fonc11 ( int x ) { ... }
On peut créer un objet (un délégué) qui va référencer l'une ou l'autre de ces deux fonctions :
  • Deleguer1  FoncDeleg1 = new Deleguer1 ( Fonc1 ) ;
                                 ou bien
  • Deleguer1  FoncDeleg1 = new Deleguer1 ( Fonc11 ) ;
On peut maintenant appeler le délégué FoncDeleg1 dans une instruction avec un paramètre d'entrée de type int, selon que le délégué référence Fonc1 ou bien Fonc11 c'est l'une ou l'autre des ces fonctions qui est en fait appelée.

Source d'un exemple C# et exécution :
delegate  string  Deleguer1 ( int  x );

class 
ClasseA {
  static 
string  Fonc1  int  )   {
    return (
x * 10 ) .ToString ( );
  }
  static  string  Fonc11  int  )   {
    return (
x * 100 ) .ToString ( );
  }

  static void  Main ( string [] args )  {
    
 string  Fonc1 ( 32 );
    
System .Console.WriteLine ("Fonc1(32) = " + s );
      Fonc11 ( 32 ); // appel de fonction classique
     System .Console.WriteLine ("Fonc11(32) = " + s );
     System .Console.WriteLine ("\nLe délégué référence Fonc1 :");
     Deleguer1  FoncDeleg1 
new  Deleguer1  Fonc1  ) ;
    
FoncDeleg1 ( 32 ); // appel au délégué qui appelle la fonction
     System .Console.WriteLine ("FoncDeleg1(32) = " + s );
     
System .Console.WriteLine ("\nLe délégué référence maintenant Fonc11 :");
     FoncDeleg1 = new  Deleguer1  Fonc11 ) ; // on change d'objet référencé (de fonction)
      FoncDeleg1 ( 32 ); // appel au délégué qui appelle la fonction
     System .Console.WriteLine ("FoncDeleg1(32) = " + s );
     System .Console.ReadLine ( );
  }
}


Résultats d'exécution sur la console :




1.3 Délégué et méthodes de classe - informations pendant l'exécution

Comme une référence de délégué peut pointer (référencer) vers des méthodes de classes différentes au cours de l'exécution, il est intéressant d'obtenir des informations sur la méthode de classe actuellement référencée par le délégué.

Nous donnons ci-dessous les deux propriétés publiques qui sont utiles lors de cette recherche d'informations, elles proviennent de la classe mère Delegate non héritable par programme :






La propriété Target sert plus particulièrement lorsque le délégué référence une méthode d'instance, la propriété Method n'est utilisable que lorsque la méthode référencée par le délégué est une méthode de classe (méthode marquée static). Lorsque la méthode référencée par le délégué est une méthode de classe le champ Target a la valeur null.

Ci-dessous nous  avons extrait quelques informations concernant la propriété Method qui est elle-même de classe MethodInfo :



Ces propriétés sont des membres de la propriété Method qui est applicable uniquement lorsque le délégué en cours référence une méthode de classe (qualifiée static). Nous illustrons dans la figure ci-après, dans le cas d'une méthode de classe, l'utilisation des propriétés Name,  DeclaringType et ReturnType membres de la propriété Method  :



Nous obtenons ainsi des informations sur le nom, le type du résultat et la classe de la méthode static pointée par le délégué.

Source complet exécutable d'un exemple d'information sur la méthode de classe référencée :
namespace PrDelegate {

 delegate  string  Deleguer1 ( int  x );

 class 
ClasseA {
   static 
string  Fonc1  int  )   {
    return (
x * 10 ) .ToString ( );
   }
 
   static void  Main ( string [] args )  {
     System .Console.WriteLine ("\nLe délégué référence Fonc1 :");
     Deleguer1  FoncDeleg1 
new  Deleguer1  Fonc1  ) ;
    
System .Console.WriteLine ( "nom : "+FoncDeleg1.Method.Name );
     
System .Console.WriteLine ("classe : "+ FoncDeleg1.Method.DeclaringType.ToString( ) );
     System .Console.WriteLine ( "retour : "+FoncDeleg1.Method.ReturnType.ToString( ) );
     System .Console.ReadLine ( );
   }
 }

}

Résultats d'exécution sur la console :




1.4 Délégué et méthodes d'instance - définition

Outre un méthode de classe, un délégué peut pointer aussi vers une méthode d'instance(une méthode d'un objet). Le fonctionnement (déclaration, instanciation, utilisation) est identique à celui du référencement d'une méthode de classe, avec syntaxiquement l'obligation, lors de l'instanciation du délégué, d'indiquer le nom de l'objet ainsi que le nom de la méthode Obj.Methode (similitude avec le pointeur de méthode en Delphi).

Ci-dessous la syntaxe d'un exemple de déclaration de classe délégation pour une méthode d'instance, nous devons :

1°) Déclarer ne classe contenant une méthode public
class clA {
  public int meth1(char x) {
    ....
  }
}

2°) Déclarer un type délégation
 delegate int Deleguer( char x ) ;

3°) Instancier un objet de la classe clA
clA ObjA = new clA ( ) ;
4°) Instancier à partir de la classe Deleguer un délégué
Deleguer  FoncDeleg = new Deleguer ( ObjA.meth1 ) ;




Nous illustrons dans la figure ci-après, dans le cas d'une méthode d'instance, l'utilisation de membres de la propriété Method et de la propriété Target  :



Source complet exécutable d'un exemple d'information sur la méthode d'instance référencée :
namespace PrDelegate {

 delegate  int  Deleguer ( char  x );

 class 
ClasseA {
   public 
 int  meth1   char  )   {
      return
  x  ;
   }
 
   static void  Main ( string [] args )  {
     ClasseA ObjX , ObjA = new ClasseA( );
     System .Console.WriteLine ("Un délégué référence ObjA.meth1 :");
     Deleguer  FoncDeleg 
new  Deleguer   ObjA.meth1  ) ;
     ObjX = (ClasseA)FoncDeleg.Target;
     if (ObjX.Equals(ObjA))
            System.Console.WriteLine ("Target référence bien ObjA");
     else System.Console.WriteLine ("Target ne référence pas ObjA");
    
System.Console.WriteLine ( "\nnom : "+FoncDeleg.Method.Name );
     
System.Console.WriteLine ("classe : "+ FoncDeleg.Method.DeclaringType.ToString( ) );
     System.Console.WriteLine ( "retour : "+FoncDeleg.Method.ReturnType.ToString( ) );
     System.Console.ReadLine ( );
   }
 }

}

Résultats d'exécution sur la console :


Dans le programme précédent, les lignes de code suivantes :

     ObjX = (ClasseA)FoncDeleg.Target ;
     if (ObjX.Equals(ObjA))
            System.Console.WriteLine ("Target référence bien ObjA") ;
     else System.Console.WriteLine ("Target ne référence pas ObjA") ;

servent à faire "pointer" la référence ObjX vers l'objet vers lequel pointe FoncDeleg.Target. La référence de cet objet est transtypée car ObjX est de type ClasseA, FoncDeleg.Target est de type Object et le compilateur n'accepterait pas l'affectation ObjX = FoncDeleg.Target. Le test  if (ObjX.Equals(ObjA))... permet de nous assurer que les deux références ObjX et ObjA pointent bien vers le même objet.


1.5 Plusieurs méthodes pour le même délégué

C# autorise le référencement de plusieurs méthodes par le même délégué, nous utiliserons le vocabulaire de délégué multicast pour bien préciser qu'il référence plusieurs méthodes. Le délégué multicast conserve les référencements dans une liste d'objet. Les méthodes ainsi référencées peuvent chacune être du genre méthode de classe ou méthode d'instance, elles doivent avoir la même signature.

Rappelons qu'un type délégué multicast est une classe qui hérite intrinsèquement de la classe MulticasteDelegate :



La documentation de .Net Framework indique que la classe MulticasteDelegate contient en particulier trois champs privés :

Object  _target ;
Int32  _methodPtr ;
MulticasteDelegate  _prev ;

Le champ _prev est utilisé pour maintenir une liste de MulticasteDelegate

Lorsque nous déclarons un progarmme comme celui-ci :

delegate  int  Deleguer ( char  x );

class  ClasseA {
  public int  meth100  (  char x  )   {
      System.Console.WriteLine ("Exécution de meth100('"+x+"')");
      return x+100 ;
  } 
  static void 
Main ( string [] args {
     ClasseA  ObjA = new ClasseA( );
     Deleguer  FoncDeleg  new  Deleguer   ObjA.meth100  ) ;
  }
}

Lors de l'exécution, nous avons vu qu'il y a création d'un ObjA de ClasseA et d'un objet délégué FoncDeleg, les propriétés Method et Target sont automatiquement initialisées par le compilateur :




En fait, ce sont les champs privés qui sont initialisés et les propriétés Method et Target qui sont en lecture seuelement, lisent les contenus respectifs de _methodPtr et de _target, le champ _prev étant pour l'instant mis à null , enfin la méthode meth100(...) est actuellement en tête de liste.

Figure virtuelle de l'objet délégué à ce stade :



Il est possible d'ajouter une nouvelle méthode meth101(...) au délégué qui va la mettre en tête de liste à la place de la méthode
meth100(...) qui devient le deuxième élément de la liste. C# utilise l'opérateur d'addition pour implémenter l'ajout d'une nouvelle méthode au délégué. Nous étendons le programme précédent  :

delegate  int  Deleguer ( char  x );

class  ClasseA {
  public int  meth100  (  char x  )   {
      System.Console.WriteLine ("Exécution de meth100('"+x+"')");
      return x+100 ;
  }
  public int  meth101  (  char x  )   {
      System.Console.WriteLine ("Exécution de meth101('"+x+"')");
      return x+101 ;
  }
  static void  Main ( string [] args {
     ClasseA  ObjA = new ClasseA( );
     //-- meth100 est en tête de liste :
     Deleguer  FoncDeleg  new  Deleguer   ObjA.meth100  ) ;
     // meth101 est ajoutée en tête de liste devant meth100 :
     FoncDeleg = FoncDeleg + new  Deleguer   ObjA.meth101  ) ;
  }
}

Figure virtuelle de l'objet délégué à cet autre stade :


C# permet de consulter et d'utiliser si nous le souhaitons toutes les référencement de méthodes en nous renvoyant la liste dans une tableau de référence de type Delegate grâce à la méthode
GetInvocationList. Le source ci-dessous retourne dans le tableau Liste, la liste d'appel dans l'ordre d'appel, du délégué FoncDeleg :

Delegate[ ] Liste = FoncDeleg.GetInvocationList( ) ;


1.6 Exécution des méthodes d'un délégué multicast - Exemple de code

Lorsque l'on invoque le délégué sur un paramètre effectif, C# appelle et exécute séquentiellement les méthodes contenues dans la liste jusqu'à épuisement. L'ordre d'appel est celui du stockage : la première stockée est exécutée en premier, la suivante après, la dernière méthode ajoutée est exécutée en dernier, s'il y a un résultat de retour, c'est celui de la dernière méthode ajoutée qui est renvoyé , les autres résultats de retour sont ignorés. L'exemple ci-dessous reprend les notions que nous venons d'exposer.

Source complet exécutable d'un exemple de délégué multicast :
namespace PrDelegate {

 delegate  int  Deleguer ( char  x );

 class 
ClasseA {
   
    public int meth100  (  char x  )   {
      System.Console.WriteLine ("Exécution de meth100('"+x+"')");
      return x+100 ;
    }
    public int meth101  (  char x  )   {
      System.Console.WriteLine ("Exécution de meth101('"+x+"')");
      return x+101 ;
    }
    public int  meth102  (  char x  )   {
      System.Console.WriteLine ("Exécution de meth102('"+x+"')");
      return x+102 ;
    }
    public static int  meth103  (  char x  )   {
      System.Console.WriteLine ("Exécution de meth103('"+x+"')");
      return x+103 ;
    }
 
   static void  Main ( string [] args )  {
     System.Console.WriteLine ("Un délégué référence ObjA.meth1 :" ) ;
     ClasseA ObjX , ObjA = new ClasseA( );
     //-- instanciation du délégué avec ajout de 4 méthodes :
     Deleguer  FoncDeleg  new  Deleguer   ObjA.meth100  ) ;
     FoncDeleg +=  new  Deleguer   ObjA.meth101  ) ;
     FoncDeleg +=  new  Deleguer   ObjA.meth102  ) ;
     FoncDeleg +=  new  Deleguer   meth103  ) ;
     
     //--la méthode meth103 est en tête de liste :
     ObjX = (ClasseA)FoncDeleg.Target ;
     if (ObjX == null System.Console.WriteLine ("Méthode static, Target = null") ;
     else if (ObjX.Equals(ObjA))System .Console.WriteLine ("Target référence bien ObjA") ;
     else System.Console.WriteLine ("Target ne référence pas ObjA") ;
     System.Console.WriteLine ( "\nnom : "+FoncDeleg.Method.Name );
     System.Console.WriteLine ( "classe : "+FoncDeleg.Method.DeclaringType.ToString( ) ) ;
     System.Console.WriteLine ( "retour : "+FoncDeleg.Method.ReturnType.ToString( ) );

     //--Appel du délégué sur le paramètre effectif 'a' :
     ObjA.champ = FoncDeleg('a') ; //code ascii 'a' = 97
     System.Console.WriteLine ( "\nvaleur du champ : "+ObjA.champ) ;
     System.Console.WriteLine ( "----------------------------------") ;

     //-- Parcours manuel de la liste des méthodes référencées :
     Delegate[ ] Liste = FoncDeleg.GetInvocationList( ) ;
     foreach ( Delegate Elt in Liste )
    {
      ObjX = (ClasseA)Elt.Target ;
      if (ObjX == null) System.Console.WriteLine ("Méthode static, Target = null") ;
      else if (ObjX.Equals(ObjA))System .Console.WriteLine ("Target référence bien ObjA") ;
      else System.Console.WriteLine ("Target ne référence pas ObjA") ;
      System.Console.WriteLine ( "\nnom : "+Elt.Method.Name ) ;
      System.Console.WriteLine ( "classe : "+Elt.Method.DeclaringType.ToString( ) ) ;
      System.Console.WriteLine ( "retour : "+Elt.Method.ReturnType.ToString( ) ) ;
      System.Console.WriteLine ( "----------------------------------") ;
    }
    System.Console.ReadLine ( ) ;
   }
 }

}

Résultats d'exécution sur la console :


Nous voyons bien que le délégué FoncDeleg contient la liste des référencement des méthodes meth100, meth101,meth102 et meth103 ordonné comme figuré ci-dessous :


Remarquons que le premier objet de la liste est une référence sur une méthode de classe, la propriété Target renvoie la valeur null (le champ _target est à null).

La méthode de classe meth103 ajoutée en dernier est bien en tête de liste :


L'invocation du délégué lance l'exécution séquentielle des 4 méthodes :

et le retour du résutat est celui de meth103('a') :


Le parcours manuel de la liste montre bien que ce sont des objets de type Delegate qui sont stockés et que l'on peut accéder entre autre possibilités, à leurs propriétés :