Plan du site  
pixel
pixel

Articles - Étudiants SUPINFO

C# : Introspection avec System.Reflection

Par Simon LOPEZ Publié le 03/10/2015 à 05:27:54 Noter cet article:
(0 votes)
Avis favorable du comité de lecture

L'introspection, ou encore la réflexion, est une technique utilisée en programmation pour inspecter et manipuler la structure et le comportement d'un programme pendant son exécution.

Le framework .NET possède cette capacité via l'API Reflection. Cette classe permet d'avoir une vue intérieure du programme et de ses types. Elle permet également de creer des types et d'y attacher des membres, que ce soit des propriétés ou des méthodes, de façon dynamique. Ainsi, le programme, à l'exécution, est capable de traiter les données d'un type sans pour autant avoir connaissance de celui-ci à la compilation.

Récupérer les attributs d'un objet

Supposons le programme suivant. PrintTypesData prend en paramètre un object de type Object (en .NET, tout les classes dérivent de System.Object), et est chargé d'écrire le nom et le type de chacun de ses attributs :

class Program
{
    static void Main()
    {
        var album = new Album();
        PrintProperties(album);
    }

    static void PrintTypeDatas(object obj)
    {
        //le corps sera décrit dans l'exemple suivant
    }
}

class Album
{
    public string Author { get; set; }
    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }

    public Album() 
    {
    }
 
    public Album(string author, string title, DateTime releaseDate)
    {
        this.Author = author;
        this.Title = title;
        this.ReleaseDate = releaseDate;
    }
}

Deux problèmes se présentent dans cette fonction :

  • Nous ne connaissons pas le type sous-jacent de l'objet reçu en paramètre : est-ce un entier ? Une chaîne de caractère ? Un objet de type Animal ?

  • La signature de la méthode nous indique que tout type d'objet peut être passé en paramètre, mais dans la portée de cette méthode, nous ne pouvons pouvons traiter l'objet en tant qu'objet d'un autre type que System.Object, à moins de le caster explicitement. Sans connaissance du type sous-jacent, un cast explicite n'est pas sans risque. A la compilation, aucune erreur ne sera levée, or le risque d'exception est toujours présent.

Si nous voulons en savoir plus sur l'objet en question, Object.GetType() peut nous fournir des informations sur le type sous-jacent de l'objet. Cette méthode retourne un objet de type System.Type, contenant toutes les méta-données de l'objet en question. Voici comment il est possible de récupérer les informations qui nous interessent concernant l'objet reçu en paramètre :

public void PrintTypeDatas(object obj)
{
    //Recupération de l'objet System.Type représentant le type sous-jacent de l'objet
    Type objectType = obj.GetType();
    
    //Récupération des informations concernant les propriétés de l'objet
    PropertyInfo[] properties = objectType.GetProperties();
            
    //Révèle le type sous-jacent
    Console.WriteLine(obj.GetType().Name);
            
    foreach(var property in properties)
    {
        string propertyData = String.Format("Property : {0} - Type : {1}", property.Name, property.PropertyType.Name);
        Console.WriteLine(propertyData);
    }
   
}

Voici ce qui est affiché en console une fois le programme mis en exécution :

Et si nous voulions récupérer des informations sur les méthodes de cet objet, comme par exemple le nom et le type de retour ? Nous savons d'ores et déjà que la classe dérive de System.Object, elle possède donc les méthodes .GetType(), .ToString(), etc...Il faut savoir aussi que les mots clés get et set définissent à eux seuls des méthodes pour acceder et modifier ces propriétés. Il serait intéréssent de découvrir par exemple comment .NET se débrouille pour nommer automatiquement l'accesseur et le mutateur d'une propriété. Modifions notre code et voyons le résultat :

public void PrintTypeDatas(object obj)
{
    //Recupération de l'objet System.Type représentant le type sous-jacent de l'objet
    Type objectType = obj.GetType();
    
    //Récupération des informations concernant les propriétés de l'objet
    PropertyInfo[] properties = objectType.GetProperties();
    
    //Récupération des informations concernant les méthodes de l'objet
    MethodInfo[] methods = objectType.GetMethods();
            
    //Révèle le type sous-jacent
    Console.WriteLine(obj.GetType().Name);
           
    foreach(var property in properties)
    {
        string propertyData = String.Format("Property : {0} - Type : {1}", property.Name, property.PropertyType.Name);
        Console.WriteLine(propertyData);
    }

    foreach (var method in methods)
    {
        string methodData = String.Format("Method : {0} - Return type : {1}", method.Name, method.ReturnType);
        Console.WriteLine(methodData);
    }
}

