Introduit avec la version 2 du Framework, les méthodes anonymes permettent de faire « pointer » un délégué sur un bout de code. Au travers de cet article, vous apprendrez comment utiliser les méthodes anonymes, mais nous irons plus loin en comprenant leur mécanisme interne afin d’élucider certains comportements étranges…

Prérequis

Afin de suivre au mieux l’article, je vous conseille de vous munir des applications suivantes :

  • Visual Studio 2005 avec C# (la version express gratuite est téléchargeable ici) afin de tester par vous même les exemples présentés ;
  • L’outil Reflector for .NET permettant de voir le code précompilé contenu dans un assembly.

Introduction

En .NET version 1, vous pouviez utiliser les délégués pour appeler une méthode, voire plusieurs méthodes les unes à la suite des autres (on comprend tout de suite le rapport qu’il y a avec les pointeurs de fonctions du C/C++). Avec .NET version 2, le concept des méthodes anonymes a été introduit afin de faire pointer des délégués non pas sur des méthodes, mais directement sur du code « inline ». Cela signifie qu’on va dire à un délégué qu’il doit pointer sur telle partie de code sans se soucier des signatures de méthodes.

Les événements du Framework étant gérés par les délégués, vous verrez aussi comment faire la même chose, mais avec des méthodes anonymes dans le but de simplifier l’écriture.

Enfin, nous irons dans le cœur des méthodes anonymes afin de mieux comprendre leur fonctionnement interne. Ne soyez pas pressés, vous en saurez plus bientôt.

Des délégués aux méthodes anonymes

Cas de présentation

Prenons un exemple tout simple : affichage de « Hello World » dans un MessageBox lors du clic sur un bouton.
Pour réaliser cette application avec un simple délégué, il nous faudra deux choses :

  1. Une méthode AfficheHelloWorld sans paramètre qui affiche « Hello World » dans un MessageBox ;
  2. Un délégué AfficheHelloWorld_Delegate, sans paramètre aussi, qui « pointera » sur la méthode précédemment citée.

Ce qui nous donne :

public partial class Form1 : Form
{
    //Déclaration du type délégué AfficheHelloWorld_Delegate
    delegate void AfficheHelloWorld_Delegate();
    
    AfficheHelloWorld_Delegate del_Hw = null;

    public Form1()
    {
        InitializeComponent();
        
        // Ajoute la méthode AfficheHelloWorld à l'instance du délégué AfficheHelloWorld_Delegate
        del_Hw = new AfficheHelloWorld_Delegate(AfficheHelloWorld);
    }

    // Méthode qui affiche Hello World dans un MessageBox
    void AfficheHelloWorld()
    {
        MessageBox.Show("Hello World !");
    }

    // Événement clic sur bouton
    // Affiche Hello World dans un MessageBox via un délégué
    private void button_TestDelegue_Click(object sender, EventArgs e)
    {
        // Exécute la méthode AfficheHelloWorld
        del_Hw();
    }    
}

Pour un cas aussi simple, on aurait aimé ne pas passer son temps à créer une méthode juste pour exécuter une ligne…

Avec les méthodes anonymes, on va pouvoir contourner ce « problème » de la manière suivante :

public partial class Form1 : Form
{
    //Déclaration du type délégué AfficheHelloWorld_Delegate
    delegate void AfficheHelloWorld_Delegate();
    
    AfficheHelloWorld_Delegate del_Hw = null;

    public Form1()
    {
        InitializeComponent();

        // Ajoute directement au délégué le code à exécuter
        del_Hw = delegate { MessageBox.Show("Hello World !"); };
    }

    private void button_TestMethodeAnonyme_Click(object sender, EventArgs e)
    {
        // Exécute le code précédemment créé
        del_Hw();
    }
}

Comme vous pouvez le constater, des accolades délimitent le code qui devra être exécuté par le délégué. Je me suis volontairement limité à une ligne de code, mais rien ne nous empêche d’en écrire bien plus.

Maintenant, la question que vous devez vous poser est comment cela est-ce possible… Certains s’en doutent déjà, mais passons en revue encore quelques exemples avant d’aborder ce sujet.

Méthode anonyme avec paramètre

