|
|
Les méthodes anonymes de C# v2 par Patrick Smacchia |
Introduction
aux méthodes anonymes de C#2
Méthodes
anonymes et arguments
Les
méthodes anonymes et la généricité
Exemples
simples d’utilisation des méthodes anonymes
Le
compilateur C#2 et les méthodes anonymes
Une
méthode anonyme accède à une variable locale de la méthode qui
l’encapsule
Variable
locale capturée et complexité du code.
Une
méthode anonyme accède à un argument de la méthode qui l’encapsule
Une
méthode anonyme accède à un membre de la classe qui définie la méthode qui
l’encapsule
Exemples
avancés d’utilisation des méthodes anonymes
Définitions:
fermeture (closure en anglais) et environnement lexical
Un
peu plus loin dans la compréhension des fermetures
· 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.
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 ?!).
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
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
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
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
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
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.
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
DelegateType delegateInstance = delegate{Console.WriteLine("Hello");};
int refVar = 5;
delegateInstance(1, "un",
ref refVar);
delegateInstance(2,
"deux", ref
refVar);
Console.ReadKey();
}
}
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
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
DelegateType<double> delegateInstance = delegate(double arg) { Console.WriteLine("Hello arg:{0}", arg.ToString()); };
delegateInstance(5.5);
Console.ReadKey();
}
}
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
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.
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
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.
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 Program{
delegate int
DelegateTypeCounter();
static DelegateTypeCounter
MakeCounter(){
int
counter = 0;
DelegateTypeCounter
delegateInstanceCounter = delegate { return
++counter; };
return delegateInstanceCounter;
}
static void
DelegateTypeCounter counter1 =
MakeCounter();
DelegateTypeCounter counter2 =
MakeCounter();
Console.WriteLine(counter1());
Console.WriteLine(counter1());
Console.WriteLine(counter2());
Console.WriteLine(counter2());
Console.ReadLine();
}
}
Ce programme affiche:
1
2
1
2
Cela peut laisser perplexe car:
· La variable counter locale à la méthode MakeCounter() semble ‘survivre’ à l’invocation de celle-ci. Cela contredit complètement la notion de variable locale telle que nous la connaissons.
·
Il semble qu’il existe deux
‘instances’ de la variable locale counter.
Comme nous l’avons mentionné, en .NET v2 il n’y a pas de nouvelles instructions IL pour la gestion des méthodes anonymes. Ce mystère doit donc forcément provenir du compilateur. Une analyse de l’assemblage produit par le compilateur s’impose donc :

