.NET : Introduction à la réflexion (VB.NET)

Olivier Brin

La réflexion permet de découvrir et d'utiliser dynamiquement des types, comme l'instanciation tardive d'objets ou l'invocation de méthodes à la volée. ♪

- Titre : .NET : Introduction à la réflexion (VB.NET)

- Auteur : Olivier Brin

- Parution : 11 février 2005

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2005 Olivier Brin. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Avertissement

- II. Introduction

- II-A. Qu'est-ce que la réflexion ?

- II-B. Comment ça marche ?

- II-C. Quelles sont ses applications ?

- III. La réflexion avec VB.NET

- III-A. Les types à utiliser

- III-B. Les méthodes à utiliser

- III-C. Chargement d'un assemblage

- III-D. Découverte de types

- III-E. Découverte de membres

- III-F. Instanciation dynamique de types

- III-G. Invocation de membres

- IV. Applications concrètes de la réflexion

- IV-A. Reflector

- IV-B. Complétion de code

- V. Masquer la réflexion

- VI. Ressources

- VII. Remerciements

I - Avertissement

Cet article est une introduction et non pas une présentation complète des possibilités de la réflexion.

II - Introduction

II-A - Qu'est-ce que la réflexion ?

La réflexion est l'art de découvrir des types et d'invoquer leurs membres à l'exécution. La réflexion permet d'inspecter dynamiquement le contenu d'assemblages, d'en lire ses types, de créer des instances de ces types durant l'exécution du programme et d'appeler leurs méthodes ou champs dynamiquement. La réflexion est également appelée introspection suivant les auteurs, mais, avec l'arrivée de .NET, le terme réflexion s'est généralisé. On utilise également le terme de liaison tardive (late binding) pour décrire une instanciation à la volée au moment de l'exécution. Il existe d'autres mécanismes s'approchant de la réflexion utilisés avant .NET, comme RTTI (Run-Time Type Identification) en C/C++ ou, dans une certaine mesure, l'interface IDispatch pour les composants COM.

Certaines personnes, dont moi, pensent que la traduction du terme anglais reflection en réflexion est inappropriée. En effet, le terme introspection est nettement plus adapté et correspond mieux à la réalité du mécanisme. Cela dit, la majorité des livres et documents en français, ainsi que le MSDN, utilisent le terme de réflexion. C'est pour cette raison que j'utiliserai également ce terme dans la suite de cet article.

CLR (Common Langage Runtime) : environnement d'exécution des programmes .NET.

II-B - Comment ça marche ?

Tous les assemblages managés (programmés pour le CLR .NET) comportent des métadonnées qui décrivent le contenu de l'assemblage. En lisant ces métadonnées, un programme peut inspecter les types contenus dans l'assemblage, déterminer ses membres et les invoquer.

II-C - Quelles sont ses applications ?

Les applications de la réflexion sont nombreuses, et beaucoup d'entre nous s'en servent tous les jours depuis longtemps sans s'en rendre compte. L'exemple le plus flagrant reste les environnements de développement évolués et leurs multiples fenêtres et outils : un explorateur de classes utilise la réflexion. Les membres des classes et les classes elles-mêmes sont découvertes et ajoutées au fur et à mesure des actions du développeur. Un autre exemple : la complétion de code. N'est-ce pas magnifique que, lorsque l'on tape un point après un identificateur d'objet en C# dans un environnement de développement comme Visual Studio, la liste de ses membres s'affiche? Dans ce cas aussi, le mécanisme repose sur la réflexion pour découvrir à la volée tous les membres de l'objet concerné.

III - La réflexion avec VB.NET

Un espace de noms entier est réservé aux types et méthodes qui permettent la réflexion dans le framework .NET : il s'agit de System.Reflection. Seul le type System.Type est utilisé par le mécanisme de réflexion en étant hors de l'espace de noms System.Reflection.

