Les méthodes anonymes de C# v2 par Patrick Smacchia

 

Prérequis. 1

Introduction. 1

Introduction aux méthodes anonymes de C#2. 1

Un premier exemple. 1

Méthodes anonymes et arguments. 1

Une subtilité syntaxique. 2

Les méthodes anonymes et la généricité. 2

Exemples simples d’utilisation des méthodes anonymes. 2

Le compilateur C#2 et les méthodes anonymes. 2

Le cas simple. 2

Une méthode anonyme accède à une variable locale de la méthode qui l’encapsule. 3

Variable locale capturée et complexité du code. 3

Une méthode anonyme accède à un argument de la méthode qui l’encapsule. 4

Une méthode anonyme accède à un membre de la classe qui définie la méthode qui l’encapsule. 4

Exemples avancés d’utilisation des méthodes anonymes. 4

Définitions: fermeture (closure en anglais) et environnement lexical 4

Un peu plus loin dans la compréhension des fermetures. 4

Utilisation des fermetures. 5

Délégué et fermeture. 5

Conclusion. 5

Prérequis

·         Une bonne connaissance générale de C#1.

·         Les délégués de .NET1 Dans cet article, nous nommerons une classe déléguée une délégation. Rappelons qu’une délégation est définie avec le mot-clé C# delegate et dérive automatiquement et implicitement de la classe System.Delegate. L’instance d’une délégation sera nommée un délégué. Rappelons aussi qu’un délégué sert à référencer et à invoquer une ou plusieurs méthodes. Rappelons aussi que si un délégué référence une méthode d’instance, il encapsule aussi une référence vers l’instance sur laquelle la méthode doit être invoquée.

Introduction

Les génériques sont, à raison, présentés comme la fonctionnalité phare des langages de .NET v2. En ‘surfant’ hors des sentiers battus, je me suis aperçu que les nouvelles fonctionnalités de méthodes anonymes et d’itérateurs sont elles aussi très intéressantes. Contrairement aux génériques, ces deux fonctionnalités n’impliquent aucune nouvelle instruction IL. Toute la magie se situe au niveau du compilateur. Je vous propose une série de deux articles pour vous présenter ces trouvailles, d’abord un article sur les méthodes anonymes puis un autre sur les itérateurs. Vous comprendrez que c’est l’ordre logique d’apprentissage. Chacun des deux articles aura la même structure : une présentation ‘classique’ de la fonctionnalité suivie de l’analyse du travail du compilateur pour enfin commenter des utilisations avancées (détournées ?!).

Introduction aux méthodes anonymes de C#2

Un premier exemple

Commençons par un premier exemple de réécriture de code C#1 au moyen des méthodes anonymes de C#2. Voici du code compilable en C#1 qui montre comment référencer une méthode au travers d’un délégué :

using System;

class Program{

   delegate void DelegateType();

   static DelegateType GetMethod(){

      return new DelegateType(MethodBody);

   }

   static void MethodBody(){

      Console.WriteLine("Hello");

   }

   static void Main(){

      DelegateType delegateInstance = GetMethod();

      delegateInstance();

      delegateInstance();

      Console.ReadKey();

   }

}

Voici le même code réécrit en C#2 au moyen d’une méthode anonyme:

using System;

class Program{

   delegate void DelegateType();

   static DelegateType GetMethod(){

      return delegate(){Console.WriteLine("Hello");};

   }

   static void Main(){

      DelegateType delegateInstance = GetMethod();

      delegateInstance();

      delegateInstance();

      Console.ReadKey();

   }

}

Plusieurs remarques s’imposent :

·         Le mot-clé delegate a une nouvelle utilisation en C#2. Il est utilisé au sein du corps d’une méthode pour indiquer au compilateur qu’il doit s’attendre à trouver le corps d’une méthode anonyme.

·         On constate que l’on peut assigner une méthode anonyme à un délégué.

·         On comprend le nom de méthode anonyme pour cette nouvelle fonctionnalité : La méthode définie au sein de GetMethod() n’a effectivement pas de nom. On peut cependant l’invoquer car elle est référencée par un délégué.