Nous avons pu afficher en console les informations concernant les méthodes dont dispose l'objet :

Un objet de type PropertyInfo ou MethodInfo est capable de nous fournir encore plus d'informations sur le membre d'une classe. Sur les modificateurs employés par exemple : cette méthode est-elle publique ? Est-elle privée ? Est elle statique ? Est-elle virtuelle ? Il est même possible de savoir si la méthode est visible par les types dérivant de la classe en question.

Création dynamique de type

Pour cet exemple, nous allons supposer maintenant que le programme prenne en entrée un fichier XML décrivant un produit. Ici nous voulons utiliser les informations du produit pour instancier un objet du type correspondant : soit un film, soit un album, soit une autre sorte de produit.

Prenons en considération le XML suivant :

<Product>
    <Album author="Autechre" title="Confield" release="2001/04/30"/>
</Product> 

Cette fois, nous allons supprimer l'implémentation de la classe Album et le recréer dynamiquement pendant l'éxécution.

Modifions un peu notre code :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;

namespace ConsoleApplication4
{
    class Program
    {
        static void Main(string[] args)
        {
            XMLStructure structure = new XMLStructure();

            using(XmlReader reader = XmlReader.Create("example.xml"))
            {
                reader.MoveToContent(); 

                while (reader.Read())
                {
                    if(!String.IsNullOrEmpty(reader.Name) && reader.IsStartElement())
                    {
                        structure.Nature = reader.Name;
                        structure.Author = reader.GetAttribute("author").ToString();
                        structure.Title = reader.GetAttribute("title").ToString();
                        structure.ReleaseDate = reader.GetAttribute("release").ToString();
                    }
                }
            }

            var product = InstantiateObject(structure);
            var productProperties = product.GetType().GetProperties();
            var productMethods = product.GetType().GetMethods();

            Console.WriteLine(product.GetType().Name);

            foreach (var property in productProperties)
            {
                string propertyData = String.Format("Property : {0} - Type : {1}", property.Name, property.PropertyType.Name);
                Console.WriteLine(propertyData);
            }

            foreach (var method in productMethods)
            {
                string methodData = String.Format("Property : {0} - Return Type : {1}", method.Name, method.ReturnType.Name);
                Console.WriteLine(methodData);

            }
            
        }

        static object InstantiateObject(XMLStructure structure)
        {
            //Le type sera placée dans l'assembly "Product"
            AssemblyName assemblyName = new AssemblyName("Product");
            AssemblyBuilder assemblyBuilder = Thread.GetDomain().DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
            ModuleBuilder module = assemblyBuilder.DefineDynamicModule("MainModule");

            //Nous créons le type dont le nom sera basé sur la nature du produit
            TypeBuilder typeBuilder = module.DefineType(structure.Nature, TypeAttributes.Public | TypeAttributes.Class);

            /*Notre objet sera similaire à la structure XML
             *la nature du produit est déjà représenté par le nom du type que nous venons de creer 
             */
            var structureProperties = structure.GetType().GetProperties().Where(x => x.Name != "Nature");

            // A partir de maintenant, nous allons creer 
            foreach (var propertyInfo in structureProperties)
            {
                /* Propriété privée. 
                 *Comme la convention C# l'indique, nous plaçons un underscore avant le nom de la propriété lorsque celle-ci est privée
                 */
                FieldBuilder field = typeBuilder.DefineField(
                    "_" + propertyInfo.Name, 
                    typeof(string), 
                    FieldAttributes.Private
                );
                
                // Propriété publique
                PropertyBuilder property = typeBuilder.DefineProperty(
                    propertyInfo.Name,
                    PropertyAttributes.None,
                    typeof(string),
                    new Type[] { typeof(string) }
                );

                // Accesseur et mutateur publiques pour la propriété
                MethodAttributes GetSetAttributes = MethodAttributes.Public;

                // Génération du getter
                MethodBuilder getMethod =
                    typeBuilder.DefineMethod(
                    "get_" + propertyInfo.Name,
                    GetSetAttributes,
                    typeof(string),
                    Type.EmptyTypes
                );

                //Génération du corps du getter
                ILGenerator getterILEmitter = getMethod.GetILGenerator();
                getterILEmitter.Emit(OpCodes.Ldarg_0);
                getterILEmitter.Emit(OpCodes.Ldfld, field);
                getterILEmitter.Emit(OpCodes.Ret);

                // Génération du setter
                MethodBuilder setMethod =
                    typeBuilder.DefineMethod(
                        "set_" + propertyInfo.Name,
                        GetSetAttributes,
                        null,
                        new Type[] { typeof(string) }
                );

                // Generation du corps du setter
                ILGenerator setterILEmitter = setMethod.GetILGenerator();
                setterILEmitter.Emit(OpCodes.Ldarg_0);
                setterILEmitter.Emit(OpCodes.Ldarg_1);
                setterILEmitter.Emit(OpCodes.Stfld, field);
                setterILEmitter.Emit(OpCodes.Ret);

                // Mappage du getter et du setter sur la propriété générée
                property.SetGetMethod(getMethod);
                property.SetSetMethod(setMethod);
            }

