.NET : Introduction à la réflexion (C#)Date de publication : 11.02.2005
Par
Olivier Brin (autres articles) 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. I. Avertissement II. Introduction A. Qu'est-ce que la réflexion ? B. Comment ca marche ? C. Quelles sont ses applications ? III. La réflexion avec C# A. Les types à utiliser B. Les méthodes à utiliser C. Chargement d'un assemblage D. Découverte de types E. Découverte de membres F. Instanciation dynamique de types G. Invocation de membres IV. Applications concrètes de la réflexion A. Reflector B. Complétion de code V. Masquer la réflexion VI. Ressources VI. Remerciements I. Avertissement
Cet article est une introduction et non pas une présentation complète des possibilités de la réflexion.
II. IntroductionA. 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.
B. Comment ca 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.
C. Quelles sont ses applications ?
Les applications de la réflexion sont nombreuses, et beaucoup d'entres 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 C#
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.
A. Les types à utiliser
B. Les méthodes à utiliser
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 :
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.
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.
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 :
La méthode GetTypes permet de lire les types de l'assemblage, et renvoie une 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 :
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 :
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 amener à 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
Donc, dans le cas d'un explorateur de classes avec une vue en arbre (TreeView) ou l'on veut colorer chaque
membre en fonction de sa nature, le code serait le suivant :
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.
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 liasion 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ééls é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.
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.
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.
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.
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é 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.
IV. Applications concrètes de la réflexionA. 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.
![]() 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és (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.
VI. 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.
|
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. Cette page est déposée.
Copyright © 2000-2012 - www.developpez.com