Avec l’exemple suivant, vous pouvez vous apercevoir qu’une méthode anonyme peut accepter des paramètres de n’importe quel type tout comme elle peut renvoyer une valeur de n’importe quel type aussi :

class Program
{
    // Déclaration d'un type délégué avec paramètre et valeur de retour
    delegate string AffichePuissance2(int param_valeur);
    static void Main(string[] args)
    {
        AffichePuissance2 del_Puissance2 = delegate(int param_valeur)
        {
            int Resultat = param_valeur * param_valeur;
            return string.Format("{0} au carré donne {1}", param_valeur, Resultat);
        };

        Console.WriteLine(del_Puissance2(2));
        Console.WriteLine(del_Puissance2(4));
        Console.WriteLine(del_Puissance2(8));
    }
}

Avec comme résultat :

2 au carré donne 4
4 au carré donne 16
8 au carré donne 64

Grâce à l’Intellisence de Visual Studio, lorsqu’on veut exécuter le délégué, on est forcé d’entrer un paramètre de type int comme déclaré dans le type du délégué en question.

Méthode anonyme avec paramètre par référence

Un autre cas intéressant est le passage de paramètres par référence.

Modifions le code précédent afin d’obtenir :

class Program
{
    // Déclaration d'un type délégué avec paramètre et valeur de retour
    delegate string AffichePuissance2ParReference(ref int param_valeur);
    static void Main(string[] args)
    {
        AffichePuissance2ParReference del_Puissance2 = delegate(ref int param_valeur)
        {
            int copie_Valeur = param_valeur;
            param_valeur = param_valeur * param_valeur;

            return string.Format("{0} au carré donne {1}", copie_Valeur, param_valeur);
        };

        int valeur = 2;
        Console.WriteLine(del_Puissance2(ref valeur));
        Console.WriteLine(del_Puissance2(ref valeur));
        Console.WriteLine(del_Puissance2(ref valeur));

        Console.ReadLine();
    }
}

Avec comme résultat :

2 au carré donne 4
4 au carré donne 16
16 au carré donne 256

La valeur de la variable a donc bien été modifiée via la méthode anonyme après chaque appel.

À noter que les types des paramètres et de retour sont définis par le délégué auquel on assigne la méthode anonyme et non par la méthode anonyme elle même.

Attention par contre que l’utilisation du mot clé params est impossible avec les méthodes anonymes.

Méthode anonyme et la généricité

Un autre cas avec les méthodes anonymes est l’utilisation des génériques. Les délégués supportant des arguments génériques, ces mêmes délégués pourront référencer une méthode anonyme ; la définition des types génériques devra se faire dans la définition de la méthode anonyme :

class Program
{
    delegate void DelegueGenerique<T>(T param_generique);

    static void Main(string[] args)
    {
        DelegueGenerique<int> del_Generique_Int = delegate(int param_generique)
        {
            Console.WriteLine("Le type est {0} avec comme valeur '{1}'", param_generique.GetType().ToString(), param_generique.ToString());
        };

        DelegueGenerique<String> del_Generique_String = delegate(String param_generique)
        {
            Console.WriteLine("Le type est {0} avec comme valeur '{1}'", param_generique.GetType().ToString(), param_generique.ToString());
        };


        del_Generique_Int(5);
        del_Generique_String("Hello");

        Console.ReadLine();
    }
}

Avec comme résultat :

Le type est System.Int32 avec comme valeur '5'
Le type est System.String avec comme valeur 'Hello'

Les méthodes anonymes et les événements

Tous les événements du Framework passant par l’utilisation de délégués… Pourquoi ne pas utiliser les méthodes anonymes ?

Avec un simple délégué, on s’abonne comme ceci à l’événement Click du composant Button (qui a dit double cliquer sur le bouton en mode design ?!) :

public Form1()
{
    InitializeComponent();

    button_TestClick.Click += new EventHandler(button_TestClick_Click);
}

void button_TestClick_Click(object sender, EventArgs e)
{
    MessageBox.Show(string.Format("Le bouton qui a été cliqué est : {0}", ((Button)sender).Name));
}