            // création du type
            Type newType = typeBuilder.CreateType();

            //instantiation
            object newProduct = Activator.CreateInstance(newType);

            //Maintenant nous allons attribuer les valeurs présents dans le XML a notre tout nouvel objet
            foreach (var property in structureProperties)
            {
                newProduct.GetType().GetProperty(property.Name).SetValue(newProduct, property.GetValue(structure));
            }

            return newProduct;
        }
    }

    class XMLStructure
    {
        public string Nature { get; set; }
        public string Author { get; set; }
        public string Title { get; set; }
        public string ReleaseDate { get; set; }
    }
}

Voici le résultat affiché en console, très similaire à celui de l'exemple précédent :

Dans cet exemple, la plus grosse fonctionnalité de Reflection à retenir est la classe ILGenerator. Celle-ci permet d'appeler à la volée des instructions en MSIL (Microsoft Intermediate Language) pendant l'exécution du programme. Voici une explication détaillé des opcodes utilisés dans l'exemple en reprenant le cas de notre getter.

Voici une représentation de notre getter en C# tel qu'on peut l'imaginer :

public propertyType get_propertyName()
{
    return this.property;
}

Et la représentation depuis notre exemple précédent où on utilisait le générateur de langage intermédiaire :

//Génération du corps du getter
ILGenerator ILEmitter = getMethod.GetILGenerator();
ILEmitter.Emit(OpCodes.Ldarg_0);
ILEmitter.Emit(OpCodes.Ldfld, field);
ILEmitter.Emit(OpCodes.Ret);

Ldarg est une instruction destinée à charger un argument sur la pile d'évaluation. Ldarg_0 concerne l'argument situé à l'index 0.

La raison pour laquelle Ldarg_0 est toujours appelée même dans le cas où une méthode ne prend aucun argument est simplement parce que chaque méthode accessible via l'instance d'une classe possède toujours implicitement comme premier paramètre l'objet représentant cette instance. C'est d'ailleurs aussi la raison pour laquelle plusieurs langages de haut niveau considèrent la représentation suivante comme étant la même chose :

//Ceci
objet.methode()
//est la même chose que
methode(objet)

Ldarg_0 représente donc this depuis la portée de notre méthode.

Ldfld pousse la valeur du champ spécifié sur la pile d'évaluation (dans l'exemple, field représentait la propriété privée que nous venions d'ajouter)

Ret effectue un saut vers l'adresse de retour. Autrement dit, le programme retourne à la méthode responsable de l'appel de la méthode dans laquelle nous sommes actuellement. Puisque nous venions de charger la valeur du champ sur la pile, c'est cette valeur qui est retournée. L'instruction ne différe pas dans une méthode qui est censée ne rien retourner : elle se contentera juste de sauter vers l'adresse sans avoir à retourner de valeur.

Conclusion

L'introspection possède de nombreux avantages et constitue une clé de la méta-programmation. Reflection fournit aux programmeurs des outils puissants pour générer du code ou alterer dynamiquement le comportement d'un programme, mais également pour obtenir la capacité de faire face à de nombreux cas ou le programme en question possède peu de connaissance sur les objets qu'il aura à traiter à la compilation.

Concernant la devination des types, il ne faut certainement pas en abuser et essayer d'avoir au mieux une bonne structure dans son code pour éviter de s'embarquer dans de multiple cas où les methodes auront à traiter des données de plusieurs types différents.

A propos de SUPINFO | Contacts & adresses | Enseigner à SUPINFO | Presse | Conditions d'utilisation & Copyright | Respect de la vie privée | Investir
Logo de la société Cisco, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société IBM, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sun-Oracle, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Apple, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sybase, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Novell, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Intel, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Accenture, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société SAP, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Prometric, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Toeic, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo du IT Academy Program par Microsoft, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management

SUPINFO International University
Ecole d'Informatique - IT School
École Supérieure d'Informatique de Paris, leader en France
La Grande Ecole de l'informatique, du numérique et du management
Fondée en 1965, reconnue par l'État. Titre Bac+5 certifié au niveau I.
SUPINFO International University is globally operated by EDUCINVEST Belgium - Avenue Louise, 534 - 1050 Brussels