La réflexion fonctionne avec tous les langages managés (C#, VB.NET, Managed C++ …).

III-A - Les types à utiliser

Type

Description

System.Reflection.Assembly

Définit un assemblage managé, comme un exécutable ou une DLL .NET. C'est à partir de ce type que seront appelées les principales méthodes de la réflexion. C'est directement depuis un objet de type Assembly que pourront être découverts et invoqués les membres.

System.Reflection.MemberInfo

Représente les informations d'un membre d'un type quelconque. Le membre peut être un champ ou une méthode, un objet de type MemberInfo définit sa signature.

III-B - Les méthodes à utiliser

Méthode

Description

Assembly.LoadFrom

Charge dynamiquement un assemblage dont le nom de fichier est passé en paramètre. Il s'agit d'un chargement physique, au contraire de la méthode Assembly.Load qui effectue un chargement logique avec le nom fort de l'assemblage.

Assembly.GetTypes

Renvoie un tableau de System.Type contenant tous les types définis dans l'assemblage.

Assembly.GetMembers

Renvoie un tableau de System.Reflection.MemberInfo contenant les signatures de tous les membres de chaque type de l'assemblage.

Activator.CreateInstance

Crée dynamiquement une instance du type spécifié. Principe de la liaison tardive (late binding).

Type.InvokeMember

Tente d'invoquer le membre spécifié sur le type. Il est possible de travailler avec des membres statiques ou sur une instance du type créée précédemment.

III-C - Chargement d'un assemblage

Il y a deux manières de charger dynamiquement un assemblage : logique ou physique. Le chargement logique s'appuie sur une des caractéristiques des assemblages .NET, les noms forts. En travaillant avec le Global Assembly Cache (GAC) de .NET, le CLR est capable de charger un assemblage en connaissant son nom, sa version ou encore sa culture. Voici un exemple :

Un nom fort .NET est un nom d'assemblage composé. Il contient plusieurs attributs qui permettent de différencier les assemblages les uns des autres, non pas par leur nom de fichier, mais par des propriétés bien plus logiques, par exemple leur langage (culture) ou leur version. Il est ainsi possible d'éviter l'un des nombreux désavantages des DLL natives qui étaient les multiples versions possibles d'une même DLL qui entraient en conflit. Cette notion du nom fort abolit une partie du tristement célèbre DLL Hell.

Chargement logique d'un assemblage

' On suppose que la directive Imports System.Reflection est présente plus haut

' Chargement en spécifiant la culture
Dim monAssembly As Assembly =  Assembly.Load("assemblyName, Culture=fr")

' Chargement en spécifiant la version
Dim monAssembly As Assembly =  Assembly.Load("assemblyName, version=1.2.3.4")

Voyons maintenant la manière de charger physiquement un assemblage. Celui-ci nécessite de connaître le nom du fichier de l'assemblage que l'on veut charger et sa localisation sur le système, ce qui le rend un peu plus dépendant que le chargement logique, mais plus facile à appréhender.

Chargement physique d'un assemblage

' On suppose que la directive Imports System.Reflection est présente plus haut

Dim monAssembly As Assembly =  Assembly.LoadFrom("C:\assemblyName.dll")

Dans les deux cas, monAssembly pourra être utilisé de manière similaire dans la suite du programme.

L'assemblage est maintenant chargé. Nous pouvons désormais inspecter ce qu'il contient. Avant cela, je vais faire un rapide rappel concernant les termes utilisés ci-dessous.

- Un type caractérise le contenu et le sens d'un objet. En C, int et char sont des types de données. En .NET, on différencie les types de valeurs et les types de références. Les types de valeurs sont alloués sur une pile (stack) alors que les types de références sont alloués sur le tas managé (managed heap). Les premiers sont accessibles plus rapidement et plus ou moins directement. Int32, Char sont des types de valeurs, alors que ArrayList ou HttpChannel sont des types références. On peut facilement connaître le type de chaque objet .NET en utilisant la méthode GetType.

- Un membre est une méthode, propriété ou champ appartenant à une classe. En .NET, toutes les classes héritent de la classe de base Object, ce qui fait qu'elles ont toutes au moins quatre membres : ToString, Equals, GetHashCode et GetType. Chaque classe contient encore un membre spécial, le constructeur .ctor.

- Invoquer un membre signifie appeler une méthode membre, lire ou écrire un champ.

III-D - Découverte de types

En règle générale, un assemblage contient un ou plusieurs types définis (une ou plusieurs classes par exemple). Ces types appartiennent à l'espace de noms défini dans l'assemblage, et il serait utile de lister tous les types que l'assemblage contient pour, par la suite, instancier l'un d'eux. L'exemple de code suivant permet d'inspecter chaque type d'un assemblage :

Découverte de types

' On suppose que la directive Imports System.Reflection est présente plus haut

Dim monAssembly As Assembly =  Assembly.LoadFrom("C:\assemblyName.dll")

Dim MonType As Type
For Each MonType In monAssembly.GetTypes()
Console.WriteLine("Type présent dans l'assemblage : " + MonType.Name)
Next

La méthode GetTypes permet de lire les types de l'assemblage, et renvoie un tableau d'instances de System.Type de tous les types contenus dans l'assemblage. Si la DLL .NET assemblyName.dll contient deux classes MyClassOne et MyClassTwo, GetTypes retournera ces deux types et la sortie du programme sera la suivante :

Type présent dans l'assemblage : MyClasseOne
Type présent dans l'assemblage : MyClasseTwo

A la place de la propriété Name qui rend le nom simple du type, on peut utiliser la méthode ToString qui renvoie elle le nom complet du type avec les espaces de noms parents (ex. : MyNameSpace.MyClassOne).

III-E - Découverte de membres

Chaque type comporte des membres. Ces membres peuvent être des méthodes, des champs ou des propriétés par exemple. La réflexion permet de découvrir chaque membre d'un type donné. Soit la séquence de code suivante :

Découverte de membres

' On suppose que la directive Imports System.Reflection est présente plus haut

Dim monAssembly As Assembly =  Assembly.LoadFrom("C:\assemblyName.dll")

Dim MonType As Type
For Each MonType In monAssembly.GetTypes()
Dim member As MemberInfo
For Each member In MonType.GetMembers()
Console.WriteLine("Type : " + MonType.Name + " \tMembre :" + member.Name)
Next
Next

Le code ci-dessus parcourt tous les membres de chaque type présent dans l'assemblage assemblyName.dll. On affiche dans la console tous les membres découverts.

Dans le cas de la réalisation d'un simple explorateur de classes, on pourrait être amené à colorer les membres selon leur nature : méthode, champ, constructeur… Pour déterminer la nature des membres, on utilise le champ MemberType de la classe MemberInfo, qui est une énumération. Voici les principales natures que vous rencontrerez.

Principales natures des membres

- MemberType.Method : ce membre est une méthode.

- MemberType.Field : ce membre est un champ.

- MemberType.Property : ce membre est une propriété.

- MemberType.Constructor : ce membre est un constructeur du type.

Donc, dans le cas d'un explorateur de classes avec une vue en arbre (TreeView) où l'on veut colorer chaque membre en fonction de sa nature, le code serait le suivant :

Détermination de la nature des membres

' nodeTemp est un noeud de l'arbre créé plus haut (racine)

Dim member As MemberInfo
For Each member In type.GetMembers()
Dim nodeMember As TreeNode =  nodeTemp.Nodes.Add(member.Name)

Select Case member.MemberType
Case MemberTypes.Method
' On colorie les méthodes en bleu ...
nodeMember.ForeColor = System.Drawing.Color.Blue
Exit For

Case MemberTypes.Field
' On colorie les champs en rouge ...
nodeMember.ForeColor = System.Drawing.Color.Red
Exit For
End Select
Next

Un champ intéressant de la classe MemberInfo se nomme DeclaringType. Il contient le nom du type ou le membre est déclaré. En d'autres termes, cela permet de savoir si le membre est un membre natif du type actuel, ou s'il est hérité d'une quelconque superclasse. Par exemple, la valeur de DeclaringType sur le membre GetType de n'importe quel objet .NET sera toujours System.Object, car c'est dans cette classe que GetType est déclarée.

III-F - Instanciation dynamique de types

Dans la plupart des cas, la découverte dynamique de types est suivie d'une instanciation de l'un de ces types. On parle alors de late binding, ou liaison tardive pour décrire la création d'un objet à l'exécution. L'exemple ci-dessous permet d'instancier des types et de les utiliser comme des objets réels également lors du développement, car leur interface sera commune et connue.

Considérons le problème suivant : nous voulons créer plusieurs implémentations de classes qui réalisent une même interface, et choisir à l'exécution quelle implémentation utiliser. Par exemple, on considère une interface IAutomobile, et deux classes AudiRS4 et BmwM3. On décide de faire deux assemblages AudiRS4 et BmwM3 qui contiennent l'implémentation des méthodes décrites dans l'interface IAutomobile, elle-même dans IAutomobile.dll.

Interface IAutomobile

Imports System

Namespace GenericAutomobile
Public Interface IAutomobile
Accelerer(Double dIncrement)
Tourner(Double dAngle)
End Interface
End Namespace

Le programme principal est chargé de découvrir et d'instancier un objet de type AudiRS4 ou BmwM3. Pour ce faire, il lui faut impérativement une référence sur l'assemblage IAutomobile.dll. Ensuite, il suffira de renseigner l'application sur le nom du type que l'on veut créer, de parcourir les assemblages et de voir si le type demandé est bel et bien présent.

Instanciation d'un type

' Objet réalisant notre interface
Dim Automobile As IAutomobile

' Nom de l'objet à créer
Dim m_sClassName As String =  "AudiRS4"

' Chargement de l'assemblage
Dim assembly As Assembly =  Assembly.LoadFrom(m_sClassName + ".dll")

' Introspection des types
Dim type As Type
For Each type In assembly.GetTypes()
If m_sClassName = type.Name Then
' Un type correspond, création d'un objet
Automobile = CType(Activator.CreateInstance(type), GenericAutomobile.IAutomobile)
End If
Next

Notre objet Automobile est maintenant créé. Dans l'exemple et pour que cela soit utilisable dynamiquement, il faudrait que le nom du type que l'on souhaite utiliser provienne d'un champ texte ou d'ailleurs, mais qu'il ne soit pas codé en dur.

III-G - Invocation de membres

Le code suivant permet d'appeler dynamiquement une méthode. Si la méthode n'existe pas, on affiche un message d'erreur dans la console. Si la méthode est présente, on affiche son résultat.

Invocation d'une méthode

' Nom de la méthode à invoquer
m_sQuery = "ToString"

Try
Object result as object = Automobile.GetType().InvokeMember(m_sQuery,
BindingFlags.Default |
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.InvokeMethod, Nothing, Automobile, Nothing)

Console.WriteLine("Résultat : " + result.ToString())
Catch mme As System.MissingMethodException
' Dans le cas où la méthode n'est pas un membre de ce type ...
Console.WriteLine("Méthode <" + m_sQuery + "> introuvable.")
End Try

On utilise la méthode InvokeMember pour invoquer un membre d'un type. Celle-ci prend plusieurs paramètres dont le nom de la méthode, les informations de liaison (binding flags) et éventuellement une instance d'objet sur lequel peut être effectuée l'invocation.

Les informations de liaison permettent de différencier les invocations suivant la nature du membre : méthode, champ (lecture/écriture), propriété… Il existe de nombreuses combinaisons qui permettent de restreindre ou de garantir l'accès à un membre. Par exemple, BindingFlags.Public spécifie que tous les membres publics doivent être inclus dans la recherche. BindingsFlags.Instance permet de rechercher dans les membres d'instance.

Plus de détails sur ces informations de liaison sont disponibles sur cette page du MSDN.

IV - Applications concrètes de la réflexion

IV-A - Reflector

Reflector est un puissant explorateur d'assemblages. Il permet de parcourir les modules, les types et leurs membres, et permet même de décompiler le code IL (intermediate language) en code C# ou VB.NET.

IV-B - Complétion de code

La complétion de code est un mécanisme très utilisé par les environnements de développement comme Visual Studio ou celui de Borland Delphi. C'est grâce à la réflexion que cela est rendu possible en .NET. L'image ci-dessous montre le type System.Double et tous ses membres découverts par Visual Studio .NET :

V - Masquer la réflexion

Dans certains cas, la réflexion peut devenir un trou de sécurité important. Comme le prouve le logiciel Reflector, il est possible de décompiler n'importe quel assemblage managé. Il existe quelques moyens pour contrer la réflexion.

Le premier consiste à utiliser des obfuscateurs de code (ou brouilleurs). Ceux-ci placent des lignes leurres dans le code IL (intermediate language, équivalent du byte-code Java) et le réordonne de sorte que les décompilateurs ne puissent plus s'y retrouver. La suite de XenoCode (voir liens en fin d'article) permet ce genre de protection.

Un autre moyen, plus simple, mais moins propre, est de passer par des librairies non managées (par ex. : écrites en C++ pur) pour les parties de code que l'on veut protéger et d'importer par la suite ces fonctions dans du code managé en utilisant le mécanisme P/Invoke (Platform Invoke). Vous trouverez plus de détails sur ce mécanisme dans l'article de Morpheus à ce sujet.

VI - Ressources

MSDN : Espace de noms System.Reflection.
MSDN : Espace de noms System.Reflection.
Site du logiciel Reflector, un puissant explorateur d'assemblages (gratuit).
Site de l'éditeur XenoCode.

VII - Remerciements

Je remercie en particulier Bestiol pour la correction, ainsi que Laurent Dardenne pour ses précieux conseils et remarques et Piotrek pour la correction du code VB.NET.