Étant donné que nous avons vu comment passer des paramètres à une méthode anonyme, essayons la même chose dans ce cas-ci :

public Form1()
{
    InitializeComponent();

    button_TestClick.Click += delegate(object sender, EventArgs e)
        { MessageBox.Show(string.Format("Le bouton qui a été cliqué est : {0}", ((Button)sender).Name)); };
}

Avec comme résultat :

Ce qui est logique, car quand on y réfléchit, tout événement du Framework est géré par délégué.

Les mystères des méthodes anonymes révélés

Durant ce chapitre, je m’appuierai régulièrement sur l’outil Reflector (de Lutz Roeder) permettant de parcourir du code compilé dans le but de décrire le comportement des méthodes anonymes dans des cas assez particuliers.

Au cœur des méthodes anonymes

Commençons par un exemple tout simple déjà utilisé précédemment :

public partial class Form1 : Form
{
    //Déclaration du type délégué AfficheHelloWorld_Delegate
    delegate void Del();

    public Form1()
    {
        InitializeComponent();

        // Ajoute directement au délégué le code à exécuter
        Del del_Hello = delegate { MessageBox.Show("Hello World !"); };

        // Exécute le code précédemment créé
        del_Hello();
    }
}

Compilons le code et analysons l’exécutable à l’aide de Reflector :

Comme vous pouvez le constater, une méthode assez étrange a été créée (<.ctor>b__0). En analysant le code qui la compose, on s’aperçoit tout de suite qu’il s’agit de notre méthode anonyme.
Si donc pour nous développeur, une méthode anonyme ne représente qu’un bout de code, pour le CLR (Common Language Runtime) il s’agit d’une méthode nommée qui a été crée lors de de la compilation.

Pour les plus intrigués d’entre vous qui se demandent ce que vient faire <> dans un nom de méthode, cela permet tout simplement de dire que la méthode en question ne peut être directement appelée via le code.

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

Dans l’exemple suivant, vous allez voir comment une variable locale va pouvoir être utilisée dans des méthodes anonymes :

class Program
{
    delegate void AfficheVariableLocale();

    static AfficheVariableLocale monDelegue1;
    static AfficheVariableLocale monDelegue2;

    static void Main(string[] args)
    {
        // Variable locale
        int maVariable = 5;

        monDelegue1 = delegate
        {
            maVariable = maVariable + 1;
            Console.WriteLine("Méthode anonyme1 : {0}", maVariable);
        };
        monDelegue2 = delegate
        {
            maVariable = maVariable + 1;
            Console.WriteLine("Méthode anonyme2 : {0}", maVariable);
        };

        monDelegue1();
        monDelegue2();
        Console.WriteLine("Méthode locale   : {0}", maVariable);
        monDelegue1();
        monDelegue2();
        Console.WriteLine("Méthode locale   : {0}", maVariable);
        monDelegue1();
        monDelegue2();
        Console.WriteLine("Méthode locale   : {0}", maVariable);

        Console.ReadLine();
    }
}

Avec comme résultat :

Méthode anonyme1 : 6
Méthode anonyme2 : 7
Méthode locale   : 7
Méthode anonyme1 : 8
Méthode anonyme2 : 9
Méthode locale   : 9
Méthode anonyme1 : 10
Méthode anonyme2 : 11
Méthode locale   : 11

On s’aperçoit tout de suite que la variable a été partagée entre :

  • la méthode locale ;
  • la méthode anonyme monDelegue1 ;
  • la méthode anonyme monDelegue2.

Pour comprendre ce phénomène étrange, jetons un œil sur le code compilé avec Reflector :

On peut constater plusieurs choses dans le code compilé :

  • nos deux méthodes anonymes se retrouvent encapsulées dans une classe appelée <>c__DisplayClass2 ;
  • la classe <>c__DisplayClass2 a comme champs notre variable locale maVariable ;
  • le code de notre classe Main a été modifié afin d’instancier un objet de la classe <>c__DisplayClass2 à chaque appel ;
  • la valeur de notre variable locale a directement été copiée dans le champ maVariable de la classe <>c__DisplayClass2.

Finalement, maVariable a été remplacé par :

  • this.maVariable dans la classe <>c__DisplayClass2 ;
  • <>8__locals3.maVariable lorsque c’est la méthode Main elle-même qui fait appel à sa variable locale.