Tous devient alors clair :
· Contrairement à la section précédente, le compilateur ne se contente pas de créer une nouvelle méthode. Il crée une nouvelle classe nommée ici <>c__DisplayClass1.
· Cette classe contient une méthode d’instance nommée <MakeCounter>b__0() qui a le corps de notre méthode anonyme.
· Cette classe contient aussi un champ d’instance nommé counter qui garde l’état de la variable locale du même nom. On dit que la variable counter est capturée par la méthode anonyme.
· La méthode MakeCounter() instancie la classe <>c__DisplayClass1 et initialise sont champ counter.
· Il est important de remarquer que la classe MakeCounter() n’a plus de variable locale counter. Elle utilise le champ counter de la nouvelle instance de <>c__DisplayClass1.
Avant d’aller plus loin et d’expliquer
pourquoi le compilateur a ce comportement pour le moins inattendu, continuons
minutieusement notre analyse.
L’exemple suivant, tiré du blog de Brad Abrams,
est plus subtil qu’il n’y parait.
using System;
using System.Threading;
class Program{
static void
for (int
i = 0; i < 5; i++)
ThreadPool.QueueUserWorkItem(delegate { Console.WriteLine(i); }, null);
Console.ReadLine();
}
}
Ce programme affiche d’une manière non
déterministe quelque chose comme ceci :
0
1
5
5
5
On comprend alors que toute les ‘instances de
notre méthode anonyme’ partagent la variable locale i. Le comportement non déterministe vient
du fait que la méthode Main() et nos ‘instances de notre méthode anonyme’
sont exécutées par des threads différents. Pour
s’en convaincre voici le code décompilé de la
méthode Main() :
private static void
bool flag1;
Program.<>c__DisplayClass1
class1 = new Program.<>c__DisplayClass1();
class1.i = 0;
goto Label_0030;
Label_000F:
ThreadPool.QueueUserWorkItem(new WaitCallback(class1.<
class1.i++;
Label_0030:
flag1 = class1.i < 5;
if (flag1){
goto Label_000F;
}
Console.ReadLine();
}
Notez enfin que la valeur 5 est obtenue lorsque la méthode anonyme est exécutée après que la méthode Main() a finit d’exécuter la boucle.
Pour obtenir un comportement déterministe, il
suffit de modifier ce programme comme ceci :
using System;
using System.Threading;
class Program{
static void
for (int i = 0; i < 5; i++){
int j = i;
ThreadPool.QueueUserWorkItem(delegate { Console.WriteLine(j); }, null);
}
Console.ReadLine();
}
}
Cette fois ci, le programme affiche bien :
0
1
2
3
4
Ce comportement vient du fait que la variable locale j est capturée à chaque itération comme le montre cette décompilation de la méthode Main():
private static void
Program.<>c__DisplayClass1 class1;
bool flag1;
int num1 = 0;
goto Label_0029;
Label_0004:
class1 = new Program.<>c__DisplayClass1();
class1.j = num1;
ThreadPool.QueueUserWorkItem(new WaitCallback(class1.<
num1++;
Label_0029:
flag1 = num1 < 5;
if (flag1){
goto Label_0004;
}
Console.ReadLine();
}
Vous voilà averti : en matière de capture de variable par une méthode anonyme, rien n’est évident. Cette fonctionnalité est donc à utiliser avec circonspection puisqu’elle peut complexifier la lisibilité de votre code.
Notez enfin qu’une variable locale capturée n’étant plus une variable locale, tout code unsafe qui accède à une telle variable doit au préalable l’avoir fixée avec le mot-clé fixed.
Les arguments d’une méthode peuvent être
considérés comme des variables locales. Ainsi, C#2 autorise une méthode anonyme
à utiliser des arguments d’une méthode qui l’encapsule. Tout se
passe comme ci l’argument était une variable locale. Par exemple:
using System;
class Program{
delegate void
DelegateTypeCounter();
static DelegateTypeCounter
MakeCounter(string
counterName){
int counter = 0;
DelegateTypeCounter
delegateInstanceCounter = delegate{
Console.WriteLine(counterName + (++counter).ToString());
};
return delegateInstanceCounter;
}
static void
DelegateTypeCounter counterA =
MakeCounter("Compteur A:");
DelegateTypeCounter counterB =
MakeCounter("Compteur B:");
counterA();
counterA();
counterB();
counterB();
Console.ReadLine();
}
}
Ce programme affiche :
Compteur
A:1
Compteur
A:2
Compteur
B:1
Compteur
B:2
Deux restrictions existent cependant à la capture d’un argument par une méthode anonyme. L’argument ne doit pas être out ou ref. Cette conséquence est logique puisqu’un tel argument ne peut plus être considéré comme une variable locale car il ‘survit’ à l’exécution de la méthode.
Une méthode anonyme peut accéder aux membres de la classe qui l’encapsule. Dans le cas de membres statiques il n’y a aucun problème de compréhension. Un champ statique existe en une seule version au sein d’un domaine d’application, et c’est cette version qui est accédée par la méthode anonyme.
Pour bien saisir le cas où une méthode anonyme
accède à un membre d’instance de la classe qui l’encapsule, il
suffit de considérer la référence this comme une variable locale à la méthode
d’instance qui définit la méthode anonyme (ce qui est d’ailleurs
bien le cas). Par exemple:
using System;
delegate void DelegateTypeCounter();
class CounterMaker{
string m_Name; // Un champ
d’instance
internal CounterMaker(string name) { m_Name = name; }
internal DelegateTypeCounter
MakeCounter(string counterName){
int counter = 0;
DelegateTypeCounter
delegateInstanceCounter = delegate{
Console.Write(counterName
+(++counter).ToString());
Console.WriteLine(" Compteur fabriqué par:" + m_Name); // on aurait pu écrire this.m_Name
};
return delegateInstanceCounter;
}
}
class Program{
static void
CounterMaker counterMaker1 = new CounterMaker("Fabrique1");
CounterMaker counterMaker2 = new CounterMaker("Fabrique2");
DelegateTypeCounter counterA =
counterMaker1.MakeCounter("Compteur A:");
DelegateTypeCounter counterB =
counterMaker1.MakeCounter("Compteur B:");
DelegateTypeCounter counterC =
counterMaker2.MakeCounter("Compteur C:");
counterA(); counterA();
counterB(); counterB();
counterC(); counterC();
Console.ReadLine();
}
}
Ce programme affiche :
Compteur
A:1 Compteur fabriqué par:Fabrique1
Compteur
A:2 Compteur fabriqué par:Fabrique1
Compteur
B:1 Compteur fabriqué par:Fabrique1
Compteur
B:2 Compteur fabriqué par:Fabrique1
Compteur
C:1 Compteur fabriqué par:Fabrique2
Compteur
C:2 Compteur fabriqué par:Fabrique2
Décompilons la méthode MakeCounter() pour bien montrer que la référence this est capturée :
internal DelegateTypeCounter
MakeCounter(string counterName){
CounterMaker.<>c__DisplayClass1
class1 = new CounterMaker.<>c__DisplayClass1();
class1.<>4__this = this;
class1.counterName = counterName;
class1.counter = 0;
return new
DelegateTypeCounter(class1.<MakeCounter>b__0);
}
Notez que la référence this ne peut pas être utilisée dans une méthode anonyme dont le type encapsulant est une structure (i. un type valeur). Voici l’erreur générée par le compilateur :
![]()
Selon la définition donnée par le Wiki de c2.com, une fermeture (closure en anglais) est une fonction qui capture les valeurs des variables de l’environnement lexical au moment où elle est créée. L’environnement lexical d’une fonction désigne l’ensemble des variables visibles à l’endroit où la fonction est déclarée.
Notez bien l’utilisation de ‘au moment’ et de ‘à l’endroit’. Cela nous indique que la fermeture est un concept dynamique (i.e qui n’existe qu’à l’exécution) alors que l’environnement lexical est un concept statique (i.e qui n’existe qu’avant la compilation).
Notez aussi que la fermeture ne concerne que les variables de l’environnement lexical qui sont effectivement utilisées par la fonction.
Notez enfin que la définition d’une fermeture parle de création de fonction. Dans les langages impératifs tels que C, C++, C#1, Java ou VB.NET1 cette notion de création de fonction est absente. Cette notion nous vient des langages fonctionnels tels que Haskell ou Lisp où tout est fonction et où l’état des variables locales des fonctions peut survivre à l’exécution d’une fonction.
Avec ce que l’on a vu concernant le travail du compilateur, la notion de méthode anonyme de C#2 est bien une implémentation du concept de fermeture (malgré certaine discussions académiques sur le sujet, en tant que développeur pragmatique et passionné, vous pouvez considérer cette assertion comme vraie). C#2 est donc plus qu’un langage impératif objet. Notez que ce n’est pas la première fois qu’un langage non-fonctionnel intègre la notion de fermeture puisque par exemple les langages Perl et Ruby supportent cette fonctionnalité.
Pour les aficionados du C++, ce concept de fermeture se rapproche aussi du concept de foncteur ou de fonction-objet du C++ si chère a M. Stroustrup (mais oui, rappelez vous, la possibilité de redéfinir l’opérateur parenthèse). Plus d’information à ce sujet dans mon cours sur ce langage (cour C++). Nous reviendrons sur cette notion de foncteur lorsque nous aborderons les itérateurs dans le prochain article.
Lorsqu’une fonction est invoquée, elle calcule son résultat en fonction des valeurs de ses arguments mais aussi en fonction du contexte dans lequel elle est invoquée. Le contexte peut être vue comme un ensemble de données d’arrière plan. Ainsi, passer des arguments à une fonction revient à mettre en avant certaines données clés essentielles à l’exécution de la fonction (i.e ses arguments).
Dans un langage objet, pour une méthode d’instance ce contexte est en général l’état de l’objet sur lequel elle est invoquée (état qui est accessible implicitement ou explicitement au travers de la référence this). Pour une fonction écrite en C, le contexte est défini par les valeurs des variables globales. Pour une méthode anonyme (i.e une fermeture), ce contexte est défini par les valeurs des variables de son environnement lexical au moment où elle est créée.
Ainsi, de même que la notion de classe, la notion de fermeture est un moyen d’associer un comportement à un état. Dans les langages objets, on associe un ou plusieurs comportement (i.e les méthodes d’instances) à un état (i.e les champs) en passant la référence this comme premier argument de la méthode. Ceci nous est caché par le compilateur mais c’est bien ce qui se passe.
Pour clarifier tout ceci :
· On peut voir un objet comme un ensemble de données auquel des fonctions sont attachées (par l’astuce du passage de la référence this en premier argument).
· On peut voir une fermeture comme une fonction auquel des données sont attachées (par l’astuce de la capture des valeurs des variables de l’environnement lexical).
Pour ceux qui se souviennent de leur cours de math, cela ne sera pas sans rappeler la notion d’espace dual…
D’après la section précédente, il semblerait que l’on puisse utiliser une méthode anonyme à la place de certaines classes simples à comportement unique. C’est bien ce que l’on faisait dans nos exemples précédents en implémentant un compteur où, le comportement est l’incrément du compteur et l’état la valeur du compteur. D’ailleurs nous avons vérifié que la compilation d’une méthode anonyme qui capture un état résulte par la création d’une classe par le compilateur.
L’exemple du compteur n’utilise pas la possibilité de passer un argument à une méthode anonyme. En ayant recours à cette possibilité, nous pouvons concevoir une fermeture pour créer un comportement paramétré qui agit sur les instances d’une classe. L’exemple simple est le multiplicateur paramétré d’entiers:
using System;
class Program{
delegate void DelegateMultiplicateur(ref
int entierAMultiplier);
static DelegateMultiplicateur
BuildMultiplicateur(int multiplicateurParam){
return delegate(ref int entierAMultiplier){
entierAMultiplier
*= multiplicateurParam;
};
}
static void Main(){
DelegateMultiplicateur
multiplicateurPar8 = BuildMultiplicateur(8);
DelegateMultiplicateur multiplicateurPar2 =
BuildMultiplicateur(2);
int entier = 3;
multiplicateurPar8(ref entier);
// ici entier vaut
24
multiplicateurPar2(ref entier);
// ici entier vaut
48
Console.ReadLine();
}
}
En ayant recours à la possibilité d’utiliser une valeur de retour pour la méthode anonyme, on peut créer des comportements paramétrés qui calculent un résultat en fonction d’un objet. Par exemple :
using System;
class Article{
public Article(decimal prix) { m_Prix = prix; }
private decimal m_Prix;
public decimal
Prix { get { return
m_Prix; } }
}
class Program{
delegate decimal
DelegateTvaComputer(Article
article);
static DelegateTvaComputer
BuildTvaComputer(decimal tva){
return delegate(Article
article){
return
(article.Prix * (100 + tva)) / 100;
};
}
static void
DelegateTvaComputer tvaComputer19_6 =
BuildTvaComputer(19.6m);
DelegateTvaComputer
tvaComputer5_5 = BuildTvaComputer(5.5m);
Article article = new
Article(97);
Console.WriteLine("Prix
TVA 19.6% : "+ tvaComputer19_6(article));
Console.WriteLine("Prix
TVA 5.5% : "+
tvaComputer5_5(article));
Console.ReadLine();
}
}
Comprenez bien que toute la puissance de l’utilisation de fermetures dans les deux exemples précédents vient du fait qu’elles nous évitent de créer des petites classes.
En y regardant de plus prés, on s’aperçoit que la notion de délégué utilisé sur une méthode d’instance en .NET1 est conceptuellement proche de la notion de fermeture. En effet, un tel délégué référence à la fois des données (l’état de l’objet cible) et un comportement. Une contrainte existe cependant : le comportement doit être une méthode d’instance de la classe définissant le type de la référence this.
Cette contrainte est affaiblie en .NET2. Grâce à
certaines surcharges de la méthode Delegate.CreateDelegate() vous pouvez
maintenant référencer le premier argument d’une méthode statique dans un
délégué. Par exemple:
using System;
class Program{
delegate void
DelegateType(int
writeNTime);
// Cette méthode est déclarée publique pour éviter des
problèmes de réflexions sur membres non public
public static void
WriteLineNTimes(string
s, int nTime) {
for(int i=0;i<nTime;i++)
Console.WriteLine(s);
}
static void
DelegateType deleg = (DelegateType)Delegate.CreateDelegate(
typeof(DelegateType),
"Bonjour",
typeof(Program).GetMethod("WriteLineNTimes"));
deleg(4);
Console.ReadLine();
}
}
Ce programme affiche :
Bonjour
Bonjour
Bonjour
Bonjour
Lorsque l’on utilise conjointement certaines
délégations génériques et certaines méthodes des types containers du
Framework 2.0, il devient aisé d’implémenter des
foncteurs avec des méthodes anonymes. Ceux qui regrettaient la STL du C++ ne
vont pas être déçus. Voici quelques exemples éloquents sur cette
caractéristique (tirés du
blog de Krzysztof
Cwalina et du
blog de Scott Allen, merci à
Jb Evain
pour l’info) :
using System.Collections.Generic;
class Program{
class Article{
public Article(decimal prix,string name){Prix = prix;Name = name;}
public readonly decimal Prix;
public readonly string Name;
}
static void Main(){
// recherche de tous les entiers pairs
// utilisation implicite d'un délégué de type public delegate bool System.Predicate<T>(T obj)
List<int> integers = new List<int>();for(int i=1; i<=10; i++) integers.Add(i);
List<int> even = integers.FindAll(delegate(int i){return i%2==0; });
// somme les éléments de la liste
// utilisation implicite d'un délégué de type public delegate void System.Action<T>(T obj)
int sum = 0;
integers.ForEach(delegate(int i) { sum += i; });
// trie d'une liste d’éléments d'un type complexe
// utilisation implicite d'un délégué de type public delegate int System.Comparison<T>(T x,T y)
List<Article> articles = new List<Article>();
articles.Add(new Article(5,"Tongues"));
articles.Add(new Article(3,"Ballon"));
articles.Sort(delegate(Article x, Article y){return Comparer<decimal>.Default.Compare(x.Prix,y.Prix); });
// cast des éléments d'une liste d’éléments d'un type complexe
// utilisation implicite d'un délégué de type delegate U Converter<T,U>(T from)
List<decimal> articlesPrix = articles.ConvertAll<decimal>(delegate(Article article) { return (decimal)article.Prix; });
System.Console.ReadKey();
}
}
Après avoir présenté les bases de la notion de méthode anonyme en C#2, nous nous sommes aperçu que cette fonctionnalité pouvait être utilisée pour remplacer certaines sortes de classes simples au moyen de fermetures. Garder à l’esprit que l’utilisation de méthodes anonymes peut cependant nuire à la lisibilité de votre code. C’est pour cela que par exemple, nous n’avons pas présenté la possibilité de définir des méthodes anonymes au sein de méthode anonyme.
Dans un prochain article, nous nous intéresserons à
la présentation des itérateurs de C#2. Là aussi, nous
nous efforcerons de souligner le travail du compilateur dans le but de pouvoir
utiliser cette fonctionnalité en toute connaissance de cause. Nous nous apercevrons alors que l’utilisation d’itérateurs, renforcée éventuellement avec
l’utilisation de méthodes anonymes peut aller bien au-delà de la notion
de fermeture.
Références :
The C#
programming language de Anders Hejlsberg, Scott Wiltamuth,
Peter Golde
Create Elegant
Code with Anonymous Methods, Iterators, and Partial
Classes de Juval Lowy
Implementation
of Closures (Anonymous Methods) in C# 2.0 (Part 6) blog
de Roshan James
Closures in CLR 2.0 blog de
Antonio Cisternino
Fun with
Anonymous Methods blog de Brad Abrams
What is closure c2 Wiki
Anonymous Methods c2 Wiki
Anonymous
Methods, Part 2 of ? GrantRi's WebLog [MS]
Charming Python: Functional
programming in Python, Part 2
de David Mertz
Le langage de
programmation C++ Patrick Smacchia
Auteur : Patrick
Smacchia
Copyright © Septembre 2004
|
Patrick Smacchia assure de nombreuses formations sur .NET, à la
fois dans l’industrie et dans le milieu universitaire (Université de Nice). Passionné par
l’architecture logicielle, il aide
L’ouvrage
Pratique de .NET et C# (O’Reilly 2003) Après avoir
construit plusieurs applications d’entreprises avec
C++/win32/COM/COM+/DCOM et Java/J2EE, il s’est tout naturellement
intéressé à .NET. De cette rencontre est né l’ouvrage Pratique de
.NET et C# (O’Reilly 2003) qui couvre la
plupart des aspects du développement sous .NET avec le langage C#
(architecture .NET sous jacente ; langage C# ; bibliothèques
ADO.NET, XML, WinForm, GDI+… ;
architectures distribuées avec COM+, .NET Remoting
et ASP.NET). Cet ouvrage contient de nombreux rappels pour le rendre
accessible aux étudiants et aux débutants. Les développeurs confirmés
pourront quant à eux rapidement exploiter les subtiles possibilités proposées
par .NET, que sont par exemple la réflexion, la programmation orientée aspect
ou le mécanisme d’attribut. |