L'objectif d'une composition claire des composants est-il vaincu en partageant trop d'informations de type entre les bibliothèques ? Peut-être avez-vous besoin d'un stockage de données efficace fortement typé, mais cela coûterait très cher si vous deviez mettre à jour votre schéma de base de données à chaque fois que le modèle objet évolue, et vous aussi. plutôt déduire son schéma de type au moment de l'exécution ? Avez-vous besoin de fournir des composants qui acceptent des objets utilisateur arbitraires et de les gérer de manière intelligente ? Voulez-vous que le compilateur de la bibliothèque soit capable de vous dire par programme quels sont leurs types
si vous avez des difficultés
?Pour conserver des structures de données fortement typées tout en maximisant la flexibilité d'exécution, vous souhaiterez probablement réfléchir à la réflexion et à la manière dont elle peut améliorer votre logiciel. Dans cette colonne, j'explorerai l'espace de noms System.Reflection dans Microsoft .NET Framework et comment il peut bénéficier à votre expérience de développement. Je commencerai par quelques exemples simples et terminerai par la façon de gérer les situations de sérialisation réelles. En cours de route, je montrerai comment la réflexion et CodeDom fonctionnent ensemble pour gérer efficacement les données d'exécution.
Avant de me plonger dans System.Reflection, j'aimerais discuter de la programmation réflexive en général. Premièrement, la réflexion peut être définie comme toute fonctionnalité fournie par un système de programmation qui permet aux programmeurs d'inspecter et de manipuler des entités de code sans connaissance préalable de leur identité ou de leur structure formelle. Il y a beaucoup de choses à aborder dans cette section, je vais donc les aborder un par un.
Premièrement, qu’apporte la réflexion ? Que pouvez-vous en faire ? J’ai tendance à diviser les tâches typiques centrées sur la réflexion en deux catégories : l’inspection et la manipulation. L'inspection nécessite l'analyse d'objets et de types pour recueillir des informations structurées sur leur définition et leur comportement. Hormis quelques dispositions de base, cela se fait souvent sans aucune connaissance préalable de celles-ci. (Par exemple, dans le .NET Framework, tout hérite de System.Object et une référence à un type d'objet est souvent le point de départ général de la réflexion.)
Les opérations invoquent dynamiquement du code en utilisant les informations recueillies lors de l'inspection, en créant de nouvelles instances ou même en les types et les objets peuvent être facilement restructurés dynamiquement. Un point important à souligner est que pour la plupart des systèmes, la manipulation de types et d’objets au moment de l’exécution entraîne une dégradation des performances par rapport à l’exécution statique d’opérations équivalentes dans le code source. Il s'agit d'un compromis nécessaire en raison de la nature dynamique de la réflexion, mais il existe de nombreux conseils et bonnes pratiques pour optimiser les performances de la réflexion (voir msdn.microsoft.com/msdnmag/issues/05 pour des informations plus détaillées sur l'optimisation des performances). l'utilisation de la réflexion /07/Réflexion).
Alors, quel est le but de la réflexion ? Qu'est-ce que le programmeur inspecte et manipule réellement ? Dans ma définition de la réflexion, j'ai utilisé le nouveau terme « entité de code » pour souligner le fait que du point de vue du programmeur, les techniques de réflexion brouillent parfois les frontières entre les deux. objets et types traditionnels. Par exemple, une tâche typique centrée sur la réflexion pourrait être :
commencer avec un handle vers l’objet O et utiliser la réflexion pour obtenir un handle vers sa définition associée (type T).
Examinez le type T et obtenez un handle vers sa méthode M.
Appelez la méthode M d'un autre objet O' (également de type T).
Notez que je passe d'une instance à son type sous-jacent, de ce type à une méthode, puis j'utilise le handle de la méthode pour l'appeler sur une autre instance - cela utilise évidemment la programmation C# traditionnelle dans le code source. La technologie ne peut pas y parvenir. Après avoir discuté de System.Reflection du .NET Framework ci-dessous, j'expliquerai à nouveau cette situation avec un exemple concret.
Certains langages de programmation fournissent une réflexion nativement via la syntaxe, tandis que d'autres plates-formes et frameworks (tels que .NET Framework) la fournissent sous forme de bibliothèque système. Quelle que soit la manière dont la réflexion est assurée, les possibilités d'utilisation de la technologie de réflexion dans une situation donnée sont assez complexes. La capacité d'un système de programmation à fournir une réflexion dépend de nombreux facteurs : le programmeur fait-il bon usage des fonctionnalités du langage de programmation pour exprimer ses concepts ? Le compilateur intègre-t-il suffisamment d'informations structurées (métadonnées) dans la sortie pour faciliter une analyse future ? Interprétation ? Existe-t-il un sous-système d'exécution ou un interpréteur hôte qui digère ces métadonnées ? La bibliothèque de la plate-forme présente-t-elle les résultats de cette interprétation d'une manière utile aux programmeurs
si vous pensez à un système de types complexe et orienté objet
?apparaît comme une simple fonction de style C dans le code, et il n'y a pas de structure de données formelle, alors il est évidemment impossible pour votre programme de déduire dynamiquement que le pointeur d'une certaine variable v1 pointe vers une instance d'objet d'un certain type T . Parce qu'après tout, le type T est un concept dans votre tête ; il n'apparaît jamais explicitement dans vos instructions de programmation. Mais si vous utilisez un langage orienté objet plus flexible (tel que C#) pour exprimer la structure abstraite du programme et introduire directement le concept de type T, alors le compilateur convertira votre idée en quelque chose qui pourra ensuite être transmis via le Logique appropriée pour comprendre le formulaire, tel que fourni par le Common Language Runtime (CLR) ou un interpréteur de langage dynamique.
La réflexion est-elle entièrement une technologie d'exécution dynamique ? En termes simples, ce n'est pas le cas. Il arrive souvent, tout au long du cycle de développement et d’exécution, que la réflexion soit disponible et utile aux développeurs. Certains langages de programmation sont implémentés via des compilateurs autonomes qui convertissent le code de haut niveau directement en instructions que la machine peut comprendre. Le fichier de sortie inclut uniquement les entrées compilées et le moteur d'exécution n'a aucune logique de prise en charge pour accepter des objets opaques et analyser dynamiquement leurs définitions. C'est exactement le cas de nombreux compilateurs C traditionnels. Comme il y a peu de logique de prise en charge dans l'exécutable cible, vous ne pouvez pas faire beaucoup de réflexion dynamique, mais les compilateurs fournissent de temps en temps une réflexion statique. Par exemple, l'opérateur typeof omniprésent permet aux programmeurs de vérifier les identifiants de type au moment de la compilation.
Une situation complètement différente est que les langages de programmation interprétés sont toujours exécutés via le processus principal (les langages de script entrent généralement dans cette catégorie). Puisque la définition complète du programme est disponible (comme le code source d’entrée), combinée à l’implémentation complète du langage (comme l’interprète lui-même), toutes les techniques nécessaires pour prendre en charge l’auto-analyse sont en place. Ce langage dynamique fournit fréquemment des capacités de réflexion complètes, ainsi qu'un riche ensemble d'outils pour l'analyse et la manipulation dynamiques des programmes.
Le .NET Framework CLR et ses langages hôtes tels que C# se situent au milieu. Le compilateur est utilisé pour convertir le code source en IL et en métadonnées. Ce dernier est de niveau inférieur ou moins « logique » que le code source, mais conserve toujours beaucoup d'informations abstraites sur la structure et le type. Une fois que le CLR démarre et héberge ce programme, la bibliothèque System.Reflection de la bibliothèque de classes de base (BCL) peut utiliser ces informations et renvoyer des informations sur le type d'objet, les membres du type, les signatures des membres, etc. En outre, il peut également prendre en charge les appels, y compris les appels à liaison tardive.
Réflexion dans .NET
Pour tirer parti de la réflexion lors de la programmation avec .NET Framework, vous pouvez utiliser l'espace de noms System.Reflection. Cet espace de noms fournit des classes qui encapsulent de nombreux concepts d'exécution, tels que des assemblys, des modules, des types, des méthodes, des constructeurs, des champs et des propriétés. Le tableau de la figure 1 montre comment les classes de System.Reflection sont mappées à leurs homologues d'exécution conceptuelles.
Bien qu'importants, System.Reflection.Assembly et System.Reflection.Module sont principalement utilisés pour localiser et charger du nouveau code dans le runtime. Dans cette chronique, je ne discuterai pas de ces parties et supposerai que tout le code pertinent a déjà été chargé.
Pour inspecter et manipuler le code chargé, le modèle typique est principalement System.Type. En règle générale, vous commencez par obtenir une instance System.Type de la classe d'exécution qui vous intéresse (via Object.GetType). Vous pouvez ensuite utiliser diverses méthodes de System.Type pour explorer la définition du type dans System.Reflection et obtenir des instances d'autres classes. Par exemple, si vous êtes intéressé par une méthode spécifique et souhaitez obtenir une instance System.Reflection.MethodInfo de cette méthode (peut-être via Type.GetMethod). De même, si vous êtes intéressé par un champ et souhaitez obtenir une instance System.Reflection.FieldInfo de ce champ (peut-être via Type.GetField).
Une fois que vous disposez de tous les objets d’instance de réflexion nécessaires, vous pouvez continuer en suivant les étapes d’inspection ou de manipulation selon vos besoins. Lors de la vérification, vous utilisez diverses propriétés descriptives dans la classe réfléchissante pour obtenir les informations dont vous avez besoin (est-ce un type générique ? Est-ce une méthode d'instance ?). Lors du fonctionnement, vous pouvez appeler et exécuter dynamiquement des méthodes, créer de nouveaux objets en appelant des constructeurs, etc.
Vérification des types et des membres
Passons au code et explorons comment vérifier à l'aide de la réflexion de base. Je vais me concentrer sur l'analyse de type. En commençant par un objet, je vais récupérer son type puis examiner quelques membres intéressants (voir Figure 2).
La première chose à noter est que dans la définition de la classe, il semble à première vue qu'il y ait beaucoup plus d'espace que ce à quoi je m'attendais pour décrire les méthodes. D'où viennent ces méthodes supplémentaires ? Toute personne familiarisée avec la hiérarchie des objets .NET Framework reconnaîtra ces méthodes héritées de la classe de base commune Object elle-même. (En fait, j'ai d'abord utilisé Object.GetType pour récupérer son type.) De plus, vous pouvez voir la fonction getter pour la propriété. Maintenant, que se passe-t-il si vous n'avez besoin que des fonctions explicitement définies de MyClass elle-même ? En d'autres termes, comment masquer les fonctions héritées ? Ou peut-être avez-vous simplement besoin des fonctions d'instance explicitement définies ?
Jetez simplement un œil en ligne sur MSDN et vous le saurez ? J'ai constaté que tout le monde est prêt à utiliser la deuxième méthode surchargée de GetMethods, qui accepte le paramètre BindingFlags. En combinant différentes valeurs de l'énumération BindingFlags, vous pouvez demander à une fonction de renvoyer uniquement le sous-ensemble de méthodes souhaité. Remplacez l'appel GetMethods par :
GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly |BindingFlags.Public)
En conséquence, vous obtenez le résultat suivant (notez qu'il n'y a pas de fonctions d'assistance statiques ni de fonctions héritées de System.Object).
Exemple de démonstration de réflexion 1
Nom du type : MyClass
Nom de la méthode : MyMethod1
Nomde la méthode : MyMethod2
Nom de la méthode : get_MyProperty
Nom de la propriété : MyProperty
Que faire si vous connaissez à l'avance le nom du type (entièrement qualifié) et les membres ? Comment pouvez-vous récupérer un type enum vers Type ? conversion ? Avec le code des deux premiers exemples, vous disposez déjà des composants de base pour implémenter un navigateur de classes primitives. Vous pouvez rechercher une entité d'exécution par son nom, puis énumérer ses différentes propriétés associées.
Appel de code dynamique
Jusqu'à présent, j'ai obtenu des handles vers des objets d'exécution (tels que des types et des méthodes) à des fins descriptives uniquement, comme l'impression de leurs noms. Mais comment faire plus ? Comment appeler une méthode ?
Quelques points clés dans cet exemple sont : d'abord, une instance de System.Type est récupérée à partir d'une instance de MyClass, mc1, puis une instance de MethodInfo est récupérée à partir de ce type. Enfin, lorsque MethodInfo est appelé, il est lié à une autre instance MyClass (mc2) en la passant comme premier paramètre de l'appel.
Comme mentionné précédemment, cet exemple brouille la distinction entre les types et l'utilisation des objets que vous vous attendez à voir dans le code source. Logiquement, vous récupérez un handle vers une méthode, puis appelez la méthode comme si elle appartenait à un objet différent. Pour les programmeurs familiers avec les langages de programmation fonctionnels, cela peut être un jeu d'enfant ; mais pour les programmeurs qui ne connaissent que C#, il n'est peut-être pas si intuitif de séparer l'implémentation d'objets et l'instanciation d'objets.
Rassembler tout cela
Jusqu'à présent, j'ai discuté des principes de base du check et du call, et maintenant je vais les rassembler avec des exemples concrets. Imaginez que vous souhaitiez fournir une bibliothèque avec des fonctions d'assistance statiques qui doivent gérer des objets. Mais au moment de la conception, vous n'avez aucune idée des types de ces objets ! Cela dépend des instructions de l'appelant de la fonction sur la manière dont il souhaite extraire des informations significatives de ces objets ! La fonction acceptera une collection d'objets et un descripteur de chaîne de la méthode. Il parcourra ensuite la collection, appelant les méthodes de chaque objet et agrégeant les valeurs de retour avec une fonction.
Pour cet exemple, je vais déclarer quelques contraintes. Premièrement, la méthode décrite par le paramètre string (qui doit être implémentée par le type sous-jacent de chaque objet) n'acceptera aucun paramètre et renverra un entier. Le code parcourra la collection d'objets, appelant la méthode spécifiée et calculera progressivement la moyenne de toutes les valeurs. Enfin, comme il ne s'agit pas de code de production, je n'ai pas à me soucier de la validation des paramètres ou du débordement d'entier lors de la sommation.
En parcourant l'exemple de code, vous pouvez voir que l'accord entre la fonction principale et l'assistant statique ComputeAverage ne repose sur aucune information de type autre que la classe de base commune de l'objet lui-même. En d'autres termes, vous pouvez complètement modifier le type et la structure de l'objet transféré, mais tant que vous pouvez toujours utiliser une chaîne pour décrire une méthode qui renvoie un entier, ComputeAverage fonctionnera correctement
! est caché dans Le dernier exemple est lié à MethodInfo (réflexion générale). Notez que dans la boucle foreach de ComputeAverage, le code récupère uniquement un MethodInfo du premier objet de la collection, puis le lie à l'appel de tous les objets suivants. Comme le montre le codage, cela fonctionne bien - il s'agit d'un exemple simple de mise en cache MethodInfo. Mais il y a ici une limite fondamentale. Une instance MethodInfo ne peut être appelée que par une instance du même type hiérarchique que l'objet qu'elle récupère. Cela est possible car les instances de IntReturner et SonOfIntReturner (héritées de IntReturner) sont transmises.
Dans l'exemple de code, une classe nommée EnemyOfIntReturner a été incluse, qui implémente le même protocole de base que les deux autres classes, mais ne partage aucun type partagé commun. En d’autres termes, les interfaces sont logiquement équivalentes, mais il n’y a pas de chevauchement au niveau du type. Pour explorer l'utilisation de MethodInfo dans cette situation, essayez d'ajouter un autre objet à la collection, obtenez une instance via "new EnemyOfIntReturner(10)" et exécutez à nouveau l'exemple. Vous rencontrerez une exception indiquant que MethodInfo ne peut pas être utilisé pour appeler l'objet spécifié car cela n'a absolument rien à voir avec le type d'origine à partir duquel MethodInfo a été obtenu (même si le nom de la méthode et le protocole sous-jacent sont équivalents). Pour que votre code soit prêt pour la production, vous devez être prêt à faire face à cette situation.
Une solution possible pourrait être d'analyser vous-même les types de tous les objets entrants, en conservant l'interprétation de leur hiérarchie de types partagée (le cas échéant). Si le type de l'objet suivant est différent de toute hiérarchie de types connue, une nouvelle MethodInfo doit être obtenue et stockée. Une autre solution consiste à intercepter l'exception TargetException et à réobtenir une instance MethodInfo. Les deux solutions mentionnées ici ont leurs avantages et leurs inconvénients. Joel Pobar a écrit un excellent article pour le numéro de mai 2007 de ce magazine sur la mise en mémoire tampon et les performances de réflexion de MethodInfo, que je recommande vivement.
Espérons que cet exemple démontre l’ajout de réflexion à une application ou à un framework pour ajouter plus de flexibilité pour une personnalisation ou une extensibilité future. Certes, l’utilisation de la réflexion peut s’avérer un peu lourde par rapport à la logique équivalente des langages de programmation natifs. Si vous pensez que l'ajout d'une liaison tardive basée sur la réflexion à votre code est trop fastidieux pour vous ou vos clients (après tout, ils ont besoin que leurs types et leur code soient pris en compte d'une manière ou d'une autre dans votre framework), alors cela n'est peut-être nécessaire que dans un souci de flexibilité de modération. pour parvenir à un certain équilibre.
Gestion efficace des types pour la sérialisation
Maintenant que nous avons couvert les principes de base de la réflexion .NET à travers plusieurs exemples, examinons une situation réelle. Si votre logiciel interagit avec d'autres systèmes via des services Web ou d'autres technologies d'accès à distance hors processus, vous avez probablement rencontré des problèmes de sérialisation. La sérialisation convertit essentiellement les objets actifs occupant de la mémoire en un format de données adapté à la transmission en ligne ou au stockage sur disque.
L'espace de noms System.Xml.Serialization dans le .NET Framework fournit un puissant moteur de sérialisation avec XmlSerializer, qui peut prendre n'importe quel objet géré et le convertir en XML (les données XML peuvent également être reconverties en une instance d'objet typée à l'avenir. Ce processus est appelée désérialisation). La classe XmlSerializer est un logiciel puissant et prêt pour l'entreprise qui sera votre premier choix si vous rencontrez des problèmes de sérialisation dans votre projet. Mais à des fins pédagogiques, explorons comment implémenter la sérialisation (ou d'autres instances de gestion de type d'exécution similaires).
Considérez ceci : vous fournissez un framework qui prend des instances d'objet de types d'utilisateurs arbitraires et les convertit en un format de données intelligent. Par exemple, supposons que vous ayez un objet résident en mémoire de type Adresse comme indiqué ci-dessous :
(pseudocode)
adressedeclasse
{
Identifiant d'adresse ;
Rue des cordes, ville ;
StateType État ;
ZipCodeTypeZipCode ;
}
Comment générer une représentation de données appropriée pour une utilisation ultérieure ? Peut-être qu'un simple rendu de texte résoudra ce problème :
Adresse : 123
Rue : 1 Microsoft Way
Ville : Redmond
État : WA
Code postal : 98052
Si les données formelles qui doivent être converties sont entièrement comprises tapez à l'avance (par exemple lorsque vous écrivez le code vous-même), les choses deviennent très simples :
foreach(Address a in AddressList)
{
Console.WriteLine("Adresse :{0}", a.ID);
Console.WriteLine("tStreet:{0}", a.Street);
... // et ainsi de suite
}
Cependant, les choses peuvent devenir vraiment intéressantes si vous ne savez pas à l'avance quels types de données vous rencontrerez lors de l'exécution. Comment écrivez-vous du code de framework général comme celui-ci ?
MyFramework.TranslateObject (entrée d'objet, sortie MyOutputWriter)
Tout d'abord, vous devez décider quels membres de type sont utiles pour la sérialisation. Les possibilités incluent la capture uniquement des membres d'un type spécifique, tels que les types de système primitifs, ou la fourniture d'un mécanisme permettant aux auteurs de types d'indiquer quels membres doivent être sérialisés, par exemple en utilisant des propriétés personnalisées comme marqueurs sur les membres de type). Vous pouvez uniquement capturer les membres d'un type spécifique, tel que les types de système primitifs, ou l'auteur du type peut indiquer quels membres doivent être sérialisés (éventuellement en utilisant des propriétés personnalisées comme marqueurs sur les membres du type).
Une fois que vous avez documenté les membres de la structure de données qui doivent être convertis, vous devez ensuite écrire la logique pour les énumérer et les récupérer à partir des objets entrants. La réflexion fait le gros du travail ici, vous permettant d'interroger à la fois les structures de données et les valeurs des données.
Par souci de simplicité, concevons un moteur de conversion léger qui prend un objet, obtient toutes ses valeurs de propriété publique, les convertit en chaînes en appelant directement ToString, puis sérialise les valeurs. Pour un objet donné nommé « input », l'algorithme est à peu près le suivant :
appelez input.GetType pour récupérer une instance System.Type, qui décrit la structure sous-jacente de l'entrée.
Utilisez Type.GetProperties et le paramètre BindingFlags approprié pour récupérer les propriétés publiques en tant qu'instances PropertyInfo.
Les propriétés sont récupérées sous forme de paires clé-valeur à l'aide de PropertyInfo.Name et PropertyInfo.GetValue.
Appelez Object.ToString sur chaque valeur pour la convertir (de manière basique) au format chaîne.
Emballez le nom du type d'objet et la collection de noms de propriétés et de valeurs de chaîne dans le format de sérialisation correct.
Cet algorithme simplifie considérablement les choses tout en capturant l'intérêt de prendre une structure de données d'exécution et de la transformer en données auto-descriptives. Mais il y a un problème : les performances. Comme mentionné précédemment, la réflexion est très coûteuse, tant pour le traitement des types que pour la récupération des valeurs. Dans cet exemple, j'effectue une analyse de type complète sur chaque instance du type fourni.
Et s'il était possible de capturer ou de préserver d'une manière ou d'une autre votre compréhension de la structure d'un type afin de pouvoir le récupérer sans effort plus tard et gérer efficacement les nouvelles instances de ce type ? La nouvelle est qu'il est possible de le faire en utilisant les fonctionnalités du .NET Framework. Une fois que vous comprenez la structure de données d'un type, vous pouvez utiliser CodeDom pour générer dynamiquement du code qui se lie à cette structure de données. Vous pouvez générer un assembly d'assistance qui contient une classe d'assistance et des méthodes qui font référence au type entrant et accèdent directement à ses propriétés (comme toute autre propriété dans le code managé), de sorte que la vérification de type n'a un impact sur les performances qu'une seule fois.
Maintenant, je vais corriger cet algorithme. Nouveau type :
récupère l'instance System.Type correspondant à ce type.
Utilisez les différents accesseurs System.Type pour récupérer le schéma (ou au moins le sous-ensemble du schéma utile pour la sérialisation), tel que les noms de propriétés, les noms de champs, etc.
Utilisez les informations de schéma pour générer un assembly d'assistance (via CodeDom) qui est lié au nouveau type et gère efficacement les instances.
Utilisez du code dans un assembly d’assistance pour extraire les données d’instance.
Sérialisez les données selon vos besoins.
Pour toutes les données entrantes d'un type donné, vous pouvez passer à l'étape 4 et obtenir une énorme amélioration des performances par rapport à la vérification explicite de chaque instance.
J'ai développé une bibliothèque de sérialisation de base appelée SimpleSerialization qui implémente cet algorithme en utilisant la réflexion et CodeDom (téléchargeable dans cette rubrique). Le composant principal est une classe nommée SimpleSerializer, qui est construite par l'utilisateur avec une instance de System.Type. Dans le constructeur, la nouvelle instance SimpleSerializer analyse le type donné et génère un assembly temporaire à l'aide de classes d'assistance. La classe d'assistance est étroitement liée au type de données donné et gère l'instance comme si vous écriviez le code avec une connaissance préalable complète du type.
La classe SimpleSerializer a la disposition suivante :
classe SimpleSerializer
{
classe publique SimpleSerializer (Type dataType);
public void Serialize (entrée d'objet, rédacteur SimpleDataWriter);
}
Tout simplement incroyable ! Le constructeur fait le gros du travail : il utilise la réflexion pour analyser la structure du type, puis utilise CodeDom pour générer l'assembly d'assistance. La classe SimpleDataWriter n'est qu'un récepteur de données utilisé pour illustrer les modèles de sérialisation courants.
Pour sérialiser une instance de classe Address simple, utilisez le pseudocode suivant pour effectuer la tâche :
SimpleSerializer
mySerializer = new SimpleSerializer(typeof(Address));
SimpleDataWriterwriter = new SimpleDataWriter(
)
;
Il est recommandé d'essayer vous-même l'exemple de code, en particulier la bibliothèque SimpleSerialization. J'ai ajouté des commentaires sur certaines parties intéressantes de SimpleSerializer, j'espère que cela vous aidera. Bien entendu, si vous avez besoin d'une sérialisation stricte dans le code de production, vous devez réellement vous appuyer sur les technologies fournies dans le .NET Framework (telles que XmlSerializer). Mais si vous constatez que vous devez travailler avec des types arbitraires au moment de l'exécution et les gérer efficacement, j'espère que vous adopterez ma bibliothèque SimpleSerialization comme solution.