Métaprogrammation en C# : Quand le code écrit le code
Imaginez que vous êtes un architecte qui ne construit pas seulement une maison, mais un robot-constructeur qui érige lui-même les murs, pose le câblage et peint les façades. Dans le monde du C#, ce « robot » est la métaprogrammation — l'art d'écrire des programmes qui créent, analysent ou modifient d'autres programmes (ou eux-mêmes) pendant l'exécution.
Pourquoi en avez-vous besoin ? Imaginez que vous écriviez des dizaines de classes similaires pour travailler avec JSON, que vous créiez des systèmes de plugins complexes ou que vous soyez trop paresseux pour appeler manuellement des centaines de méthodes par réflexion. La métaprogrammation en C# permet d'automatiser les tâches répétitives, de créer des frameworks flexibles et d'accélérer considérablement le développement. Aujourd'hui, nous allons décortiquer les trois piliers de cette technique : la réflexion, les arbres d'expressions et la génération dynamique de code.
Cet article s'adresse à ceux qui écrivent déjà du C# avec aisance, mais qui souhaitent explorer les entrailles du CLR et apprendre la magie qui distingue un senior d'un développeur intermédiaire. C'est parti !
Réflexion (Reflection) : Un rayon X pour votre code
La réflexion est le fondement de la métaprogrammation en C#. Elle permet d'obtenir des informations sur les types, les méthodes, les propriétés et les champs pendant l'exécution, et de les appeler dynamiquement. Imaginez que vous regardez un objet comme un livre ouvert et que vous pouvez lire n'importe quelle page.
Obtention d'informations sur un type
La manière la plus simple est d'utiliser le mot-clé typeof ou la méthode GetType() :
Type stringType = typeof(string);Console.WriteLine($"Nom : {stringType.Name}"); // StringConsole.WriteLine($"Espace de noms : {stringType.Namespace}"); // SystemConsole.WriteLine($"Est-ce une classe ? {stringType.IsClass}"); // TrueAppel dynamique de méthodes
Supposons que vous ayez une classe Calculator et que vous souhaitiez appeler sa méthode Add, dont le nom n'est connu qu'à l'exécution (par exemple, à partir d'une configuration) :
public class Calculator{ public int Add(int a, int b) => a + b;}
// Utilisation de la réflexionCalculator calc = new Calculator();Type calcType = typeof(Calculator);MethodInfo? addMethod = calcType.GetMethod("Add");
if (addMethod != null){ int result = (int)addMethod.Invoke(calc, new object[] { 5, 3 }); Console.WriteLine($"Résultat : {result}"); // 8}Quand la réflexion est-elle utile ?
- Sérialisation / Désérialisation — des bibliothèques comme Newtonsoft.Json utilisent activement la réflexion.
- Plugins — chargement d'assemblages externes et appel de leurs méthodes.
- Tests — accès aux champs et méthodes privés (bien qu'il soit préférable d'éviter cela).
Important ! La réflexion est une opération lente. Si vous appelez la même méthode des milliers de fois, envisagez de mettre en cache le MethodInfo ou d'utiliser des délégués.
Arbres d'expressions (Expression Trees) : Le code comme données
Les arbres d'expressions sont un moyen de représenter le code sous forme de structure de données (un arbre). Vous pouvez analyser, modifier et même compiler ce code « à la volée ». C'est un outil puissant pour créer des ORM (Entity Framework), des validateurs et des requêtes dynamiques.
Création d'un arbre simple
Au lieu d'écrire une lambda x => x * 2, nous la construisons manuellement :
using System.Linq.Expressions;
// Création du paramètre xParameterExpression param = Expression.Parameter(typeof(int), "x");// Création de la constante 2ConstantExpression constant = Expression.Constant(2);// Création de la multiplication x * 2BinaryExpression body = Expression.Multiply(param, constant);// Assemblage de la lambda : x => x * 2Expression<Func<int, int>> lambda = Expression.Lambda<Func<int, int>>(body, param);
// Compilation et exécutionFunc<int, int> compiled