Notez aussi que la syntaxe d’assignement de plusieurs méthodes à un délégué avec l’opérateur += est utilisable avec les méthodes anonymes :

using System;

class Program{

   delegate void DelegateType();

   static void Main(){

      DelegateType delegateInstance = delegate() { Console.WriteLine("Hello"); };

      delegateInstance += delegate() { Console.WriteLine("Bonjour"); };

      delegateInstance();

      Console.ReadKey();

   }

}

Comme l’on peut s’en douter ce programme affiche :

Hello

Bonjour

Méthodes anonymes et arguments

Comme le montre l’exemple suivant, une méthode anonyme peut avoir des arguments. Tous les types sont applicables, y compris la possibilité de passer des arguments par référence avec le mot-clé ref et des arguments de sortie avec le mot-clé out :

using System;

class Program{

   delegate int DelegateType(int valTypeParam, string refTypeParam, ref int refParam, out int outParam);

   static DelegateType GetMethod(){

      return delegate(int valTypeParam, string refTypeParam,ref int refParam,out int outParam)

      {

         Console.WriteLine("Hello valParam:{0} refTypeParam:{1}", valTypeParam, refTypeParam);

         refParam++;

         outParam = 9;

         return valTypeParam;

      };

   }

   static void Main(){

      DelegateType delegateInstance = GetMethod();

      int refVar = 5;

      int outVar;

      int i = delegateInstance(1, "un", ref refVar, out outVar);

      int j = delegateInstance(2, "deux", ref refVar, out outVar);

      Console.WriteLine("i:{0} j:{1} refVar:{2} outVar:{3}", i, j, refVar, outVar);

      Console.ReadKey();

   }

}

Ce programme affiche :

Hello valParam:1 refTypeParam:un

Hello valParam:2 refTypeParam:deux

i:1 j:2 refVar:7 outVar:9

Notez cependant que le type de la valeur de retour n’est pas définie par la méthode anonyme, mais par le type du délégué auquel elle est assignée. Celui-ci est toujours défini car le compilateur oblige à assigner toutes méthodes anonymes à un délégué dont le type est connu à la compilation. On en conclut qu’on ne peut assigner une méthode anonyme à un délégué référencé par une référence de type System.Delegate.

Notez aussi que la syntaxe C# avec le mot clé param n’est pas utilisable dans la liste d’arguments d’une méthode anonyme :

using System;

class Program{

   delegate void DelegateType(params int[] arr);

   static DelegateType GetMethod(){

      return delegate(params int[] arr){ // erreur de compilation: param is not valid in this context

         Console.WriteLine("Hello");

      };

   }

//...

}

La raison pour laquelle le mot clé param n’est pas utilisable dans la liste d’arguments d’une méthode anonyme est simple : en interne le mot clé param oblige le compilateur a marquer la méthode concernée avec l’attribut ParamArrayAttribute. Or, les méthodes anonymes ne peuvent supporter d’attribut.

Une subtilité syntaxique

Lorsque le mot-clé delegate est utilisé sans parenthèse lors de la définition d’une méthode anonyme sans argument d’entrée ni de retour, celle-ci peut être assignée à n’importe quel type de délégué. Bien entendu, le corps de la méthode anonyme ne peut alors pas utiliser les paramètres d’entrée. En outre, une telle méthode anonyme ne peut rien retourner. En conséquence, cette syntaxe n’est pas utilisable si la signature du délégué cible retourne une valeur ou prend un paramètre out :

using System;

class Program{

   delegate void DelegateType(int valTypeParam, string refTypeParam, ref int refParam);

   static void Main(){

      DelegateType delegateInstance = delegate{Console.WriteLine("Hello");};

      int refVar = 5;

      delegateInstance(1, "un", ref refVar);

      delegateInstance(2, "deux", ref refVar);

      Console.ReadKey();

   }

}

Les méthodes anonymes et la généricité

Une méthode anonyme peut avoir des arguments de type générique, comme le montre l’exemple suivant :

using System;

class UneClasse<T>{

   delegate void DelegateType(T t);

   internal void UneMethode(T t){

      DelegateType delegateInstance = delegate(T arg){Console.WriteLine("Hello arg:{0}",arg.ToString());};

      delegateInstance(t);

   }

}

 