Ce qui explique ce partage de variable locale.

Capture d’une variable locale par une méthode anonyme

Cette fois-ci, nous allons créer une méthode qui renverra une instance d’une méthode anonyme utilisant une variable locale afin d’en observer le comportement :

class Program
{
    delegate void AfficheVariableLocale();

    static AfficheVariableLocale monDelegue1;
    static AfficheVariableLocale monDelegue2;

    static AfficheVariableLocale CreationDelegue()
    {
        // Variable locale
        int maVariable = 5;

        return delegate
        {
            maVariable = maVariable + 1;
            Console.WriteLine("Méthode anonyme : {0}", maVariable);
        };
    }

    static void Main(string[] args)
    {
        monDelegue1 = CreationDelegue();
        monDelegue2 = CreationDelegue();

        monDelegue1();
        monDelegue1();
        monDelegue1();
        monDelegue2();
        monDelegue2();
        monDelegue2();

        Console.ReadLine();
    }
}

Avec comme résultat :

Méthode anonyme : 6
Méthode anonyme : 7
Méthode anonyme : 8
Méthode anonyme : 6
Méthode anonyme : 7
Méthode anonyme : 8

Il semblerait que chaque méthode anonyme a sa propre instance de la variable locale ; on dit que la variable a été capturée par la méthode anonyme.

Vérifions le code compilé avec une fois de plus Reflector :

On peut constater plusieurs choses dans le code compilé :

  • notre méthode anonyme se retrouve encapsulée dans une classe appelée <>c__DisplayClass1 ;
  • la classe <>c__DisplayClass1 a comme champs notre variable locale maVariable ;
  • le code de notre classe AfficheVariableLocale a été modifié afin d’instancier un objet de la classe <>c__DisplayClass1 à chaque appel.

Chaque méthode ayant sa propre instance de <>c__DisplayClass1, on comprend tout de suite que la variable locale n’est plus partagée.

De la même façon qu’il a été possible d’accéder à une variable locale à la méthode qui encapsule la méthode anonyme, il est possible aussi d’accéder :

  • à un paramètre de la méthode qui encapsule la méthode anonyme ;
  • ou encore à un champ de la classe qui définit la méthode qui encapsule la méthode anonyme.

Tout n’est pas possible avec les méthodes anonymes

En effet, il y a bien quelque chose qui est impossible de faire avec les méthodes anonymes : ne plus référencer un bout de code.

Autant avec les méthodes nommées, on peut utiliser les opérateurs += et -= pour, respectivement, pointer et ne plus pointer sur une méthode donnée, autant avec les méthodes anonymes, c’est impossible à faire. Et la réponse est dans le titre, quand vous donnez la référence d’un bout de code à un délégué, c’est de manière anonyme avec donc aucune possibilité d’identifier ce bout de code par après.

delegate void Del();

static void f()
{
    Console.WriteLine("Code d'une fonction");
}
static void Main(string[] args)
{
    // Ajoute la référence de f
    Del monDelegue = new Del(f);
    // Ajoute la référence d'une méthode anonyme
    monDelegue += delegate { Console.WriteLine("Code d'une méthode anonyme"); };

    // Exécution de f et de la méthode anonyme
    monDelegue();

    // Retire la référence f
    monDelegue -= f;

    // Exécution de la méthode anonyme
    monDelegue();

    Console.ReadLine();
}

La seule alternative est de retirer toutes les références que le délégué contient en le mettant tout simplement à null :

monDelegue = null;

Conclusion

Au travers de cet article, nous avons vu ce qu’était une méthode anonyme. Après quelques explications, vous avez appris à appliquer leur utilisation en reproduisant ce qui est déjà possible avec les méthodes nommées ; simples appels de méthodes, utilisation de paramètres et valeur de retour, les événements…
Après cette partie pratique, la technique a fait place afin de comprendre le mécanisme des méthodes anonymes au travers d’exemples autrefois troublants.

Remerciements

Un grand merci à l’équipe .NET de Developpez.com et plus particulièrement à fearyourself pour les corrections.

Sources