Notes
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Objets Expando dans C# 4.0
Dino Esposito
La plus grande partie du code écrit pour Microsoft .NET Framework est basée sur le typage statique, même si .NET prend en charge le typage dynamique. Il y a 10 ans, JScript avait un système de type dynamique en plus du système .NET, tout comme Visual Basic. Le typage statique signifie que le type de chaque expression est connu. Les types et les attributions sont validés au moment de la compilation et la plupart des erreurs de typage sont repérées à l'avance.
Il existe une exception bien connue : lorsque vous tentez une conversion au moment de l'exécution, ce qui peut parfois provoquer une erreur dynamique si le type de source n'est pas compatible avec le type de cible.
Le typage statique procure des avantages en termes de performances et de clarté, mais il suppose que vous sachiez presque tout de votre code (et de vos données) à l'avance. Il est aujourd'hui essentiel d'assouplir cette contrainte. Aller au-delà du typage statique signifie généralement prendre en compte trois options distinctes : le typage dynamique, les objets dynamiques et la programmation indirecte ou réflexive.
En programmation .NET, la réflexion est disponible depuis .NET Framework 1.0 et a été largement utilisée pour alimenter des infrastructures spéciales, comme les conteneurs d'inversion de contrôle (IoC, Inversion of Control). Ces infrastructures fonctionnent par la résolution de dépendances de types au moment de l'exécution. Cela permet à votre code de fonctionner avec une interface sans devoir connaître le type concret qui se trouve derrière l'objet, ni son comportement effectif. La réflexion .NET vous permet d'implémenter des formes de programmation indirecte lorsque votre code communique avec un objet intermédiaire qui envoie à son tour des appels vers une interface fixe. Vous transmettez le nom d'un membre pour l'invoquer en tant que chaîne. Vous avez ainsi la possibilité de le lire à partir d'une source extérieure. L'interface de l'objet cible est fixe et immuable. Il y a toujours une interface connue derrière chaque appel effectué par réflexion.
Le typage dynamique signifie que votre code compilé ignore la structure statique des types qui peuvent être détectés à la compilation. Le typage dynamique diffère toutes les vérifications des types jusqu'à l'exécution. L'interface que vous codez est fixe et immuable, mais la valeur que vous utilisez peut renvoyer différentes interfaces selon les circonstances.
Le .NET Framework 4 propose de nouvelles fonctionnalités qui vous permettent d'aller au-delà des types statiques. J'ai abordé le nouveau mot clé dynamique dans le numéro de mai 2010. Dans cet article, je présenterai la prise en charge de types définis de manière dynamique, comme les objets expando et les objets dynamiques. Les objets dynamiques vous permettent de définir l'interface du type par programme plutôt que de la lire à partir d'une définition stockée statiquement dans des assemblys. Les objets dynamiques associent la propreté des objets de type statique à la flexibilité des objets de type dynamique.
Scénarios pour les objets dynamiques
Les objets dynamiques ne sont pas destinés à remplacer les qualités des types statiques. En effet, ces derniers sont la base du développement logiciel et ils le resteront encore quelques temps. Avec le typage statique, vous pouvez détecter les erreurs de typage de façon fiable à la compilation et produire un code qui, par conséquent, ne comporte pas de vérification lors de l'exécution et qui s'exécute plus rapidement. La nécessité de réussir l'étape de compilation conduit également les développeurs et les architectes à faire attention à la conception des logiciels et à la définition des interfaces publiques permettant l'interaction entre les couches.
Dans certaines situations cependant, des blocs de données relativement bien structurés doivent être consommés à l'aide d'un programme. Dans l'idéal, vous souhaitez que ces données soient exposées par le biais d'objets. Mais, au lieu de cela, vous les recevez sous la forme d'un flux de données brut, qu'elles vous parviennent via une connexion réseau ou que vous les lisiez à partir d'un fichier de disque. Deux options vous permettent d'utiliser ces données : utiliser une approche indirecte ou utiliser un type ad hoc.
Dans le cas de l'approche indirecte, vous employez une API générique faisant office de proxy et organisant les requêtes et les mises à jour pour vous. Si vous utilisez un type ad hoc, vous disposez d'un type spécifique qui modélise parfaitement les données que vous utilisez. La question est de savoir qui va créer un tel type ad hoc ?
Dans certains segments de .NET Framework, vous disposez déjà d'exemples intéressants de modules internes créant des types ad hoc pour des blocs de données spécifiques. Les Web Forms ASP.NET constituent un exemple évident. Lorsque vous demandez une ressource ASPX, le serveur Web récupère le contenu du fichier serveur ASPX. Ce contenu est alors chargé dans une chaîne pour être traité dans une réponse HTML. Vous obtenez ainsi un texte relativement bien structuré pour travailler.
Pour exploiter ces données, vous devez comprendre les références dont vous disposez pour les contrôles serveur, les instancier correctement et les associer dans une page. Vous pouvez le faire à l'aide d'un analyseur XML pour chaque requête. Néanmoins, les frais supplémentaires de l'analyseur sont imputés pour chaque requête, ce qui n'est sans doute pas acceptable.
En raison de ces frais supplémentaires d'analyse des données, l'équipe ASP.NET a décidé d'introduire une étape unique pour analyser le balisage dans une classe pouvant être compilée dynamiquement. Vous obtenez ainsi un extrait de balisage comme celui-ci-dessous, consommé via une classe ad hoc dérivée de la classe code-behind de la page Web Forms :
<html>
<head runat="server">
<title></title>
</head>
<body>
<form id="Form1" runat="server">
<asp:TextBox runat="server" ID="TextBox1" />
<asp:Button ID="Button1" runat="server" Text="Click" />
<hr />
<asp:Label runat="server" ID="Label1"></asp:Label>
</form>
</body>
</html>
La figure 1 montre la structure de l'exécution de la classe créée à partir du balisage. Les noms des méthodes affichés en gris concernent les procédures internes utilisées pour analyser des éléments avec les éléments runat=server dans les instances de contrôles serveur.
Figure 1 Structure d'une classe Web Forms créée dynamiquement
Vous pouvez appliquer cette approche à presque toutes les situations dans lesquelles votre application reçoit des données externes à traiter plusieurs fois. Prenez par exemple un flux de données XML qui arrive dans l'application. Plusieurs API sont disponibles pour gérer les données XML, de XML DOM à LINQ-to-XML. Dans ce cas, vous devez poursuivre indirectement en interrogeant l'API XML DOM ou LINQ-to-XML, ou utiliser les mêmes API pour analyser les données brutes dans des objets ad hoc.
Dans .NET Framework 4, les objets dynamiques proposent une autre API, plus simple, pour créer des types dynamiquement, sur la base de données brutes. Pour illustrer rapidement ceci, examinez la chaîne XML suivante :
<Persons>
<Person>
<FirstName> Dino </FirstName>
<LastName> Esposito </LastName>
</Person>
<Person>
<FirstName> John </FirstName>
<LastName> Smith </LastName>
</Person>
</Persons>
Dans .NET Framework 3.5, vous utiliseriez sans doute un code similaire à celui de la figure 2 pour transformer ceci en type programmable.
Figure 2 Utilisation de LINQ-to-XML pour charger des données dans un objet Person
var persons = GetPersonsFromXml(file);
foreach(var p in persons)
Console.WriteLine(p.GetFullName());
// Load XML data and copy into a list object
var doc = XDocument.Load(@"..\..\sample.xml");
public static IList<Person> GetPersonsFromXml(String file) {
var persons = new List<Person>();
var doc = XDocument.Load(file);
var nodes = from node in doc.Root.Descendants("Person")
select node;
foreach (var n in nodes) {
var person = new Person();
foreach (var child in n.Descendants()) {
if (child.Name == "FirstName")
person.FirstName = child.Value.Trim();
else
if (child.Name == "LastName")
person.LastName = child.Value.Trim();
}
persons.Add(person);
}
return persons;
}
Le code utilise LINQ-to-XML pour charger un contenu brut dans une instance de la classe Person :
public class Person {
public String FirstName { get; set; }
public String LastName { get; set; }
public String GetFullName() {
return String.Format("{0}, {1}", LastName, FirstName);
}
}
.NET Framework 4 propose une API différente pour parvenir au même résultat. L'écriture de cette API, centrée sur la classe ExpandoObject, est plus directe. L'API ne nécessite pas de planification, d'écriture, de débogage, de test ni de gestion d'une classe Person. Voici plus d'informations à propos des ExpandoObject.
Utilisation de la classe ExpandoObject
Les objets Expando n'ont pas été inventés pour .NET Framework. Ils sont en fait apparus quelques années avant .NET. La première fois que j'ai entendu ce terme, c'était pour décrire les objets JScript, au milieu des années 1990. Un expando est une sorte d'objet gonflable, dont la structure est entièrement définie au moment de l'exécution. Dans .NET Framework 4, ils sont utilisés comme des objets gérés de façon classique, mis à part leur structure qui n'est pas lue à partir d'une assembly, mais entièrement créée de manière dynamique.
Un objet expando est idéal pour modéliser des informations qui évoluent de manière dynamique, comme le contenu d'un fichier de configuration. Voyons comment utiliser la classe ExpandoObject pour stocker le contenu du document XML mentionné ci-dessus. Le code source complet est affiché à la figure 3.
Figure 3 Utilisation de LINQ-to-XML pour charger des données dans un objet Expando
public static IList<dynamic> GetExpandoFromXml(String file) {
var persons = new List<dynamic>();
var doc = XDocument.Load(file);
var nodes = from node in doc.Root.Descendants("Person")
select node;
foreach (var n in nodes) {
dynamic person = new ExpandoObject();
foreach (var child in n.Descendants()) {
var p = person as IDictionary<String, object>);
p[child.Name] = child.Value.Trim();
}
persons.Add(person);
}
return persons;
}
La fonction renvoie une liste d'objets définis de manière dynamique. Avec LINQ-to-XML, vous analysez les nœuds dans les balises et créez une instance ExpandoObject pour chaque nœud. Le nom de chaque nœud situé en dessous de <Person> devient une nouvelle propriété de l'objet Expando. La valeur de la propriété est le texte interne du nœud. Étant donné le contenu XML, vous obtenez un objet Expando dont la propriété FirstName est définie sur Dino.
La figure 3 indique cependant la syntaxe de l'indexeur utilisée pour remplir l'objet Expando. Cela nécessite quelques explications supplémentaires.
Dans la classe ExpandoObject
La classe ExpandoObject appartient à l'espace de noms System.Dynamic et elle est définie dans l'assembly System.Core. ExpandoObject représente un objet dont les membres peuvent être ajoutés ou retirés de façon dynamique au moment de l'exécution. La classe est scellée et implémente plusieurs interfaces :
public sealed class ExpandoObject :
IDynamicMetaObjectProvider,
IDictionary<string, object>,
ICollection<KeyValuePair<string, object>>,
IEnumerable<KeyValuePair<string, object>>,
IEnumerable,
INotifyPropertyChanged;
Comme vous pouvez le remarquer, la classe expose son contenu à l'aide de plusieurs interfaces énumérables, dont IDictionary<String, Object> et IEnumerable. Elle implémente également IDynamicMetaObjectProvider. Il s'agit de l'interface standard qui permet de partager un objet dans le Dynamic Language Runtime (DLR) par des programmes écrits conformément au modèle d'interpolarité du DLR. En d'autres termes, seuls les objets qui implémentent l'interface IDynamicMetaObjectProvider peuvent être partagés entre les langages dynamiques .NET. Un objet Expando peut être transmis, par exemple, à un composant IronRuby. Vous ne pouvez pas faire cela facilement avec un objet géré par .NET. Enfin, c'est possible, mais vous ne pouvez pas obtenir le comportement dynamique.
La classe ExpandoObject implémente également l'interface INotifyPropertyChanged. Cela permet à la classe de générer un événement PropertyChanged lorsqu'un membre est ajouté ou modifié. La prise en charge de l'interface INotifyPropertyChanged est indispensable pour utiliser les objets Expando dans les systèmes frontaux des applications Silverlight et Windows Presentation Foundation.
Vous créez une instance ExpandoObject de la même manière que tout autre objet .NET, à la différence près que la variable utilisée pour stocker l'instance est type dynamique :
dynamic expando = new ExpandoObject();
À ce stade, vous devez simplement affecter une nouvelle valeur à l'objet Expando pour lui ajouter une propriété, comme illustré ci-dessous :
expando.FirstName = "Dino";
Peu importe qu'aucune information n'existe à propos du membre FirstName, de son type ou de sa visibilité. Il s'agit de code dynamique. De ce fait, utiliser le mot clé var pour attribuer une instance ExpandoObject à une variable fait une grande différence :
var expando = new ExpandoObject();
Ce code compilera et fonctionnera correctement. Cependant, cette définition ne vous autorise pas à attribuer de valeur à une propriété FirstName. La classe ExpandoObject, telle qu'elle est définie dans System.Core, ne comporte pas ce type de membre. Plus précisément, la classe ExpandoObject ne comporte pas de membres publics.
Cela constitue un point important. Lorsque le type statique d'un objet Expando est dynamique, les opérations sont liées comme des opérations dynamiques, y compris la recherche de membres. Lorsque le type statique est ExpandoObject, les opérations sont liées en tant que recherche standard de membres à la compilation. Le compilateur sait ainsi que le type dynamique est un type spécial, mais il ne sait pas que le type ExpandoObject est un type spécial.
La figure 4 illustre les options IntelliSense de Visual Studio 2010 lorsqu'un objet Expando est déclaré de type dynamique et lorsqu'il est considéré comme un objet .NET brut. Dans ce dernier cas, IntelliSense vous montre les membres Systeme.Object par défaut, ainsi que la liste des méthodes d'extension pour les classes de collection.
Figure 4 Visual Studio 2010 IntelliSense et objets Expando
Notez que, dans certaines circonstances, certains outils disponibles peuvent aller au-delà de ce comportement de base. La figure 5 illustre ReSharper 5.0, qui capture la liste des membres actuellement définis sur l'objet. Cela ne se produit pas si des membres sont ajoutés par programme via un indexeur.
Figure 5 ReSharper 5.0 IntelliSense en action avec les objets Expando
Pour ajouter une méthode à un objet Expando, vous devez simplement le définir en tant que propriété, mais vous utilisez un délégué Action<T> ou Func<T> pour exprimer le comportement. Voici un exemple :
person.GetFullName = (Func<String>)(() => {
return String.Format("{0}, {1}",
person.LastName, person.FirstName);
});
La méthode GetFullName renvoie une chaîne obtenue en combinant les propriétés de nom et de prénom supposées disponibles de l'objet Expando. Si vous tentez d'accéder à un membre manquant sur des objets Expando, vous recevez une exception RuntimeBinderException.
Programmes XML
Pour lier les concepts que je vous ai montrés jusqu'à présent, laissez-moi vous décrire un exemple où la structure des données et la structure de l'interface utilisateur sont définies dans un fichier XML. Le contenu du fichier est décomposé en plusieurs objets expando et traité par l'application. Cependant, cette dernière fonctionne uniquement avec des informations présentées de façon dynamique et n'est liée à aucun type statique.
Le code de la figure 3 définit une liste d'objets expando personne définis de manière dynamique. Comme vous pouvez l'imaginer, si vous ajoutez un nouveau nœud au schéma XML, une nouvelle propriété est créée dans l'objet expando. Pour lire le nom du membre à partir d'une source externe, vous devez utiliser l'API de l'indexeur pour l'ajouter à l'expando. La classe ExpandoObject implémente explicitement l'interface IDictionary<String, Object>. Cela signifie que vous devez séparer l'interface ExpandoObject du type dictionnaire pour pouvoir utiliser l'API de l'indexeur ou la méthode Add (Ajouter) :
(person as IDictionary<String, Object>)[child.Name] = child.Value;
Étant donné ce comportement, vous devez modifier le fichier XML pour mettre à disposition un nouveau jeu de données. Comment consommer ces données modifiées dynamiquement ? Votre interface utilisateur doit être assez flexible pour recevoir un jeu de données variable.
Prenons un exemple simple, dans lequel vous vous contentez d'afficher des données sur la console. Supposons que le fichier XML contient une section décrivant l'interface utilisateur attendue, quelle qu'elle soit dans votre contexte. Voici l'exemple :
<Settings>
<Output Format="{0}, {1}"
Params="LastName,FirstName" />
</Settings>
Cette information sera chargée dans un autre objet expando à l'aide du code suivant :
dynamic settings = new ExpandoObject();
settings.Format =
node.Attribute("Format").Value;
settings.Parameters =
node.Attribute("Params").Value;
La structure de la procédure principale sera la suivante :
public static void Run(String file) {
dynamic settings = GetExpandoSettings(file);
dynamic persons = GetExpandoFromXml(file);
foreach (var p in persons) {
var memberNames =
(settings.Parameters as String).
Split(',');
var realValues =
GetValuesFromExpandoObject(p,
memberNames);
Console.WriteLine(settings.Format,
realValues);
}
}
L'objet expando contient le format de la sortie, ainsi que les noms des membres dont les valeurs doivent être affichées. Étant donné l'objet dynamique Person, vous devez charger les valeurs des membres spécifiés à l'aide d'un code similaire à celui-ci :
public static Object[] GetValuesFromExpandoObject(
IDictionary<String, Object> person,
String[] memberNames) {
var realValues = new List<Object>();
foreach (var m in memberNames)
realValues.Add(person[m]);
return realValues.ToArray();
}
Étant donné qu'un objet expando implémente IDictionary<String, Object>, vous pouvez utiliser l'API de l'indexeur pour obtenir et définir des valeurs.
Enfin, la liste des valeurs récupérées à partir de l'objet expando sont transmises à la console pour être affichées. La figure 6 illustre deux écrans pour l'exemple d'application de console, qui diffèrent uniquement par la structure du fichier XML sous-jacent.
Figure 6 Deux exemples d'applications de console basées sur un fichier XML
Certes, cet exemple est simple, mais le mécanisme nécessaire à son fonctionnement est identique à celui d'exemples plus complexes. Essayez-le et faites-nous part de vos commentaires !
Dino Esposito est l'auteur de « Programming ASP.NET MVC » chez Microsoft Press et co-auteur de « Microsoft .NET: Architecting Applications for the Enterprise » (Microsoft Press, 2008). Basé en Italie, Dino Esposito participe régulièrement aux différentes manifestations du secteur organisées aux quatre coins du monde. Vous trouverez son blog à l'adresse : weblogs.asp.net/despos.
Je remercie l'expert technique suivant d'avoir relu cet article : Eric Lippert