class Program{

   static void Main(){

      UneClasse<double> inst = new UneClasse<double>();

      inst.UneMethode(5.5);

      Console.ReadKey();

   }

}

En C#2, les délégations peuvent admettre des arguments génériques. Un délégué instance d’une telle délégation peut référencer une méthode anonyme. Il faut alors résoudre les types génériques lors de la définition de la méthode anonyme, comme le montre l’exemple suivant :

using System;

class Program{

   delegate void DelegateType<T>(T t);

   static void Main(){

      DelegateType<double> delegateInstance = delegate(double arg) { Console.WriteLine("Hello arg:{0}", arg.ToString()); };

      delegateInstance(5.5);

      Console.ReadKey();

   }

}

Exemples simples d’utilisation des méthodes anonymes

L’utilisation de méthodes anonymes est particulièrement adaptée à la définition de ‘petites’ méthodes destinées à être invoquées au moyen d’un délégué. Par exemple, vous pouvez définir au moyen d’une méthode anonyme la procédure sur laquelle démarrera un nouveau thread :

using System;

using System.Threading;

class Program{

   static void Main(){

      Thread thread = new Thread(delegate(){

         Console.WriteLine("ThreadHashCode:{0} Hello",Thread.CurrentThread.GetHashCode());

      });

      thread.Start();

      Console.WriteLine("ThreadHashCode:{0} Bonjour", Thread.CurrentThread.GetHashCode());

      Console.ReadKey();

   }

}

Ce programme affiche :

ThreadHashCode:1 Bonjour

ThreadHashCode:3 Hello

Vous pouvez aussi définir au moyen d’une méthode anonyme toute sorte de call-back, comme la réponse aux évènements des contrôles Windows Forms:

public class UneForm : Form{

   Button m_Button;

   public UneForm(){

      InitializeComponent();

      m_Button.Click += delegate(object sender, EventArgs args){

         MessageBox.Show("m_Button Clicked");

      };

   }

   void InitializeComponent()  

   {...}

}

Si l’utilisation des méthodes anonymes ne se cantonnait qu’à ces exemples simples, le sujet n’aurait certainement pas mérité un article sur DNG. Entrons maintenant dans les arcanes du compilateur C#2 pour comprendre toute la puissance de ce mécanisme.

Le compilateur C#2 et les méthodes anonymes

Le cas simple

Comme l’on peut s’en douter, lorsque le compilateur C#2 rencontre une méthode anonyme, il crée une méthode dans la classe de la méthode qui l’encapsule :

using System;

class Program{

   delegate void DelegateType();

   static void Main(){

      DelegateType delegateInstance = delegate() { Console.WriteLine("Hello"); };

      delegateInstance();

      Console.ReadKey();

   }

}

Ainsi le compilateur C#2 produit l’assemblage suivant à partir du code précédent (l’assemblage est visualisé avec l’outil Reflector de Lutz Roeder qui supporte déjà les assemblages de .NET2) :

On vérifie bien qu’une méthode privée, statique et nommée <Main>b__0() a été créée dans la classe encapsulante (i.e la classe Program) et contient le code de notre méthode anonyme. Une méthode anonyme est une méthode d’instance si elle est définie dans le corps d’une méthode d’instance.

On note aussi que cette méthode est référencée par le délégué <>9_CachedAnonymousMethodDelegate1, instance de DelegateType, qui est un champ statique de la classe encapsulante.

Il est intéressant de remarquer que le nom de cette méthode contient une paire de <>. De ce fait le compilateur ne vous autorise pas à l’invoquer directement à partir de votre code et l’intellisense ne l’affiche pas. En revanche le CLR est tout à fait capable d’utiliser une méthode avec un tel nom.

Une méthode anonyme accède à une variable locale de la méthode qui l’encapsule

Pour ne pas compliquer notre exposé, nous n’avons pas encore mentionné le fait qu’une méthode anonyme a la possibilité d’accéder à une variable locale de la méthode qui l’encapsule. Et c’est bien là que les choses commencent à être intéressantes. Analysons l’exemple suivant:

using System;

class