Référence
LINQ
Language Integrated Query en C# 2008 Joseph C. Rattz
Réseaux et télécom Programmation
Génie logiciel
Sécurité Système d’exploitation
Linq FM Prél Page I Mercredi, 18. février 2009 8:15 08
LINQ Language Integrated Query en C# 2008 Joseph C. Rattz, Jr.
Traduction : Michel Martin, MVP Relecture technique : Mitsuru Furuta, Microsoft France Pierrick Gourlain, MVP Client Application Matthieu Mezil, MVP C#
Linq.book Page II Mercredi, 18. février 2009 7:58 07
Pearson Education France a apporté le plus grand soin à la réalisation de ce livre afin de vous fournir une information complète et fiable. Cependant, Pearson Education France n’assume de responsabilités, ni pour son utilisation, ni pour les contrefaçons de brevets ou atteintes aux droits de tierces personnes qui pourraient résulter de cette utilisation. Les exemples ou les programmes présents dans cet ouvrage sont fournis pour illustrer les descriptions théoriques. Ils ne sont en aucun cas destinés à une utilisation commerciale ou professionnelle. Pearson Education France ne pourra en aucun cas être tenu pour responsable des préjudices ou dommages de quelque nature que ce soit pouvant résulter de l’utilisation de ces exemples ou programmes. Tous les noms de produits ou marques cités dans ce livre sont des marques déposées par leurs propriétaires respectifs.
Publié par Pearson Education France 47 bis, rue des Vinaigriers 75010 PARIS Tél. : 01 72 74 90 00 www.pearson.fr
Titre original : Pro LINQ Language Integrated Query in C# 2008 Traduit de l’américain par Michel Martin
Mise en pages : TyPAO
Relecture technique : Mitsuru Furuta, Pierrick Gourlain, Matthieu Mezil
ISBN : 978-2-7440-4106-8 Copyright © 2009 Pearson Education France Tous droits réservés
ISBN original : 978-1-59059-789-9 Copyright © 2007 by Joseph C. Rattz, Jr. All rights reserved Édition originale publiée par Apress 2855 Telegraph Avenue, Suite 600, Berkeley, CA 94705 www.apress.com
Aucune représentation ou reproduction, même partielle, autre que celles prévues à l’article L. 122-5 2˚ et 3˚ a) du code de la propriété intellectuelle ne peut être faite sans l’autorisation expresse de Pearson Education France ou, le cas échéant, sans le respect des modalités prévues à l’article L. 122-10 dudit code. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.
Linq.book Page III Mercredi, 18. février 2009 7:58 07
Table des matières À propos de l’auteur..........................................................................................................
XI
Traducteur et relecteurs techniques ................................................................................
XIII
Partie I LINQ et C# 2008 1 Hello LINQ.................................................................................................................... Un changement de paradigme .................................................................................... Interrogation XML .......................................................................................... Interrogation d’une base de données SQL Server ........................................... Introduction ................................................................................................................ LINQ et l’interrogation des données ............................................................... Composants ..................................................................................................... Comment travailler avec LINQ ....................................................................... LINQ ne se limite pas aux requêtes ............................................................................ Quelques conseils avant de commencer ..................................................................... Utilisez le mot-clé var si vous n’êtes pas à l’aise ........................................... Utilisez les opérateurs Cast ou OfType pour les collections héritées ............. Préférez l’opérateur OfType à l’opérateur Cast .............................................. Les requêtes aussi peuvent être boguées ......................................................... Sachez tirer parti des requêtes différées .......................................................... Utiliser le log du DataContext ....................................................................... Utilisez le forum LINQ ................................................................................... Résumé .......................................................................................................................
3 3 4 5 6 7 7 9 9 12 12 14 15 15 16 17 18 18
2 Améliorations de C# 3.0 pour LINQ .......................................................................... Les nouveautés du langage C# 3.0 ............................................................................. Les expressions lambda ................................................................................... Arbres d’expressions ....................................................................................... Le mot-clé var, l’initialisation d’objets et les types anonymes ...................... Méthodes d’extension ..................................................................................... Méthodes partielles ......................................................................................... Expressions de requête .................................................................................... Résumé .......................................................................................................................
19 19 20 25 26 31 37 39 49
Linq.book Page IV Mercredi, 18. février 2009 7:58 07
IV
Table des matières
Partie II LINQ to Objects 3 Introduction à LINQ to Objects..................................................................................
53
Vue d’ensemble de LINQ to Objects .......................................................................... IEnumerable, séquences et opérateurs de requête standard ................................ IEnumerable, yield et requêtes différées ......................................................... Délégués Func ................................................................................................. Les opérateurs de requête standard ............................................................................. Résumé ............................................................................................................
53 54 55 58 59 61
4 Les opérateurs différés.................................................................................................
63
Espaces de noms référencés ....................................................................................... Assemblies référencés ................................................................................................ Classes communes ...................................................................................................... Les opérateurs différés, par groupes fonctionnels ...................................................... Restriction ....................................................................................................... Projection ........................................................................................................ Partage ............................................................................................................ Concaténation .................................................................................................. Tri .................................................................................................................... Opérateurs de jointure ..................................................................................... Opérateurs de regroupement ........................................................................... Opérateurs d’initialisation ............................................................................... Opérateurs de conversion ................................................................................ Opérateurs dédiés aux éléments ...................................................................... Opérateurs de génération ................................................................................. Résumé .......................................................................................................................
63 64 64 65 65 67 76 83 85 100 104 110 115 122 126 129
5 Les opérateurs non différés .........................................................................................
131
Espaces de noms référencés ....................................................................................... Classes communes ...................................................................................................... Les opérateurs non différés, par groupes fonctionnels ............................................... Opérateurs de conversion ................................................................................ Opérateurs d’égalité ........................................................................................ Opérateurs agissant au niveau des éléments ................................................... Quantificateurs ................................................................................................ Fonctions de comptage .................................................................................... Résumé .......................................................................................................................
131 131 134 134 145 148 160 165 178
Linq.book Page V Mercredi, 18. février 2009 7:58 07
Table des matières
V
6 Introduction à LINQ to XML ..................................................................................... Introduction ................................................................................................................ Se passer de l’API W3C DOM XML ......................................................................... Résumé .......................................................................................................................
183 185 185 187
7 L’API LINQ to XML.................................................................................................... Espaces de noms référencés ....................................................................................... Améliorations de l’API ............................................................................................... La construction fonctionnelle simplifie la création d’arbres XML ................. L’élément, point central d’un objet XML ....................................................... Noms, espaces de noms et préfixes ................................................................. Extraction de valeurs de nœuds ....................................................................... Le modèle d’objet LINQ to XML .............................................................................. Exécution différée des requêtes, suppression de nœuds et bogue d’Halloween ......... Création XML ............................................................................................................ Création d’éléments avec XElement ............................................................... Création d’attributs avec XAttribute ............................................................ Création de commentaires avec XComment ..................................................... Création de conteneurs avec XContainer ....................................................... Création de déclarations avec XDeclaration ................................................. Création de types de documents avec XDocumentType .................................. Création de documents avec XDocument ......................................................... Création de noms avec XName ......................................................................... Création d’espaces de noms avec XNamespace ............................................... Création de nœuds avec XNode ........................................................................ Création d’instructions de traitement avec XProcessingInstruction ......... Création d’éléments streaming avec XStreamingElement .......................... Création de textes avec XText ......................................................................... Définition d’un objet CData avec XCData ....................................................... Sauvegarde de fichiers XML ...................................................................................... Sauvegardes avec XDocument.Save() ........................................................... Sauvegarde avec XElement.Save ................................................................... Lecture de fichiers XML ............................................................................................ Lecture avec XDocument.Load() ................................................................... Lecture avec XElement.Load() ..................................................................... Extraction avec XDocument.Parse() ou XElement.Parse() ....................... Déplacements XML .................................................................................................... Propriétés de déplacement ............................................................................... Méthodes de déplacement ...............................................................................
189 189 190 190 192 194 196 199 200 202 202 205 206 207 207 208 209 210 211 211 211 213 215 215 216 216 217 218 218 219 220 221 222 225
Partie III LINQ to XML
Linq.book Page VI Mercredi, 18. février 2009 7:58 07
VI
Table des matières
Modification de données XML ................................................................................... Ajout de nœuds ............................................................................................... Suppression de nœuds ..................................................................................... Mise à jour de nœuds ...................................................................................... XElement.SetElementValue() sur des objets enfants de XElement ............ Attributs XML ............................................................................................................ Création d’un attribut ...................................................................................... Déplacements dans un attribut ........................................................................ Modification d’attributs ................................................................................... Annotations XML ....................................................................................................... Ajout d’annotations avec XObject.AddAnnotation() .................................. Accès aux annotations avec XObject.Annotation() ou XObject.Annotations() .......................................................................... Suppression d’annotations avec XObject.RemoveAnnotations() .............. Exemples d’annotations .................................................................................. Événements XML ....................................................................................................... XObject.Changing ....................................................................................... XObject.Changed ........................................................................................ Quelques exemples d’événements .................................................................. Le bogue d’Halloween .................................................................................... Résumé .......................................................................................................................
238 238 242 245 248 250 250 250 253 258 258
8 Les opérateurs LINQ to XML..................................................................................... Introduction aux opérateurs LINQ to XML ............................................................... Opérateur Ancestors ................................................................................................. Prototypes ........................................................................................................ Exemples ......................................................................................................... Opérateur AncestorsAndSelf ................................................................................... Prototypes ........................................................................................................ Exemples ......................................................................................................... Opérateur Attributes ............................................................................................... Prototypes ........................................................................................................ Exemples ......................................................................................................... Opérateur DescendantNodes ..................................................................................... Prototype ......................................................................................................... Exemple ........................................................................................................... Opérateur DescendantNodesAndSelf ....................................................................... Prototype ......................................................................................................... Exemple ........................................................................................................... Opérateur Descendants ............................................................................................. Prototypes ........................................................................................................ Exemples ......................................................................................................... Opérateur DescendantsAndSelf ............................................................................... Prototypes ........................................................................................................ Exemples .........................................................................................................
269 270 270 270 271 274 274 275 277 277 277 279 279 279 280 280 281 282 282 282 284 284 284
258 258 259 262 262 262 263 267 267
Linq.book Page VII Mercredi, 18. février 2009 7:58 07
Table des matières
VII
Opérateur Elements ................................................................................................... Prototypes ........................................................................................................ Exemples ......................................................................................................... Opérateur InDocumentOrder ..................................................................................... Prototype ......................................................................................................... Exemple ........................................................................................................... Opérateur Nodes ......................................................................................................... Prototype ......................................................................................................... Exemple ........................................................................................................... Opérateur Remove ....................................................................................................... Prototypes ........................................................................................................ Exemples ......................................................................................................... Résumé .......................................................................................................................
287 287 287 289 289 289 290 290 291 292 292 292 294
9 Les autres possibilités de XML ................................................................................... Espaces de noms référencés ....................................................................................... Requêtes ..................................................................................................................... La description du chemin n’est pas une obligation ......................................... Une requête complexe ..................................................................................... Transformations .......................................................................................................... Transformations avec XSLT ............................................................................ Transformations avec la construction fonctionnelle ........................................ Astuces ............................................................................................................ Validation .................................................................................................................... Les méthodes d’extension ............................................................................... Prototypes ........................................................................................................ Obtention d’un schéma XML .......................................................................... Exemples ......................................................................................................... XPath .......................................................................................................................... Prototypes ........................................................................................................ Résumé .......................................................................................................................
295 295 296 296 298 303 304 306 308 314 314 314 315 317 328 328 329
Partie IV LINQ to DataSet 10 LINQ to DataSet ......................................................................................................... Référence des assemblies ........................................................................................... Espaces de noms référencés ....................................................................................... Code commun utilisé dans les exemples .................................................................... Opérateurs dédiés aux DataRow .................................................................................. Opérateur Distinct ........................................................................................ Opérateur Except ............................................................................................ Opérateur Intersect ......................................................................................
333 334 334 334 336 336 340 342
Linq.book Page VIII Mercredi, 18. février 2009 7:58 07
VIII
Table des matières
Opérateur Union .............................................................................................. Opérateur SequencialEqual .......................................................................... Opérateurs dédiés aux champs ................................................................................... Opérateur Field ........................................................................................ Opérateur SetField .................................................................................. Opérateurs dédiés aux DataTable .............................................................................. Opérateur AsEnumerable ................................................................................ Opérateur CopyToDataTable ........................................................ Résumé .......................................................................................................................
344 346 347 351 356 359 359 360 365
11 Possibilités complémentaires des DataSet................................................................ Espaces de noms référencés ....................................................................................... DataSets typés ........................................................................................................... Un exemple plus proche de la réalité .......................................................................... Résumé .......................................................................................................................
367 367 367 369 372
Partie V LINQ to SQL 12 Introduction à LINQ to SQL..................................................................................... Introduction à LINQ to SQL ...................................................................................... La classe DataContext ................................................................................... Classes d’entités .............................................................................................. Associations .................................................................................................... Détection de conflit d’accès concurrentiel ...................................................... Résolution de conflit d’accès concurrentiel .................................................... Prérequis pour exécuter les exemples ......................................................................... Obtenir la version appropriée de la base de données Northwind .................... Génération des classes d’entité de la base de données Northwind ................. Génération du fichier de mappage XML de la base de données Northwind ... Utilisation de l’API LINQ to SQL ............................................................................. IQueryable ......................................................................................................... Quelques méthodes communes .................................................................................. La méthode GetStringFromDb() ................................................................... La méthode ExecuteStatementInDb() ......................................................... Résumé .......................................................................................................................
377 378 380 381 382 383 383 383 384 384 385 386 386 386 387 388 388
13 Astuces et outils pour LINQ to SQL......................................................................... Introduction aux astuces et aux outils pour LINQ to SQL ......................................... Astuces ....................................................................................................................... La propriété DataContext.Log ...................................................................... La méthode GetChangeSet() ......................................................................... Utilisation de classes partielles ou de fichiers de mappage ............................. Utilisation de méthodes partielles ...................................................................
391 391 392 392 393 393 394
Linq.book Page IX Mercredi, 18. février 2009 7:58 07
Table des matières
IX
Outils .......................................................................................................................... SQLMetal ........................................................................................................ Le Concepteur Objet/Relationnel .................................................................... Utiliser SQLMetal et le Concepteur O/R ................................................................... Résumé .......................................................................................................................
394 394 401 414 415
14 Opérations standard sur les bases de données......................................................... Prérequis pour exécuter les exemples ......................................................................... Méthodes communes ....................................................................................... Utilisation de l’API LINQ to SQL .................................................................. Opérations standard de bases de données ................................................................... Insertions ......................................................................................................... Requêtes .......................................................................................................... Mises à jour ..................................................................................................... Suppressions .................................................................................................... Surcharger les méthodes de mise à jour des bases de données .................................. Surcharge de la méthode Insert .................................................................... Surcharge de la méthode Update .................................................................... Surcharge de la méthode Delete .................................................................... Exemple ........................................................................................................... Surcharge dans le Concepteur Objet/Relationnel ............................................ Considérations ................................................................................................. Traduction SQL .......................................................................................................... Résumé .......................................................................................................................
417 417 418 418 418 418 423 446 450 453 453 454 454 454 457 457 457 459
15 Les classes d’entité LINQ to SQL ............................................................................. Prérequis pour exécuter les exemples ......................................................................... Les classes d’entité ..................................................................................................... Création de classes d’entité ............................................................................. Schéma de fichier de mappage externe XML ................................................. Projection dans des classes d’entité/des classes de non-entité ........................ Dans une projection, préférez l’initialisation d’objet à la construction paramétrée ............................................................. Extension des classes d’entité avec des méthodes partielles ...................................... Les classes API importantes de System.Data.Linq ................................................. EntitySet ................................................................................................. EntityRef ................................................................................................. Table ......................................................................................................... IExecuteResult ............................................................................................. ISingleResult ........................................................................................ IMultipleResults ......................................................................................... Résumé .......................................................................................................................
461 461 461 462 493 494 496 499 501 502 502 504 505 506 506 508
16 La classe DataContext................................................................................................ Prérequis pour exécuter les exemples .........................................................................
509 509
Linq.book Page X Mercredi, 18. février 2009 7:58 07
X
Table des matières
Méthodes communes ....................................................................................... Utilisation de l’API LINQ to SQL .................................................................. La classe [Your]DataContext .................................................................................. La classe DataContext .............................................................................................. Principaux objectifs ......................................................................................... Datacontext() et [Your]DataContext() .................................................... SubmitChanges() ........................................................................................... DatabaseExists() ......................................................................................... CreateDatabase() ......................................................................................... DeleteDatabase() ........................................................................................ CreateMethodCallQuery() ........................................................................... ExecuteQuery() ............................................................................................ Translate() ................................................................................................... ExecuteCommand() ......................................................................................... ExecuteMethodCall() ................................................................................... GetCommand() ................................................................................................. GetChangeSet() ............................................................................................. GetTable() ..................................................................................................... Refresh() ....................................................................................................... Résumé .......................................................................................................................
509 509 510 510 513 520 532 539 540 541 542 543 546 547 549 557 558 560 562 568
17 Les conflits d’accès concurrentiels ............................................................................
571
Prérequis pour exécuter les exemples ......................................................................... Méthodes communes ....................................................................................... Utilisation de l’API LINQ to SQL .................................................................. Conflits d’accès concurrentiels ................................................................................... Contrôle d’accès concurrentiel optimiste ........................................................ Contrôle d’accès concurrentiel pessimiste ...................................................... Une approche alternative pour les middle-tier et les serveurs ......................... Résumé .......................................................................................................................
571 571 571 571 572 585 588 591
18 Informations complémentaires sur SQL ..................................................................
593
Prérequis pour exécuter les exemples ......................................................................... Utilisation de l’API LINQ to SQL .................................................................. Utilisation de l’API LINQ to XML ................................................................. Les vues d’une base de données ................................................................................. Héritage des classes d’entité ....................................................................................... Transactions ................................................................................................................ Résumé .......................................................................................................................
593 593 593 593 595 601 603
Index ...................................................................................................................................
607
Linq.book Page XI Mercredi, 18. février 2009 7:58 07
À propos de l’auteur Joseph C. Rattz Jr a commencé sa carrière de développeur en 1990, lorsqu’un ami lui a demandé de l’aide pour développer l’éditeur de texte "ANSI Master" sur un ordinateur Commodore Amiga. Un jeu de pendu (The Gallows) lui a rapidement fait suite. Après ces premiers programmes écrits en Basic compilé, Joe s’est tourné vers le langage C, à des fins de vitesse et de puissance. Il a alors développé des applications pour les magazines JumpDisk (périodique avec CD consacré aux ordinateurs Amiga) et Amiga World. Comme il développait dans une petite ville et sur une plate-forme isolée, Joe a appris toutes les "mauvaises" façons d’écrire du code. C’est en tentant de faire évoluer ses applications qu’il a pris conscience de l’importance de la maintenabilité du code. Deux ans plus tard, Joe a intégré la société Policy Management Systems en tant que programmeur pour développer une application client/serveur dans le domaine de l’assurance pour OS/2 et Presentation Manager. D’année en année, il a ajouté le C++, Unix, Java, ASP, ASP.NET, C#, HTML, DHTML et XML à sa palette de langages alors qu’il travaillait pour SCT, DocuCorp, IBM et le comité d’Atlanta pour les jeux Olympiques, CheckFree, NCR, EDS, Delta Technology, Radiant Systems et la société Genuine Parts. Joe apprécie particulièrement le développement d’interfaces utilisateurs et de programmes exécutés côté serveur. Sa phase favorite de développement est le débogage. Joe travaille actuellement pour la société Genuine Parts Company (maison mère de NAPA), dans le département Automotive Parts Group Information System, où il développe le site web Storefront. Ce site gère les stocks de NAPA et fournit un accès à leurs comptes et données à travers un réseau d’ordinateurs AS/400. Vous pouvez le contacter sur le site www.linqdev.com.
Linq.book Page XII Mercredi, 18. février 2009 7:58 07
Linq.book Page XIII Mercredi, 18. février 2009 7:58 07
Traducteur et relecteurs techniques À propos du traducteur Michel Martin est un passionné des technologies Microsoft. Nommé MVP par Microsoft depuis 2003, il anime des ateliers de formation, réalise des CD-ROM d’autoformation vidéo et a écrit plus de 250 ouvrages techniques, parmi lesquels Développez des gadgets pour Windows Vista et Windows Live (Pearson, 2007) et le Programmeur Visual Basic 2008 (Pearson, 2008). Il a récemment créé le réseau social eFriends Network, accessible à l’adresse http://www.efriendsnetwork.com.
À propos des relecteurs techniques Mitsuru Furuta est responsable technique en charge des relations développeurs chez Microsoft France. Il blogue sur http://blogs.msdn.com/mitsufu. Pierrick Gourlain est architecte logiciel. Nommé MVP par Microsoft depuis 2007, il est passionné de nouvelles technologies, plus particulièrement de LINQ, WPF, WCF, WF et des langages dynamiques. Il collabore à plusieurs projets open-source hébergés sur codeplex (http://www.codeplex.com). Matthieu Mezil est consultant formateur, nommé MVP C# par Microsoft depuis avril 2008. Passionné par .NET, il s’est spécialisé sur l’Entity Framework. Il blogue sur http://blogs.codes-sources.com/matthieu (fr) et http://msmvps.com/blogs/matthieu (en).
Linq.book Page XIV Mercredi, 18. février 2009 7:58 07
Linq.book Page 1 Mercredi, 18. février 2009 7:58 07
I LINQ et C# 2008
Linq.book Page 2 Mercredi, 18. février 2009 7:58 07
Linq.book Page 3 Mercredi, 18. février 2009 7:58 07
1 Hello LINQ Listing 1.1 : Hello Linq. using System; using System.Linq; string[] greetings = {"hello world", "hello LINQ", "hello Pearson"}; var items = from s in greetings where s.EndsWith("LINQ") select s; foreach (var item in items) Console.WriteLine(item);
INFO Le code du Listing 1.1 a été inséré dans un projet basé sur le modèle "Application Console", de Visual Studio 2008. Si cette directive n’est pas déjà présente dans le squelette de l’application, ajoutez une instruction using System.Linq pour référencer cet espace de noms.
L’exécution de ce code avec le raccourci clavier Ctrl+F5 affiche le message suivant dans la console : Hello LINQ
Un changement de paradigme Avez-vous remarqué un changement par rapport à votre style de programmation ? En tant que développeur .NET, vous n’êtes certainement pas passé à côté. À travers cet exemple trivial, une requête SQL (Structured Query Language) a été exécutée sur un
Linq.book Page 4 Mercredi, 18. février 2009 7:58 07
4
LINQ et C# 2008
Partie I
tableau de Strings1. Intéressez-vous à la clause where. Vous ne rêvez pas, j’ai bien utilisé la méthode EndsWidth sur un objet String. Vous vous demandez certainement quel est le type de cette variable. C# fait-il toujours des vérifications statiques des types ? Oui, à la compilation ! Cette prouesse est rendue possible par LINQ (Language INtegrated Query). Interrogation XML Après avoir examiné le code du Listing 1.1, ce deuxième exemple va commencer à vous faire entrevoir le potentiel mis entre les mains du développeur .NET par LINQ. En utilisant l’API LINQ to XML, le Listing 1.2 montre avec quelle facilité il est possible d’interagir et d’interroger des données XML (eXtensible Markup Language). Remarquez en particulier comment les données XML sont manipulées à travers l’objet books. Listing 1.2 : Requête XML basée sur LINQ to XML. using System; using System.Linq; using System.Xml.Linq; XElement books = XElement.Parse( @" Pro LINQ: Language Integrated Query en C# 2008 Joe Rattz Pro WF: Windows Workflow en .NET 3.0 Bruce Bukovics Pro C# 2005 et la plateforme.NET 2.0, Troisième édition Andrew Troelsen "); var titles = from book in books.Elements("book") where (string) book.Element("author") == "Joe Rattz" select book.Element("title"); foreach(var title in titles) Console.WriteLine(title.Value);
INFO Si l’assembly System.Xml.Linq.dll n’apparaît pas dans les références du projet, ajoutezla. Remarquez également la référence à l’espace de noms System.Xml.Linq.
1. L’ordre d’interrogation est inversé par rapport à une requête SQL traditionnelle. Par ailleurs, une instruction "s in" a été ajoutée pour fournir une référence à l’ensemble des éléments source. Ici, le tableau de chaînes "hello world", "hello LINQ" et "hello Pearson".
Linq.book Page 5 Mercredi, 18. février 2009 7:58 07
Chapitre 1
Hello LINQ
5
Appuyez sur Ctrl+F5 pour exécuter ce code. Voici le résultat affiché dans la console. Pro LINQ: Language Integrated Query en C# 2008
Avez-vous remarqué comment les données XML ont été découpées dans un objet de type XElement sans qu’il ait été nécessaire de définir un objet XmlDocument ? Les extensions de l’API XML sont un des avantages de LINQ to XML. Au lieu d’être centré sur les objets XmlDocument, comme le préconise le W3C Document Object Model (DOM), LINQ to XML permet au développeur d’interagir à tous les niveaux du document en utilisant la classe XElement. INFO Outre ses possibilités d’interrogation, LINQ to XML fournit également une interface de travail XML plus puissante et plus facile à utiliser.
Notez également que la même syntaxe SQL est utilisée pour interroger les données XML, comme s’il s’agissait d’une base de données. Interrogation d’une base de données SQL Server Ce nouvel exemple montre comment utiliser LINQ to SQL pour interroger des tables dans des bases de données. Le Listing 1.3 interroge la base de données exemple Microsoft Northwind. Listing 1.3 : Une simple interrogation de base de données basée sur une requête LINQ to SQL. using System; using System.Linq; using System.Data.Linq; using nwind; Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); var custs = from c in db.Customers where c.City == "Rio de Janeiro" select c; foreach (var cust in custs) Console.WriteLine("{0}", cust.CompanyName);
INFO Ce code fait référence à l’assembly System.Data.Linq.dll. Si cette assembly n’est pas spécifiée dans les premières lignes du listing, ajoutez-la. Notez qu’il est également fait référence à l’espace de noms System.Data.Linq.
Linq.book Page 6 Mercredi, 18. février 2009 7:58 07
6
LINQ et C# 2008
Partie I
Pour que cet exemple fonctionne, il est nécessaire de faire appel à l’utilitaire en ligne de commande SQLMetal ou au concepteur d’objets relationnels, afin de générer des classes d’entités qui pointent vers la base de données Northwind. Reportez-vous au Chapitre 12 pour en savoir plus sur l’utilisation de SQLMetal. Les classes d’entités de cet exemple faisant partie de l’espace de noms nwind, la clause using nwind; a été utilisée en début de listing pour y faire référence. INFO Il se peut que vous deviez changer la chaîne de connexion passée au constructeur Northwind dans ce listing. Reportez-vous aux sections relatives à DataContext() et [Your]DataContext() du Chapitre 16 pour prendre connaissance des différents modes de connexion possibles.
Appuyez sur Ctrl+F5 pour exécuter ce code. Le résultat ci-après devrait s’afficher dans la console : Hanari Carnes Que Delícia Ricardo Adocicados
Cet exemple utilise la table Customers de la base de données Northwind. Il se contente de sélectionner les clients qui résident à Rio de Janeiro. À première vue, il n’y a rien de nouveau ou de différent dans ce code. Vous remarquerez pourtant que la requête est intégrée dans le code. Les fonctionnalités de l’éditeur sont donc également accessibles au niveau de la requête ; en particulier la vérification de la syntaxe et l’Intellisense. L’écriture "à l’aveuglette" des requêtes et la détection des erreurs à l’exécution font donc bel et bien partie du passé ! Vous voulez baser une clause where sur un champ de la table Customers, mais vous n’arrivez pas à vous rappeler le nom des champs ? Intellisense affichera les noms des champs et vous n’aurez plus qu’à choisir dans la liste. Dans l’exemple précédent, il suffit de taper c. pour qu’Intellisense liste tous les champs de la table Customers. Vous verrez au Chapitre 2 que les requêtes LINQ peuvent utiliser deux syntaxes : la syntaxe "à point" object.method(), traditionnelle dans le langage C#, et une nouvelle syntaxe propre à LINQ. Les requêtes présentées jusqu’ici utilisent cette nouvelle syntaxe mais, bien entendu, vous pouvez continuer à utiliser la syntaxe traditionnelle.
Introduction La plate-forme .NET et les langages qui l’accompagnent (C# et VB) sont aujourd’hui éprouvés. Cependant, il reste un point douloureux pour les développeurs : l’accès aux sources de données. La manipulation de bases de données et de code XML se révèle généralement lourde et parfois problématique.
Linq.book Page 7 Mercredi, 18. février 2009 7:58 07
Chapitre 1
Hello LINQ
7
Les problèmes rencontrés dans la manipulation des bases de données sont multiples. Pour commencer, le langage n’est pas en mesure d’interagir avec les données au niveau natif. Cela signifie que, fréquemment, les erreurs de syntaxe ne sont pas détectées jusqu’à l’exécution. De même, les champs incorrectement référencés ne sont pas détectés. De telles erreurs peuvent être désastreuses, en particulier si elles se produisent pendant l’exécution d’une routine de gestion d’erreurs. Rien n’est plus frustrant qu’un mécanisme de gestion d’erreurs mis en échec à cause d’une erreur syntaxique qui n’a jamais été détectée ! Un autre problème peut provenir d’une différence entre les types des données stockés dans une base de données ou dans des éléments XML, par exemple, et les types gérés par le langage de programmation. Les données date et heure sont en particulier concernées. L’extraction, l’itération et la manipulation de données XML risquent également d’être très fastidieuses. Souvent, alors qu’un simple fragment XML doit être manipulé, il est nécessaire de créer un XmlDocument pour se conformer à l’API W3C DOM XML. Au lieu d’ajouter de nouvelles classes et méthodes pour pallier ces déficiences, les ingénieurs de Microsoft ont décidé d’aller plus loin en modifiant la syntaxe des requêtes d’interrogation. C’est ainsi que LINQ a vu le jour. Cette technologie, directement accessible dans les langages de programmation, permet d’interroger tous types de données, des tableaux mémoire aux collections en passant par les bases de données, les documents XML et bien d’autres ensembles de données. LINQ et l’interrogation des données LINQ est essentiellement un langage d’interrogation. Il peut retourner un ensemble d’objets, un objet unique ou un sous-ensemble de champs appartenant à un objet ou à un ensemble d’objets. Cet ensemble d’objets est appelé une "séquence". La plupart des séquences LINQ sont de type IEnumerable, où T est le type des objets stockés dans la séquence. Par exemple, une séquence d’entiers est stockée dans une variable de type IEnumerable. Comme vous le verrez dans la suite du livre, la plupart des méthodes LINQ retournent un IEnumerable. Dans les exemples étudiés jusqu’ici, toutes les requêtes ont retourné un IEnumerable ou un type hérité. Le mot-clé "var" a parfois été utilisé par souci de simplification. Vous verrez au Chapitre 2 qu’il s’agit d’un raccourci d’écriture. Composants La puissance et l’universalité de LINQ devraient le faire adopter dans de nombreux domaines. En fait, tous les types de données stockés sont de bons candidats aux requêtes LINQ. Ceci concerne les bases de données, Active Directory, le Registre de Windows, le système de fichiers, les feuilles de calcul Excel, etc.
Linq.book Page 8 Mercredi, 18. février 2009 7:58 07
8
LINQ et C# 2008
Partie I
Microsoft a défini plusieurs domaines de prédilection pour LINQ. Il ne fait aucun doute que cette liste sera complétée par la suite. LINQ to Objects LINQ to Objects est le nom donné à l’API IEnumerable pour les opérateurs de requête standard. Vous l’utiliserez par exemple pour requêter des tableaux et des collections de données en mémoire. Les opérateurs de requête standard LINQ to Objects sont les méthodes statiques de la classe System.Linq.Enumerable. LINQ to XML LINQ to XML est le nom de l’API dédiée au travail sur les données XML (cette interface était précédemment appelée XLINQ). LINQ to XML ne se contente pas de définir des librairies XML afin d’assurer la compatibilité avec LINQ. Il apporte également une solution à plusieurs déficiences du standard XML DOM et facilite le travail avec les données XML. À titre d’exemple, il n’est désormais plus nécessaire de créer un XmlDocument pour traiter une portion réduite de XML. Qui s’en plaindra ? Pour pouvoir travailler avec LINQ to XML, vous devez faire référence à l’assembly System.Xml.Linq.dll dans votre projet : using System.Xml.Linq;
LINQ to DataSet LINQ to DataSet est le nom de l’API permettant de travailler avec des DataSets. De nombreux développeurs utilisent ces types d’objets. Sans qu’aucune réécriture de code ne soit nécessaire, ils pourront désormais tirer avantage de la puissance de LINQ pour interroger leurs DataSets. LINQ to SQL LINQ to SQL est le nom de l’API IQueryable, qui permet d’appliquer des requêtes LINQ aux bases de données Microsoft SQL Server (cette interface était précédemment connue sous le nom DLinq). Pour pouvoir utiliser LINQ to SQL, vous devez faire référence à l’assembly System.Data.Linq.dll : using System.Data.Linq;
LINQ to Entities LINQ to Entities est une API alternative utilisée pour interfacer des bases de données. Elle découple le modèle objet entity de la base de données elle-même en ajoutant un mappage logique entre les deux. Ce découplage procure une puissance et une flexibilité accrues. Étant donné que LINQ to Entities ne fait pas partie du framework LINQ, nous ne nous y intéresserons pas dans cet ouvrage. Cependant, si LINQ to SQL ne vous
Linq.book Page 9 Mercredi, 18. février 2009 7:58 07
Chapitre 1
Hello LINQ
9
semble pas assez flexible, vous devriez vous intéresser à LINQ to Entities ; en particulier si vous avez besoin d’une plus grande souplesse entre les entités et la base de données, si vous manipulez des données provenant de plusieurs tables ou si vous voulez personnaliser la modélisation des entités. Comment travailler avec LINQ Il n’existe aucun produit LINQ à acheter ou à installer : c’est juste le nom qui a été donné à l’outil d’interrogation de C# 3.0 et au Framework .NET 3.5, apparu dans Visual Studio 2008. Pour obtenir des informations à jour sur LINQ et Visual Studio 2008, connectezvous sur les pages www.linqdev.com et http://apress.com/book/bookDisplay .html?bID=10241.
LINQ ne se limite pas aux requêtes LINQ étant l’abréviation de Language INtegrated Query (langage d’interrogation intégré), vous pourriez penser qu’il se limite à l’interrogation de données. Comme vous le verrez dans la suite du livre, son domaine d’action va beaucoup plus loin... Vous est-il déjà arrivé de devoir remanier les données renvoyées par une méthode avant de pouvoir les passer en argument à une autre méthode ? Supposons par exemple que vous appeliez la méthode A. Cette méthode retourne un tableau de string contenant des valeurs numériques stockées en tant que chaînes de caractères. Vous devez alors appeler une méthode B qui demande un tableau d’entiers en entrée. Puis mettre en place une boucle pour convertir un à un les éléments du tableau. Quelle plaie ! LINQ apporte une réponse élégante à ce problème. Supposons que nous ayons un tableau de string reçu d’une méthode A, comme indiqué dans le Listing 1.4. Listing 1.4 : Une requête XML basée sur LINQ to XML. string[] numbers = { "0042", "010", "9", "27" };
Dans cet exemple, le tableau de string a été déclaré de façon statique. Avant d’appeler la méthode B, il est nécessaire de convertir ce tableau de chaînes en un tableau d’entiers : int[] nums = numbers.Select(s => Int32.Parse(s)).ToArray();
Cette conversion pourrait-elle être plus simple ? Voici le code à utiliser pour afficher le tableau d’entiers nums : foreach(int num in nums) Console.WriteLine(num);
Linq.book Page 10 Mercredi, 18. février 2009 7:58 07
10
LINQ et C# 2008
Partie I
Et voici l’affichage résultant dans la console : 42 10 9 27
Peut-être pensez-vous que cette conversion s’est contentée de supprimer les zéros devant les nombres. Pour nous en assurer, nous allons trier les données numériques. Si tel est le cas, 9 sera affiché en dernier et 10, en premier. Le Listing 1.5 effectue la conversion et le tri des données. Listing 1.5 : Conversion d’un tableau de chaînes en entiers et tri croissant. string[] numbers = { "0042", "010", "9", "27" }; int[] nums = numbers.Select(s => Int32.Parse(s)).OrderBy(s => s).ToArray(); foreach(int num in nums) Console.WriteLine(num);
Voici le résultat : 9 10 27 42
Cela fonctionne, mais il faut bien avouer que cet exemple est simpliste. Nous allons maintenant nous intéresser à des données plus complexes. Supposons que nous disposions de la classe Employee et qu’une de ses méthodes retourne le nom des employés. Supposons également que nous disposions d’une classe Contact et qu’une de ses méthodes liste les contacts d’un des employés. Supposons enfin que vous souhaitiez obtenir la liste des contacts de chacun des employés. La tâche semble assez simple. Cependant, la méthode qui retourne le nom des employés fournit un ArrayList d’objets Employee, et la méthode qui liste les contacts nécessite un tableau de type Contact. Voici le code des classes Employee et Contact : namespace LINQDev.HR { public class Employee { public int id; public string firstName; public string lastName; public static ArrayList GetEmployees() { // Le "vrai" code ferait certainement une requête // sur une base de données à ce point précis ArrayList al = new ArrayList();
Linq.book Page 11 Mercredi, 18. février 2009 7:58 07
Chapitre 1
Hello LINQ
11
// Ajout des données dans le tableau ArrayList al al.Add(new Employee { id = 1, firstName = "Joe", lastName = "Rattz"} ); al.Add(new Employee { id = 2, firstName = "William", lastName = "Gates"} ); al.Add(new Employee { id = 3, firstName = "Anders", lastName = "Hejlsberg"} ); return(al); } } } namespace LINQDev.Common { public class Contact { public int Id; public string Name; public static void PublishContacts(Contact[] contacts) { // Cette méthode se contente d’afficher les contacts dans la console foreach(Contact c in contacts) Console.WriteLine("Contact Id: {0} Contact: {1}", c.Id, c.Name); } } }
Comme vous pouvez le voir, la classe Employee et la méthode GetEmployee sont dans l’espace de noms LINQDev.HR, et la méthode GetEmployees retourne un ArrayList. Quant à la méthode PublishContacts, elle se trouve dans l’espace de noms LINQDev.Common et demande un tableau d’objets Contact en entrée. Avant l’arrivée de LINQ, vous auriez dû passer en revue les ArrayList retournés par la méthode GetEmployees et créer un nouveau tableau de type Contact afin d’assurer la compatibilité avec la méthode PublishContacts. Comme le montre le Listing 1.6, LINQ facilite grandement les choses. Listing 1.6 : Appel des méthodes GetEmployees et PublishContacts. ArrayList alEmployees = LINQDev.HR.Employee.GetEmployees(); LINQDev.Common.Contact[] contacts = alEmployees .Cast() .Select(e => new LINQDev.Common.Contact { Id = e.id, Name = string.Format("{0} {1}", e.firstName, e.lastName) }) .ToArray(); LINQDev.Common.Contact.PublishContacts(contacts);
Pour convertir le tableau ArrayList d’objets Employee en un tableau d’objets Contact, nous l’avons transformé en une séquence IEnumerable en utilisant l’opérateur de requête standard Cast. Cette transformation est nécessaire car une collection héritée ArrayList est renvoyée par GetEmployees. Syntaxiquement parlant, ce sont les objets de la classe System.Object et non ceux de la classe Employee qui sont stockés dans l’ArrayList. Le casting vers des objets Employee est donc nécessaire. Si la méthode GetEmployees avait renvoyé une collection générique List, cette étape n’aurait pas été nécessaire. Malheureusement, ce type de collection n’était pas disponible lors de l’écriture de ce code hérité.
Linq.book Page 12 Mercredi, 18. février 2009 7:58 07
12
LINQ et C# 2008
Partie I
Le casting terminé, l’opérateur Select est appliqué sur la séquence d’objets Employee. Dans l’expression lambda (le code passé comme argument de la méthode Select), un objet Contact est instancié et initialisé en utilisant les valeurs retournées par les objets Employee (vous en saurez plus en consultant la section réservées aux méthodes anonymes au Chapitre 2). Pour terminer, la séquence d’objets Contact est convertie en un tableau d’objets Contact en utilisant l’opérateur ToArray. Ceci afin d’assurer la compatibilité avec la méthode PublishContacts. Voici le résultat affiché dans la console : Contact Id: 1 Contact: Joe Rattz Contact Id: 2 Contact: William Gates Contact Id: 3 Contact: Anders Hejlsberg
J’espère que vous êtes maintenant convaincu que LINQ ne se limite pas à l’interrogation de données. En parcourant les autres chapitres de ce livre, essayez de trouver de nouveaux champs d’application de LINQ.
Quelques conseils avant de commencer Pendant l’écriture de cet ouvrage, j’ai parfois été troublé, embrouillé, voire bloqué alors que j’expérimentais LINQ. Pour vous éviter de tomber dans les mêmes pièges, je vais vous donner quelques conseils. Tous les concepts propres à LINQ n’ayant pas encore été introduits, il serait logique que ces conseils figurent à la fin de l’ouvrage. Rassurez-vous : je ne vais pas vous imposer la lecture complète de l’ouvrage ! Mais ne vous formalisez pas si vous ne comprenez pas entièrement ce qui va être dit dans les pages suivantes… Utilisez le mot-clé var si vous n’êtes pas à l’aise Il n’est pas nécessaire d’utiliser le mot-clé var lorsque vous affectez une séquence de classes anonymes à une variable, mais cela peut vous aider à passer l’étape de la compilation, en particulier si vous ne savez pas exactement quel type de données vous êtes en train de manipuler. Bien entendu, il est préférable de connaître le type des données T des IEnumerable mais, parfois, en particulier lorsque vous commencez en programmation LINQ, cela peut se révéler difficile. Si le code ne veut pas se compiler à cause d’une incompatibilité dans un type de données, pensez à transformer ce type en utilisant le mot-clé var. Supposons que vous ayez le code suivant : // Ce code produit une erreur à la compilation Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IEnumerable orders = db.Customers .Where(c => c.Country == "USA" && c.Region == "WA") .SelectMany(c => c.Orders);
Linq.book Page 13 Mercredi, 18. février 2009 7:58 07
Chapitre 1
Hello LINQ
13
Il se peut que vous ne sachiez pas exactement quel est le type des données de la séquence d’IEnumerable. Une astuce bien pratique consiste à affecter le résultat de la requête à une variable dont le type est spécifié automatiquement grâce au mot-clé var, puis à obtenir son type grâce à la méthode GetType (voir Listing 1.7). Listing 1.7 : Un exemple de code qui utilise le mot-clé var. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); var orders = db.Customers .Where(c => c.Country == "USA" && c.Region == "WA") .SelectMany(c => c.Orders); Console.WriteLine(orders.GetType());
Dans cet exemple, le type de la variable orders est spécifié par l’intermédiaire du motclé var. Voici le type affiché dans la console : System.Data.Linq.DataQuery`1[nwind.Order]
Dans tout le charabia retourné par le compilateur, nwind.Order est certainement la partie la plus importante, puisqu’elle indique le type de la séquence. Si l’expression affichée dans la console vous intrigue, exécutez l’exemple dans le débogueur et examinez la variable orders dans la fenêtre Espion Express. Son type est le suivant : System.Linq.IQueryable {System.Data.Linq.DataQuery}
La séquence est donc de type nwind.Order. Il s’agit en fait d’un IQueryable, mais vous pouvez l’affecter à un IEnumerable, puisque IQueryable hérite de IEnumerable. Vous pouvez donc réécrire le code précédent et passer en revue les résultats en utilisant les instructions du Listing 1.8. Listing 1.8 : Le même code que dans le Listing 1.7, sauf au niveau des codes explicites. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IEnumerable orders = db.Customers .Where(c => c.Country == "USA" && c.Region == "WA") .SelectMany(c => c.Orders); foreach(Order item in orders) Console.WriteLine("{0} - {1} - {2}", item.OrderDate, item.OrderID, item.ShipName);
INFO Pour que ce code fonctionne, vous devez spécifier une directive using pour les espaces de noms System.Collections.Generic et System.Linq (ce deuxième espace de noms est obligatoire dès que vous utilisez des instructions en rapport avec LINQ).
Linq.book Page 14 Mercredi, 18. février 2009 7:58 07
14
LINQ et C# 2008
Partie I
Ce code produit le résultat suivant : 3/21/1997 12:00:00 AM - 10482 - Lazy K Kountry Store 5/22/1997 12:00:00 AM - 10545 - Lazy K Kountry Store … 4/17/1998 12:00:00 AM - 11032 - White Clover Markets 5/1/1998 12:00:00 AM - 11066 - White Clover Markets
Utilisez les opérateurs Cast ou OfType pour les collections héritées La grande majorité des opérateurs de requête LINQ ne peut être utilisée que sur des collections qui implémentent l’interface IEnumerable. Aucune des collections héritées de C# (celles présentes dans l’espace de noms System.Collection) n’implémente cette interface. Mais, alors, comment utiliser LINQ avec des collections héritées ? Deux opérateurs de requête standard sont là pour convertir des collections héritées en séquences IEnumerable : Cast et OfType (voir Listing 1.9). Listing 1.9 : Conversion d’une collection héritée en un IEnumerable avec l’opérateur Cast. // Création d’une collection héritée ArrayList arrayList = new ArrayList(); // L’initialisation de collections ne fonctionne pas // avec les collections héritées arrayList.Add("Adams"); arrayList.Add("Arthur"); arrayList.Add("Buchanan"); IEnumerable names = arrayList.Cast().Where(n => n.Length < 7); foreach(string name in names) Console.WriteLine(name);
Le Listing 1.10 représente le même exemple, en utilisant cette fois-ci l’opérateur OfType. Listing 1.10 : Utilisation de l’opérateur OfType. // Création d’une collection héritée ArrayList arrayList = new ArrayList(); // L’initialisation de collections ne fonctionne pas // avec les collections héritées arrayList.Add("Adams"); arrayList.Add("Arthur"); arrayList.Add("Buchanan"); IEnumerable names = arrayList.OfType().Where(n => n.Length < 7); foreach(string name in names) Console.WriteLine(name);
Ces deux exemples produisent le même résultat : Adams Arthur
Ces deux opérateurs sont quelque peu différents : Cast essaye de convertir tous les éléments de la collection dans le type spécifié. Une exception est générée si un des
Linq.book Page 15 Mercredi, 18. février 2009 7:58 07
Chapitre 1
Hello LINQ
15
éléments ne peut pas être converti. Au contraire, OfType ne convertit que les éléments qui peuvent l’être. Préférez l’opérateur OfType à l’opérateur Cast Les génériques ont été implémentés dans C# pour permettre une vérification de type statique (c’est-à-dire pendant la compilation) sur les collections. Avant l’apparition des génériques, il n’y avait aucun moyen de s’assurer que les éléments d’une collection héritée (un ArrayList ou un Hashtable, par exemple) étaient tous de même type et avaient le type requis. Rien par exemple n’empêchait l’insertion d’un objet Textbox dans un ArrayList supposé ne contenir que des objets Label. Avec l’apparition des génériques dans C# 2.0, les développeurs peuvent désormais s’assurer qu’une collection ne contient que des éléments dont le type est spécifié. Bien que les opérateurs OfType et Cast soient utilisables sur une collection héritée, Cast nécessite que tous les objets de la collection aient le type attendu. Pour éviter de générer des exceptions en cas d’incompatibilité de type, préférez-lui l’opérateur OfType. Par son intermédiaire, seuls les objets du type spécifié seront stockés dans la séquence IEnumerable, et aucune exception ne sera générée. Le cas échéant, les objets dont le type n’est pas celui attendu ne seront pas convertis. Les requêtes aussi peuvent être boguées Au Chapitre 3, vous verrez que les requêtes LINQ sont souvent différées. Elles ne sont donc pas exécutées dès leur invocation. Considérez par exemple le code suivant, extrait du Listing 1.1 : var items = from s in greetings where s.EndsWith("LINQ") select s; foreach (var item in items) Console.WriteLine(item);
Contrairement à ce que vous pourriez penser, la requête n’est pas exécutée à l’initialisation de la variable items. Elle ne sera exécutée que lorsqu’une ligne de code aura besoin de son résultat ; typiquement lors de l’énumération du résultat de la requête. Ici, le résultat de la requête n’est pas calculé jusqu’à ce que l’instruction foreach soit exécutée. On oublie souvent que l’exécution d’une requête est différée jusqu’à l’énumération de sa séquence. Une requête mal formulée pourrait ainsi produire une erreur bien des lignes plus loin, lorsque sa séquence est énumérée, et le programmeur pourrait avoir du mal à penser que la requête en est l’origine. Examinons le code du Listing 1.11.
Linq.book Page 16 Mercredi, 18. février 2009 7:58 07
16
LINQ et C# 2008
Partie I
Listing 1.11 : Cette requête contient une erreur intentionnelle qui n’est levée qu’à l’énumération. string[] strings = { "un", "deux", null, "trois" }; Console.WriteLine("Avant l’appel à Where()"); IEnumerable ieStrings = strings.Where(s => s.Length == 3); Console.WriteLine("Après l’appel à Where()"); foreach(string s in ieStrings) { Console.WriteLine("Traitement " + s); }
Le troisième élément du tableau a pour valeur null. L’expression null.Length va produire une exception lors de l’énumération de la séquence ieStrings, et en particulier de son troisième élément. Pourtant, la ligne à l’origine de l’erreur est allègrement passée… Voici le résultat obtenu à l’exécution de ce code : Avant l’appel à Where() Après l’appel à Where() Traitement un Traitement deux Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object. …
L’opérateur Where n’a pas produit d’exception. L’exception a seulement été levée lorsque l’on a essayé de lire le troisième élément de la séquence. Imaginez que la séquence ieStrings soit passée à une fonction qui énumère la séquence dans une liste déroulante ou un contrôle équivalent. Penseriez-vous que l’exception provient de la requête LINQ ? Il y a de grandes chances pour que vous cherchiez l’erreur dans le code de la fonction… Sachez tirer parti des requêtes différées Au Chapitre 3, vous en apprendrez bien plus sur les requêtes différées. Cependant, je voudrais dès à présent insister sur le fait que, si une requête différée retourne un IEnumerable, cet objet peut être énuméré autant de fois que nécessaire sans pour autant devoir rappeler la requête. La plupart des codes de cet ouvrage appellent une requête et stockent l’ IEnumerable retourné dans une variable. Une instruction foreach est alors appliquée sur la séquence IEnumerable à des fins démonstratives. Si ce code est exécuté à plusieurs reprises, il n’est pas nécessaire de rappeler la requête à chaque exécution. Il serait plus judicieux d’écrire une méthode d’initialisation et d’y placer toutes les requêtes nécessaires. Cette méthode serait appelée une fois. Vous pourriez alors énumérer la séquence de votre choix pour obtenir la dernière version des résultats.
Linq.book Page 17 Mercredi, 18. février 2009 7:58 07
Chapitre 1
Hello LINQ
17
Utiliser le log du DataContext Lorsque vous travaillerez avec LINQ to SQL, vous devrez garder à l’esprit que la classe relative à la base de données, générée par SQLMetal, hérite de System.Data.Linq.DataContext. Cette classe dispose donc de quelques fonctionnalités préinstallées. Entre autres de l’objet TextWriter Log. Si vous avez déjà expérimenté une rupture de code liée aux données, vous serez ravi d’apprendre qu’il est possible d’utiliser l’objet Log du DataContext pour observer les données résultant de la requête, tout comme vous le feriez dans SQL Server Enterprise Manager ou Query Analyzer (voir l’exemple du Listing 1.12). Listing 1.12 : Un exemple d’utilisation du log du DataContext. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); db.Log = Console.Out; IQueryable orders = from c in db.Customers from o in c.Orders where c.Country == "USA" && c.Region == "WA" select o; foreach(Order item in orders) Console.WriteLine("{0} - {1} - {2}", item.OrderDate, item.OrderID, item.ShipName);
Ce code produit la sortie suivante dans la console : SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate], [t1].[RequiredDate], [t1].[ShippedDate], [t1].[ShipVia], [t1].[Freight], [t1].[ShipName], [t1].[ShipAddress], [t1].[ShipCity], [t1].[ShipRegion], [t1].[ShipPostalCode], [t1].[ShipCountry] FROM [dbo].[Customers] AS [t0], [dbo].[Orders] AS [t1] WHERE ([t0].[Country] = @p0) AND ([t0].[Region] = @p1) AND ([t1].[CustomerID] = [t0].[CustomerID]) -- @p0: Input String (Size = 3; Prec = 0; Scale = 0) [USA] -- @p1: Input String (Size = 2; Prec = 0; Scale = 0) [WA] -- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 3/21/1997 12:00:00 AM - 10482 - Lazy K Kountry Store 5/22/1997 12:00:00 AM - 10545 - Lazy K Kountry Store 6/19/1997 12:00:00 AM - 10574 - Trail’s Head Gourmet Provisioners 6/23/1997 12:00:00 AM - 10577 - Trail’s Head Gourmet Provisioners 1/8/1998 12:00:00 AM - 10822 - Trail’s Head Gourmet Provisioners 7/31/1996 12:00:00 AM - 10269 - White Clover Markets 11/1/1996 12:00:00 AM - 10344 - White Clover Markets 3/10/1997 12:00:00 AM - 10469 - White Clover Markets 3/24/1997 12:00:00 AM - 10483 - White Clover Markets 4/11/1997 12:00:00 AM - 10504 - White Clover Markets 7/11/1997 12:00:00 AM - 10596 - White Clover Markets 10/6/1997 12:00:00 AM - 10693 - White Clover Markets 10/8/1997 12:00:00 AM - 10696 - White Clover Markets 10/30/1997 12:00:00 AM - 10723 - White Clover Markets 11/13/1997 12:00:00 AM - 10740 - White Clover Markets 1/30/1998 12:00:00 AM - 10861 - White Clover Markets 2/24/1998 12:00:00 AM - 10904 - White Clover Markets 4/17/1998 12:00:00 AM - 11032 - White Clover Markets 5/1/1998 12:00:00 AM - 11066 - White Clover Markets
Linq.book Page 18 Mercredi, 18. février 2009 7:58 07
18
LINQ et C# 2008
Partie I
Utilisez le forum LINQ Il y a fort à parier que, tôt ou tard, vous vous retrouverez dans une situation bloquante en expérimentant LINQ. N’hésitez pas à faire appel au forum dédié à LINQ sur MSDN.com, en vous connectant à l’adresse www.linqdev.com. Ce forum est suivi par les développeurs Microsoft. Vous y trouverez de nombreuses ressources très intéressantes.
Résumé Je sens que vous êtes impatient de passer au chapitre suivant. Je voudrais cependant vous rappeler quelques petites choses avant que vous ne tourniez les pages. LINQ va changer la façon dont les développeurs .NET interrogent leurs données. Les éditeurs de logiciels vont certainement ajouter un sticker "Compatible LINQ" sur leurs produits, tout comme ils le font actuellement avec XML. Gardez bien en mémoire que LINQ n’est pas juste une nouvelle librairie que vous ajoutez à vos projets. Il s’agit d’une tout autre approche pour interroger vos données, consistant en plusieurs composants qui dépendent de la source de données à interroger. Alors que nous écrivons ces lignes, vous pouvez utiliser LINQ pour interroger des collections de données en mémoire avec LINQ to Objects, des fichiers XML avec LINQ to SQL, des DataSets avec LINQ to DataSets et des bases de données SQL Server avec LINQ to SQL. Rappelez-vous également que LINQ n’est pas simplement un langage de requête. Dans un de mes projets, j’ai utilisé LINQ avec succès non seulement pour interroger des sources de données, mais également pour modifier le format des données afin de les présenter dans une fenêtre WinForm. Enfin, j’espère que vous tiendrez compte des astuces que j’ai mentionnées à la fin de ce chapitre. Si vous ne comprenez pas entièrement certaines d’entre elles, ce n’est pas un problème. Vous en saisirez toutes les subtilités au fur et à mesure de votre progression dans le livre. Stockez-les dans un coin de votre tête : elles vous feront gagner du temps. Après vous être intéressé aux exemples et conseils de ce chapitre, vous êtes peut-être perplexe devant la syntaxe de LINQ. Ne vous en faites pas, au prochain chapitre vous allez découvrir en détail toutes les modifications apportées au langage C# 3.0 par Microsoft et comprendrez plus facilement le code.
Linq.book Page 19 Mercredi, 18. février 2009 7:58 07
2 Améliorations de C# 3.0 pour LINQ Le chapitre précédent vous a initié au monde merveilleux de LINQ. J’y ai donné quelques exemples pour attiser votre appétit et des astuces qui pourront vous paraître quelque peu prématurées. Certaines syntaxes vous laissent peut-être perplexe, car le code revêt un aspect entièrement nouveau. C# a en effet dû être remanié pour supporter les fonctionnalités avancées de LINQ. Dans ce chapitre, vous allez découvrir les facettes les plus innovantes de C# 3.0.
Les nouveautés du langage C# 3.0 Pour que LINQ s’intègre parfaitement dans C#, des améliorations significatives ont dû être apportées au langage. Toutes les améliorations déterminantes ont été dictées par le support de LINQ. Bien que chacune d’entre elles soit intéressante en tant que telle, c’est l’ensemble qui fait de C# 3.0 un langage si puissant. Pour bien comprendre la syntaxe de LINQ, vous devez au préalable vous intéresser à certaines nouvelles fonctionnalités de C# 3.0. Ce chapitre va passer en revue les nouveautés suivantes : m
les expressions lambda ;
m
les arbres d’expressions ;
m
le mot-clé var, l’initialisation des objets et des collections et les types anonymes ;
m
les méthodes d’extension ;
m
les méthodes partielles ;
m
les expressions de requête.
Linq.book Page 20 Mercredi, 18. février 2009 7:58 07
20
LINQ et C# 2008
Partie I
Les assemblies et espaces de noms nécessaires à la bonne exécution des exemples de ce chapitre ne seront pas mentionnés s’ils ont déjà été utilisés au Chapitre 1. En revanche, les nouveaux assemblies et espaces de noms seront signalés lors de leur première utilisation. Les expressions lambda Bien qu’inventées en 1936 par le mathématicien américain Alonzo Church et utilisées dans des langages aussi anciens que LISP, les expressions lambda sont une nouveauté du langage C# 3.0. Leur but premier vise à simplifier la syntaxe des algorithmes. Avant de nous intéresser aux expressions lambda, nous allons nous attarder quelques instants sur la possibilité de passer un algorithme dans un argument d’une méthode. Utilisation de méthodes nommées Avant la sortie de C# 2.0, lorsqu’une méthode/une variable avait besoin d’un délégué, le développeur devait créer une méthode nommée et passer ce nom à chaque utilisation du délégué.
Supposons que deux développeurs travaillent sur un même projet. Le développeur numéro 1 crée un code réutilisable et le développeur numéro 2 utilise ce code pour créer une application. Supposons que le développeur 1 définisse une méthode générique permettant de filtrer des tableaux d’entiers, en permettant de spécifier l’algorithme de tri à utiliser. Dans un premier temps, il crée un délégué qui reçoit un entier et retourne la valeur true si la valeur passée peut être incluse dans le tableau. Ainsi, il créé une classe utilitaire et ajoute le délégué et la méthode de filtre. Voici le code utilisé : public class Common { public delegate bool IntFilter(int i); public static int[] FilterArrayOfInts(int[] ints, IntFilter filter) { ArrayList aList = new ArrayList(); foreach (int i in ints) { if (filter(i)) { aList.Add(i); } } return ((int[])aList.ToArray(typeof(int))); } }
Le développeur numéro 1 a placé le délégué et la méthode FilterArrayOfInt() dans une DLL (Dynamic Link Library) afin de les rendre accessibles dans plusieurs applications.
Linq.book Page 21 Mercredi, 18. février 2009 7:58 07
Chapitre 2
Améliorations de C# 3.0 pour LINQ
21
La méthode FilterArrayOfInt() du listing précédent admet deux paramètres en entrée : le tableau à trier et un délégué qui fait référence à la méthode de tri à utiliser. Le tableau d’entiers trié est renvoyé par la méthode. Supposons maintenant que le développeur numéro 2 veuille limiter le tri aux entiers impairs. Voici la méthode de tri utilisée : public class Application { public static bool IsOdd(int i) { return ((i & 1) == 1); } }
En se basant sur le code de la méthode FilterArrayOfInts, la méthode IsOdd sera appelée pour tous les entiers du tableau qui lui seront passés. Ce filtre ne retournera la valeur true que dans le cas où l’entier passé est impair. Le Listing 2.1 donne un exemple d’utilisation de la méthode FilterArrayOfInts. Listing 2.1 : Appel de la méthode commune FilterArrayOfInts. using System.Collections; int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int[] oddNums = Common.FilterArrayOfInts(nums, Application.IsOdd); foreach (int i in oddNums) Console.WriteLine(i);
Voici le résultat : 1 3 5 7
Comme vous pouvez le remarquer, pour passer le délégué dans le second paramètre de la méthode FilterArrayOfInts, il suffit d’indiquer son nom. En définissant un autre filtre, le résultat peut être tout autre. Il est ainsi possible de définir un filtre pour les nombres pairs, pour les nombres premiers ou pour un tout autre critère. Les délégués sont intéressants chaque fois que le code doit être utilisé à plusieurs reprises. Utiliser des méthodes anonymes Cet exemple fonctionne à la perfection, mais à la longue il peut être fastidieux d’écrire tous les filtres et autres délégués dont vous avez besoin : la plupart de ces méthodes seront appelées une seule fois et il peut être frustrant de créer autant de méthodes que de tris nécessaires. Depuis C# 2.0, les développeurs peuvent faire appel aux méthodes anonymes, afin de passer du code comme argument et ainsi d’éviter l’utilisation de délégués.
Linq.book Page 22 Mercredi, 18. février 2009 7:58 07
22
LINQ et C# 2008
Partie I
Dans cet exemple, plutôt que créer la méthode IsOdd, le code de filtrage est passé dans l’argument (voir Listing 2.2). Listing 2.2 : Appel du filtre par l’intermédiaire d’une méthode anonyme. int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int[] oddNums = Common.FilterArrayOfInts(nums, delegate(int i) { return ((i & 1) == 1); }); foreach (int i in oddNums) Console.WriteLine(i);
Comme vous le voyez, il n’est plus nécessaire de définir une méthode de filtrage. Cette technique est particulièrement intéressante si le code qui remplace le délégué a peu de chances d’être utilisé à plusieurs reprises. Le résultat est bien entendu identique à celui de l’exemple précédent : 1 3 5 7
Les méthodes anonymes ont un inconvénient : elles sont verbeuses et difficiles à lire. Il serait vraiment agréable de pouvoir écrire le code de la méthode d’une manière plus concise ! Utiliser les expressions lambda En C#, les expressions lambda consistent en une liste de paramètres séparés entre eux par des virgules1, suivis de l’opérateur lambda (=>) puis d’une expression ou d’une déclaration. (param1, param2, …paramN) => expr
Si l’expression/la déclaration est plus complexe, vous pouvez utiliser un bloc délimité par les caractères { et } : (param1, param2, …paramN) => { statement1; statement2; ... statementN; return(lambda_expression_return_type); }
Dans cet exemple, le type de données renvoyé par l’instruction return doit correspondre au code de retour spécifié par le délégué. Voici un exemple d’expression lambda : x => x
1. Si les paramètres sont au nombre de deux (ou plus), ils doivent être délimités par des parenthèses.
Linq.book Page 23 Mercredi, 18. février 2009 7:58 07
Chapitre 2
Améliorations de C# 3.0 pour LINQ
23
Cette expression lambda pourrait se lire "x conduit à x" ou encore "entrée x sortie x". Cela signifie que la variable d’entrée x est également renvoyée par l’expression lambda. Étant donné que la fonction ne compte qu’un seul paramètre en entrée, il n’est pas nécessaire de l’entourer de parenthèses. Il est important d’avoir à l’esprit que le délégué détermine le type de l’entrée x ainsi que le type qui doit être retourné. Par exemple, si le délégué définit une chaîne en entrée et retourne un booléen, l’expression x => x ne peut pas être utilisée. Dans ce cas, la partie à droite de l’opérateur lambda doit retourner un booléen. Par exemple : x => x.Length > 0
Cette expression lambda pourrait se lire "x conduit à x.Length > 0" ou encore "entrée x, sortie x.Length > 0". Étant donné que la partie à droite de l’opérateur lambda est équivalente à un booléen, le délégué doit indiquer que la méthode renvoie un booléen, sans quoi une erreur se produira à la compilation. L’expression lambda ci-après tente de retourner la longueur de l’argument fourni en entrée. Le délégué doit donc spécifier que la valeur retournée est de type entier ( int). s => s.Length
Si plusieurs paramètres sont passés en entrée de l’expression lambda, séparez-les par des virgules et entourez-les par des parenthèses, comme dans l’expression suivante : (x, y) => x == y
Les expressions lambda complexes peuvent être spécifiées à l’intérieur d’un bloc, comme dans : (x, y) => { if (x > y) return (x); else return (y); }
ATTENTION Gardez à l’esprit que le délégué doit indiquer le type des paramètres en entrée et de l’élément renvoyé. Dans tous les cas, assurez-vous que ces éléments sont en accord avec les types définis dans le délégué.
Pour vous rafraîchir la mémoire, voici la déclaration delegate définie par le programmeur numéro 1 : delegate bool IntFilter(int i);
L’application développée par le programmeur numéro 2 devra accepter un paramètre de type int et retourner une valeur de type bool. Cela peut se déduire de la méthode appelée et du but du filtre, mais dans tous les cas rappelez-vous que c’est le délégué qui dicte les types en entrée et en sortie.
Linq.book Page 24 Mercredi, 18. février 2009 7:58 07
24
LINQ et C# 2008
Partie I
En utilisant une expression lambda, l’exemple précédent se transforme en le Listing 2.3. Listing 2.3 : Appel du filtre avec une expression lambda. int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int[] oddNums = Common.FilterArrayOfInts(nums, i => ((i & 1) == 1)); foreach (int i in oddNums) Console.WriteLine(i);
Ce code est vraiment concis. S’il vous semble quelque peu déroutant, une fois que vous y serez habitué vous verrez à quel point il est réutilisable et facile à maintenir. Bien entendu, les résultats sont les mêmes que dans les exemples précédents : 1 3 5 7
Pour récapituler, voici quelques instructions concernant les trois approches dont nous venons de parler : int[] oddNums = // Approche méthode nommée Common.FilterArrayOfInts(nums, Application.IsOdd); int[] oddNums = // Approche méthode anonyme Common.FilterArrayOfInts(nums, delegate(int i){return((i & 1) == 1);}); int[] oddNums = // Approche expression lambda Common.FilterArrayOfInts(nums, i => ((i & 1) == 1));
La première version semble plus courte que les autres, mais vous devez garder à l’esprit qu’elle est associée à une méthode nommée dans laquelle est défini le traitement à effectuer. Cette alternative sera certainement le meilleur choix si la méthode doit être réutilisée et/ou si l’algorithme mis en œuvre est complexe et/ou doit être confié à des spécialistes. ASTUCE Les algorithmes complexes et/ou réutilisés sont mieux gérés par des méthodes nommées. Ils sont alors accessibles à tout développeur, même s’il ne saisit pas toutes les nuances du code mis en œuvre.
C’est au développeur de choisir quelle méthode est la plus appropriée dans son cas précis : une méthode nommée, une méthode anonyme ou une expression lambda. Les expressions lambda peuvent être passées comme argument des requêtes LINQ. Étant donné que ces requêtes ont toutes les chances d’utiliser des arguments à usage unique ou en tout cas peu réutilisés, l’alternative des opérateurs lambda offre une
Linq.book Page 25 Mercredi, 18. février 2009 7:58 07
Chapitre 2
Améliorations de C# 3.0 pour LINQ
25
grande flexibilité et n’oblige pas le programmeur à écrire une méthode nommée pour chaque requête. Arbres d’expressions Les arbres d’expressions permettent de représenter sous la forme d’arbres les expressions lambda utilisées dans des requêtes. Ils autorisent l’évaluation simultanée de tous les opérateurs impliqués dans une requête. Ils semblent donc parfaitement adaptés à la manipulation de sources de données telles que celles embarquées dans une base de données. Dans la plupart des exemples passés en revue jusqu’ici, les opérateurs de requête ont été exécutés de façon séquentielle. Examinons le code ci-après : int[] nums = new int[] { 6, 2, 7, 1, 9, 3 }; IEnumerable numsLessThanFour = nums .Where(i => i < 4) .OrderBy(i => i);
Cette requête utilise les opérateurs Where et OrderBy, qui attendent des méthodes déléguées en argument. Lorsque ce code est compilé, L’IL (Intermediate Language) .NET fabriqué est identique à celui que produirait une méthode anonyme pour chacun des opérateurs des expressions lambda. À l’exécution, les opérateurs Where puis OrderBy sont appelés successivement. Cette exécution séquentielle des opérateurs semble convenir dans cet exemple, mais supposez que cette requête soit appliquée dans une source de données volumineuse (une base de données, par exemple). Cela aurait-il un sens de filtrer les données une première fois avec l’opérateur Where, puis une seconde avec l’opérateur OrderBy. Cette technique n’est évidemment pas applicable aux requêtes de bases de données ni potentiellement à d’autres types de requêtes. C’est ici que les arbres d’expressions prennent toute leur importance. Ils autorisent en effet l’évaluation et l’exécution simultanées de tous les opérateurs d’une requête. Le compilateur est donc maintenant en mesure de coder deux types de codes pour une expression lambda : du code IL ou un arbre d’expressions. C’est le prototype de l’opérateur qui détermine quel type de code sera généré. Si sa déclaration l’autorise à accepter une méthode déléguée, du code IL sera généré. Si sa déclaration l’autorise à accepter une expression d’une méthode déléguée, un arbre d’expressions sera généré. À titre d’exemple, nous allons nous intéresser à deux implémentations différentes de l’opérateur Where. La première est l’opérateur de requête standard Where de l’API LINQ to Objects, définie dans la classe System.Linq.Enumerable : public static IEnumerable Where( this IEnumerable source, Func predicate);
Linq.book Page 26 Mercredi, 18. février 2009 7:58 07
26
LINQ et C# 2008
Partie I
La seconde implémentation de l’opérateur Where provient de l’API LINQ to SQL et de la classe System.Linq.Queryable : public static IQueryable Where( this IQueryable source, System.Linq.Expressions.Expression predicate);
Comme vous pouvez le voir, le premier opérateur Where accepte la méthode déléguée Func en argument. Du code IL sera donc généré par le compilateur pour l’expression lambda de cet opérateur. Reportez-vous au Chapitre 3 pour avoir plus d’informations sur le délégué Func. Pour l’instant, il vous suffit de comprendre que le délégué Func définit la signature de l’argument. Le deuxième opérateur Where accepte un arbre d’expressions (Expression) en argument. Le compilateur générera donc un arbre d’expressions pour représenter les données. Les opérateurs qui admettent une séquence IEnumerable comme premier argument utilisent des délégués pour manipuler les expressions lambda. En revanche, les opérateurs qui admettent une séquence IQueryable comme premier argument utilisent des arbres d’expressions. INFO Le compilateur produit du code IL pour les méthodes d’extension des séquences IEnumerable, alors qu’il produit des arbres d’expressions pour les méthodes d’extension des séquences IQueryable.
Le développeur qui se contente d’utiliser LINQ n’est pas obligé de connaître les tenants et les aboutissants des arbres d’expressions. C’est la raison pour laquelle cet ouvrage n’ira pas plus loin dans les fonctionnalités avancées des arbres d’expressions. Le mot-clé var, l’initialisation d’objets et les types anonymes Il est quasiment impossible de s’intéresser au mot-clé var et à l’inférence de type sans aborder l’initialisation des objets et les types anonymes. De même, il est quasiment impossible de s’intéresser à l’initialisation d’objets et aux types anonymes en passant sous silence le mot-clé var. Étant donné leurs fortes imbrications, plutôt que décrire séparément ces trois nouveautés du langage C#, je vais vous les présenter simultanément. Examinez la déclaration ciaprès : var1 mySpouse = new {2 FirstName = "Vickey"3, LastName = "Rattz" };
1. Le mot-clé var apparaît clairement devant le nom de la variable. 2. Un type anonyme sera utilisé, car l’opérateur new est utilisé sans préciser une classe nommée. 3. L’objet anonyme sera explicitement initialisé en utilisant la nouvelle fonctionnalité d’initialisation d’objet.
Linq.book Page 27 Mercredi, 18. février 2009 7:58 07
Chapitre 2
Améliorations de C# 3.0 pour LINQ
27
Dans cet exemple, la variable mySpouse est déclarée en utilisant le mot-clé var. Cette variable se voit assigner un type anonyme… dont le type est connu grâce aux nouveautés de C# en matière d’initialisation d’objets. Cette simple ligne de code tire parti du mot-clé var, des types anonymes et de l’initialisation d’objets. Pour résumer, le mot-clé var permet de déduire le type d’un objet en tenant compte du type des données utilisées pour l’initialiser. Les types anonymes permettent donc de créer des types de classes à la volée. Comme le laisse prévoir le mot "anonyme", ces nouveaux types de données n’ont pas de nom. Il n’est pas simple de créer une donnée anonyme sans connaître ses variables membres, et vous ne pouvez pas connaître ses variables membres sans connaître leurs types. Enfin, vous ne pouvez pas connaître le type de ses membres jusqu’à ce qu’ils soient initialisés. Mais, rassurez-vous, la fonctionnalité d’initialisation de C# 3.0 gère tout ce fatras pour vous ! Lorsque cette ligne de code passera entre les mains du compilateur, une nouvelle classe de type anonyme sera créée. Elle contiendra deux membres de type String : FirstName et LastName. Le mot-clé var est implicitement typé pour les variables locales L’introduction des types anonymes dans le langage C# a induit un problème sousjacent : si une variable dont le type n’est pas défini est instanciée avec un objet de type anonyme, quel sera le type de la variable ? Considérez le code ci-après : // Ce code n’est pas compilable ! ??? unnamedTypeVar = new {firstArg = 1, secondArg = "Joe" };
Quel type déclareriez-vous pour la variable unnamedTypeVar ? Pour résoudre ce problème, le mot-clé var a été défini par les ingénieurs en charge du développement du langage C# chez Microsoft. Ce mot-clé informe le compilateur qu’il doit implicitement définir le type de la variable en utilisant l’initialiseur de la variable. Si vous ne définissez pas un initialiseur, il en résultera une erreur à la compilation. Le Listing 2.4 représente un code qui déclare une variable avec le mot-clé var sans l’initialiser. Listing 2.4 : Une déclaration de variable invalide utilisant le mot-clé var. var name;
Voici l’erreur générée par le compilateur. Implicitly-typed local variables must be initialized
Étant donné que le type des variables est vérifié de façon statique à la compilation, il est nécessaire de définir un initialiseur pour que le compilateur puisse faire son travail jusqu’au bout. Mais, attention, vous ne devrez pas affecter une valeur d’un autre type à
Linq.book Page 28 Mercredi, 18. février 2009 7:58 07
28
LINQ et C# 2008
Partie I
cette variable dans la suite du code, sans quoi une erreur se produira à la compilation. Examinons le code du Listing 2.5. Listing 2.5 : Une affectation incorrecte à une variable déclarée avec le mot-clé var. var name = "Joe"; // Jusqu’ici, tout va bien name = 1; // Ceci est incorrect ! Console.WriteLine(name);
Ce code ne passera pas l’étape de la compilation, car le type de la variable est implicitement défini à String par sa première affectation. Il est donc impossible de lui affecter une valeur entière par la suite. Voici l’erreur générée par le compilateur : Cannot implicitly convert type ’int’ to ’string’
Comme vous le voyez, le compilateur s’occupe de la cohérence du type des données affectées à la variable. Pour en revenir à la déclaration du type anonyme unnamedTypeVar, la syntaxe à utiliser est celle du Listing 2.6. Listing 2.6 : Un type anonyme affecté à une variable déclarée avec le mot-clé var. var unnamedTypeVar = new {firstArg = 1, secondArg = "Joe" }; Console.WriteLine(unnamedTypeVar.firstArg + ". " + unnamedTypeVar.secondArg);
Voici le résultat de ce code : 1.Joe
L’utilisation du mot-clé var apporte deux avantages : la vérification de type statique et la flexibilité apportée par le support des types anonymes. Ce dernier point deviendra très important lorsque nous nous intéresserons aux opérateurs de projection dans la suite de l’ouvrage. Dans les exemples passés en revue jusqu’ici, le mot-clé var était obligatoire. En effet, si vous affectez un objet résultant d’une classe anonyme à une variable, cette dernière doit être déclarée avec le mot-clé var. Notez cependant que le mot-clé var peut être utilisé à chaque déclaration de variable, à condition que cette dernière soit correctement initialisée. Pour des questions de maintenance du code, il n’est cependant pas conseillé d’abuser de cette technique : les développeurs devraient toujours connaître le type des données qu’ils manipulent. Bien sûr, vous connaissez le type de vos données aujourd’hui, mais qu’en sera-t-il dans six mois ? Et si un autre programmeur prend la relève ? ASTUCE Afin de faciliter la maintenance de votre code, n’abusez pas du mot-clé var. Ne l’utilisez que lorsque cela est nécessaire. Par exemple lorsque vous affectez un objet de type anonyme à une variable.
Linq.book Page 29 Mercredi, 18. février 2009 7:58 07
Chapitre 2
Améliorations de C# 3.0 pour LINQ
29
Expressions d’initialisation d’objets et de collections Les types anonymes autorisant l’utilisation de types de données dynamiques, le mode d’initialisation des objets et des collections a été simplifié, essentiellement grâce aux expressions lambda ou aux arbres d’expressions.
Initialisation d’objets Vous pouvez désormais spécifier les valeurs des membres et propriétés public d’une classe pendant son instanciation : public class Address { public string address; public string city; public string state; public string postalCode; }
Sans la fonctionnalité d’initialisation ajoutée à C# 3.0, vous n’auriez pas pu utiliser un constructeur spécialisé, et vous auriez dû définir un objet de type Address, comme dans le Listing 2.7. Listing 2.7 : Instanciation et initialisation de la classe avec l’ancienne méthode. Address address = new Address(); address.address = "105 Elm Street"; address.city = "Atlanta"; address.state = "GA"; address.postalCode = "30339";
Cette technique serait très lourde dans une expression lambda. Supposons que vous ayez défini une requête à partir d’une source de données et que vous vouliez projeter certains membres dans un objet Address en utilisant l’opérateur Select : // Ce code ne passera pas la compilation IEnumerable addresses = somedatasource .Where(a => a.State = "GA") .Select(a => new Address(???)???);
Il n’existe aucun moyen simple d’initialiser les membres de l’objet Address. N’ayez crainte : l’initialisation d’objet de C# 3.0 est la solution. Bien sûr, il serait possible de créer un constructeur qui vous permettrait de passer les valeurs à initialiser à l’instanciation de l’objet. Mais quel travail ! Le Listing 2.8 montre comment résoudre le problème par l’intermédiaire d’un type anonyme construit à la volée. Listing 2.8 : Instanciation et initialisation de la classe avec la nouvelle méthode. Address address = new Address { address = "105 Elm Street", city = "Atlanta", state = "GA", postalCode = "30339" };
Linq.book Page 30 Mercredi, 18. février 2009 7:58 07
30
LINQ et C# 2008
Partie I
Les expressions lambda autorisent ce genre de manipulation, y compris en dehors des requêtes LINQ ! Le compilateur instancie les membres nommés avec les valeurs spécifiées. Les éventuels membres non spécifiés utiliseront le type de données par défaut. Initialisation de collections Les ingénieurs de Microsoft ont également mis au point une technique d’initialisation de collections. Il vous suffit pour cela de spécifier les valeurs de la collection, tout comme vous le feriez pour un objet. Une restriction : la collection doit implémenter l’interface System.Collections.Generic.ICollection. Les collections C# héritées (celles qui se trouvent dans l’espace de noms System.Collection) ne sont pas concernées. Le Listing 2.9 donne un exemple d’initialisation de collection. Listing 2.9 : Un exemple d’initialisation de collection. using System.Collections.Generic; List presidents = new List { "Adams", "Arthur", "Buchanan" }; foreach(string president in presidents) { Console.WriteLine(president); }
Voici le résultat obtenu lorsque vous exécutez le programme en appuyant sur Ctrl+F5 : Adams Arthur Buchanan
Vous pouvez également utiliser cette technique pour créer facilement des collections initialisées dans le code, même si vous n’utilisez pas LINQ. Types anonymes C# étant dans l’impossibilité de créer de nouveaux types de données à la compilation, il est difficile de définir une nouvelle API agissant au niveau du langage pour les requêtes génériques. Les ingénieurs qui ont mis au point le langage C# 3.0 ont relevé cette prouesse : désormais, il est possible de créer dynamiquement des classes non nommées et des propriétés dans ces classes. Ce type de classe est appelé "type anonyme".
Un type anonyme n’a pas de nom et est généré à la compilation, en initialisant un objet en cours d’instanciation. Étant donné que la classe n’a pas de type, toute variable affectée à un objet d’un type anonyme doit pouvoir le déclarer. C’est là qu’intervient le motclé new de C# 3.0. Un type anonyme ne peut pas être estimé s’il est issu d’un opérateur Select ou SelectMany. Sans les types anonymes, des classes nommées devraient être définies pour rece-
Linq.book Page 31 Mercredi, 18. février 2009 7:58 07
Chapitre 2
Améliorations de C# 3.0 pour LINQ
31
voir des données issues des opérateurs Select ou SelectMany. Ceci se révélerait très lourd et peu pratique à mettre en place. Dans la section relative à l’initialisation d’objets, j’ai introduit le code d’instanciation et d’initialisation suivant : Address address = new Address { address = "105 Elm Street", city = "Atlanta", state = "GA", postalCode = "30339" };
Pour utiliser un type anonyme à la place de la classe nommée Address, il suffit d’omettre le nom de la classe. Notez cependant qu’il est impossible de stocker le nouvel objet instancié dans une variable de type Address, car l’objet n’est pas encore de type Address. Son type n’est connu que du compilateur. Il est donc également nécessaire de changer le type de données de la variable address en utilisant le mot-clé var (voir Listing 2.10). Listing 2.10 : Instanciation et initialisation d’un type anonyme en utilisant l’initialisation d’objets. var address = new { address = "105 Elm Street", city = "Atlanta", state = "GA", postalCode = "30339" }; Console.WriteLine("address = {0} : city = {1} : state = {2} : zip = {3}", address.address, address.city, address.state, address.postalCode); Console.WriteLine("{0}", address.GetType().ToString());
La dernière ligne a été ajoutée pour afficher le nom de la classe anonyme générée par le compilateur. Voici le résultat : address = 105 Elm Street : city = Atlanta : state = GA : zip = 30339 f__AnonymousType5`4[System.String,System.String,System.String,System.String]
Ce nom peu orthodoxe laisse clairement entendre qu’il a été généré par un compilateur (le nom généré par votre compilateur a de grandes chances d’être différent). Méthodes d’extension Une méthode d’extension est une méthode ou une classe statique qui peut être invoquée comme s’il s’agissait d’une méthode d’instance d’une classe différente. Vous pourriez par exemple créer la méthode statique d’extension ToDouble dans la classe statique StringConversions. Cette méthode serait appelée comme s’il s’agissait d’une méthode d’un objet de type string.
Linq.book Page 32 Mercredi, 18. février 2009 7:58 07
32
LINQ et C# 2008
Partie I
Avant d’entrer dans le détail des méthodes d’extension, nous allons nous intéresser au problème qui leur a donné naissance. Nous allons comparer les méthodes statiques (class) aux méthodes d’instance (object). Les méthodes d’instance peuvent seulement être appelées dans les instances d’une classe, aussi appelées objets. Il est impossible d’appeler une méthode d’instance dans la classe elle-même. Au contraire, les méthodes statiques ne peuvent être appelées qu’à l’intérieur d’une classe. Rappel sur les méthodes d’instance et les méthodes statiques La méthode ToUpper de la classe string est un exemple d’une méthode d’instance : elle ne peut être appelée que sur un objet string. En aucun cas sur la classe string elle-même.
Dans le code du Listing 2.11, la méthode ToUpper est appelée sur l’objet name. Listing 2.11 : Appel d’une méthode d’instance d’un objet. // Ce code passe l’étape de la compilation string name = "Joe"; Console.WriteLine(name.ToUpper());
Ce code est compilable. Son exécution affiche la conversion en majuscules de la variable name : JOE
Si vous essayez d’appeler la méthode ToUpper sur la classe string, vous obtiendrez une erreur de compilation, car ToUpper est une méthode d’instance. Elle ne peut donc être appelée qu’à partir d’un objet et non d’une classe. Le Listing 2.12 donne un exemple d’un tel code. Listing 2.12 : Tentative d’appel d’une méthode d’instance sur une classe. // Ce code ne passe pas l’étape de la compilation string.ToUpper();
Voici l’erreur affichée par le compilateur : An object reference is required for the nonstatic field, method, or property ’string.ToUpper()’
Cet exemple peut sembler un peu bizarre, puisque aucune valeur n’a été communiquée à ToUpper. Si vous essayiez de passer une valeur à ToUpper, cela reviendrait à appeler une variante de la méthode ToUpper. Ceci est impossible puisqu’il n’existe aucun prototype de ToUpper dont la signature contienne un string. Faites la différence entre la méthode ToUpper et la méthode Format de la classe string. Cette dernière est statique. Elle doit donc être appliquée à la classe string et non à un
Linq.book Page 33 Mercredi, 18. février 2009 7:58 07
Chapitre 2
Améliorations de C# 3.0 pour LINQ
33
objet string. Essayons d’invoquer cette méthode sur un objet string (voir Listing 2.13). Listing 2.13 : Tentative d’appel d’une méthode de classe sur un objet. string firstName = "Joe"; string lastName = "Rattz"; string name = firstName.Format("{0} {1}", firstName, lastName); Console.WriteLine(name);
Ce code produit l’erreur suivante lors de la compilation : Member ’string.Format(string, object, object)’ cannot be accessed with an instance reference; qualify it with a type name instead
Appliquons maintenant la méthode Format sur la classe string elle-même (voir Listing 2.14). Listing 2.14 : Appel d’une méthode de classe sur une classe. string firstName = "Joe"; string lastName = "Rattz"; string name = string.Format("{0} {1}", firstName, lastName); Console.WriteLine(name);
Ce code passe la compilation et donne le résultat suivant à l’exécution : Joe Ratz
Outre le mot-clé static, il suffit souvent d’observer la signature d’une méthode pour savoir qu’il s’agit d’une méthode d’instance. Considérez par exemple la méthode ToUpper. Elle ne comprend aucun autre argument que la version surchargée de la référence à l’objet. Si elle ne dépend pas d’une instance string d’une donnée interne, quelle valeur string pourrait-elle mettre en majuscules ? Résolution du problème par les méthodes d’extension Supposons que vous soyez un développeur et que vous deviez mettre en place une nouvelle façon d’interroger des objets. Supposons que vous décidiez de créer une méthode Where pour traiter la clause Where. Comment procéderiez-vous ?
L’opérateur Where devrait-il être traité dans une méthode d’instance ? Dans ce cas, à quelle classe ajouteriez-vous cette méthode, étant donné que vous voulez que la méthode Where puisse interroger toute collection d’objets. Aucune réponse logique à cette question ! En adoptant cette approche, vous devriez modifier un très grand nombre de classes si vous vouliez que la méthode soit universelle. La méthode doit donc être statique. Comme nous allons le voir dans les lignes suivantes, si l’on se réfère aux requêtes SQL traditionnelles, incluant plusieurs clauses where, jointures, regroupements et/ou tris, une méthode statique n’est pas vraiment appropriée.
Linq.book Page 34 Mercredi, 18. février 2009 7:58 07
34
LINQ et C# 2008
Partie I
Supposons que vous ayez défini un nouveau type de données : une séquence d’objets génériques que nous appellerons Enumerable. La méthode Where devrait opérer sur un Enumerable et retourner un autre Enumerable filtré. De plus, la méthode Where devrait accepter un argument qui permette au développeur de préciser la logique utilisée pour filtrer les enregistrements de données depuis ou dans l’Enumerable. Cet argument, que j’appellerai le prédicat, pourrait être spécifié dans une méthode nommée, une méthode anonyme ou une expression lambda. ATTENTION Les trois codes qui suivent sont purement démonstratifs. Ils ne passeront pas l’étape de la compilation.
Étant donné que la méthode Where demande une entrée à filtrer de type Enumerable, et que la méthode est statique, cette entrée doit être spécifiée dans un argument de la méthode Where. Ceci pourrait se matérialiser comme suit : static Enumerable Enumerable.Where(Enumerable input, LambdaExpression predicate) { … }
En ignorant pour l’instant la sémantique d’une expression lambda, un appel à la méthode Where pourrait s’effectuer par les instructions suivantes : Enumerable enumerable = {"one", "two", "three"}; Enumerable filteredEnumerable = Enumerable.Where(enumerable, lambdaExpression);
Cela ne s’annonce pas trop mal. Mais que faire si nous avons besoin de plusieurs clauses Where ? Puisque l’Enumerable sur lequel travaille la méthode Where doit être un argument de la méthode, le chaînage des méthodes revient à les imbriquer. Voici comment appeler trois clauses Where : Enumerable enumerable = {"one", "two", "three"}; Enumerable finalEnumerable = Enumerable.Where(Enumerable.Where(Enumerable.Where(enumerable, lX1), lX2), lX3);
Vous devez lire la dernière instruction de la partie la plus interne vers la partie la plus externe. Très difficile à lire ! Pouvez-vous imaginer à quoi ressemblerait une requête plus complexe ? Si seulement il y avait un autre moyen… La solution Une solution élégante consisterait à appeler la méthode statique Where sur chaque objet Enumerable, plutôt que sur la classe. Il ne serait alors plus nécessaire de passer chaque Enumerable dans la méthode Where, puisque l’objet Enumerable aurait accès à ses propres Enumerable. La requête précédente deviendrait donc : Enumerable enumerable = {"one", "two", "three"}; Enumerable finalEnumerable = enumerable.Where(lX1).Where(lX2).Where(lX3);
Linq.book Page 35 Mercredi, 18. février 2009 7:58 07
Chapitre 2
Améliorations de C# 3.0 pour LINQ
35
ATTENTION Les codes qui précèdent ainsi que le code qui suit sont purement démonstratifs. Ils ne passeront pas l’étape de la compilation.
Ce code pourrait être réécrit comme suit : Enumerable enumerable = {"one", "two", "three"}; Enumerable finalEnumerable = enumerable .Where(lX1) .Where(lX2) .Where(lX3);
Ce code est bien plus lisible : la déclaration peut maintenant être lue de gauche à droite et de haut en bas. Comme vous pouvez le voir, cette syntaxe est très simple à suivre. C’est la raison pour laquelle vous verrez de nombreuses requêtes LINQ exprimées de la sorte dans la documentation officielle et dans cet ouvrage. Pour terminer, vous avez besoin d’une méthode statique qui puisse être appelée dans une méthode de classe. Ce sont exactement les possibilités offertes par les méthodes d’extension. Elles ont été ajoutées à C# pour permettre d’appeler élégamment une méthode statique sans avoir à passer le premier argument de la méthode. Cela permet d’appeler la méthode d’extension comme s’il s’agissait de la méthode du premier argument. Les appels chaînés aux méthodes d’extension sont donc bien plus lisibles. Les méthodes d’extension permettent à LINQ d’appliquer des opérateurs de requête standard aux types qui implémentent l’interface IEnumerable. INFO Les méthodes d’extension peuvent être appelées sur une instance de classe (un objet) et non sur la classe elle-même.
Déclarations et invocations de méthodes d’extension Il suffit d’utiliser le mot-clé this comme premier argument d’une méthode pour la transformer en une méthode d’extension.
La méthode d’extension peut être utilisée sur n’importe quel objet dont le type est le même que celui de son premier argument. Si, par exemple, le premier argument de la méthode d’extension est de type string, elle apparaîtra comme une méthode d’instance string et pourra être appliquée à tout objet string. Ayez toujours à l’esprit que les méthodes d’extension ne peuvent être déclarées que dans des classes statiques.
Linq.book Page 36 Mercredi, 18. février 2009 7:58 07
36
LINQ et C# 2008
Partie I
Voici un exemple d’une méthode d’extension : namespace Netsplore.Utilities { public static class StringConversions { public static double ToDouble(this string s) { return Double.Parse(s); } public static bool ToBool(this string s) { return Boolean.Parse(s); } } }
Les classes et méthodes utilisées sont toutes statiques. Pour utiliser ces méthodes d’extension, il suffit d’appeler les méthodes statiques sur des instances d’objets, comme dans le Listing 2.15. Étant donné que la méthode ToDouble est statique et que son premier argument est this, ToDouble est une méthode d’extension. Listing 2.15 : Appel d’une méthode d’extension. using Netsplore.Utilities; double pi = "3.1415926535".ToDouble(); Console.WriteLine(pi);
Voici le résultat du WriteLine : 3.1415926535
Il est important de spécifier la directive using sur l’espace de noms Netsplore.Utilities. Si vous l’omettez, le compilateur ne trouvera pas les méthodes d’extension et vous obtiendrez une erreur du type suivant : ’string’ does not contain a definition for ’ToDouble’ and no extension method ’ToDouble’ accepting a first argument of type ’string’ could be found (are you missing a using directive or an assembly reference?)
Comme indiqué précédemment, il n’est pas permis de déclarer une méthode d’extension à l’intérieur d’une classe non statique. Si vous le faites, vous obtiendrez le message d’erreur suivant : Extension methods must be defined in a non-generic static class
Précédence des méthodes d’extension Les instances d’objets conventionnelles ont une précédence sur les méthodes d’extension lorsque leur signature est identique à la signature d’appel.
Les méthodes d’extension sont un concept très utile, en particulier si vous voulez étendre une classe "scellée" ou dont vous ne connaissez pas le code. Les méthodes d’extension
Linq.book Page 37 Mercredi, 18. février 2009 7:58 07
Chapitre 2
Améliorations de C# 3.0 pour LINQ
37
précédentes ajoutent des méthodes à la classe string. Si les méthodes d’extension n’existaient pas, vous ne pourriez pas le faire, car la classe string est scellée. Méthodes partielles Les méthodes partielles ajoutent un mécanisme de gestion d’événements ultraléger au langage C#. Oubliez les conclusions que vous êtes certainement en train de tirer sur les méthodes partielles : le seul point commun entre les méthodes partielles et les classes partielles est qu’une méthode partielle ne peut exister que dans une classe partielle. Avant de passer en revue les autres règles sur les méthodes partielles, nous allons nous intéresser à leur nature. Le prototype ou la définition d’une méthode partielle est spécifié dans sa déclaration, mais cette dernière n’inclut pas l’implémentation de la méthode. Aucun code IL n’est donc émis par le compilateur lors de la déclaration de la méthode, l’appel de la méthode ou l’évaluation des arguments passés à la méthode. C’est comme si la méthode n’avait jamais existé ! Le terme "méthode partielle" peut sembler inapproprié si l’on compare le comportement d’une méthode partielle à celui d’une classe partielle. Le terme "méthode fantôme" aurait certainement été plus judicieux… Un exemple de méthode partielle Voici un exemple de classe partielle dans lequel est définie une méthode partielle.
La classe MyWidget public partial class MyWidget { partial void MyWidgetStart(int count); partial void MyWidgetEnd(int count); public MyWidget() { int count = 0; MyWidgetStart(++count); Console.WriteLine("In the constructor of MyWidget."); MyWidgetEnd(++count); Console.WriteLine("count = " + count); } }
Cette classe partielle MyWidget contient une méthode partielle également nommée MyWidget. Les deux premières lignes définissent les méthodes partielles MyWidgetStart et MyWidgetStop. Toutes deux acceptent un paramètre et retournent void (cette dernière caractéristique est une obligation des méthodes partielles). Le bloc de code suivant est le constructeur. Comme vous pouvez le voir, il définit l’int count et l’initialise à 0. La méthode MyWidgetStart est alors appelée, un message est affiché dans la console, la méthode MyWidgetStop est appelée puis la valeur de count est affichée dans la console. La valeur de count est incrémentée à chaque passage dans
Linq.book Page 38 Mercredi, 18. février 2009 7:58 07
38
LINQ et C# 2008
Partie I
la méthode partielle. Ceci afin de prouver que, si une méthode partielle n’est pas implémentée, ses arguments ne sont pas évalués. Le code du Listing 2.16 définit un objet de classe MyWidget. Listing 2.16 : Instanciation de la classe MyWidget. MyWidget myWidget = new MyWidget();
Appuyez sur Ctrl+F5 pour exécuter le code. Voici le résultat obtenu dans la console : In the constructor of MyWidget. count = 0
Comme vous pouvez le voir, après que le constructeur de MyWidget eut incrémenté à deux reprises la variable count, la valeur affichée à la fin du constructeur est égale à zéro. Ceci vient du fait que les arguments des méthodes partielles ne sont pas implémentés. Aucun code IL n’est donc émis par le compilateur. Nous allons maintenant ajouter une implémentation pour les deux méthodes partielles : Une autre déclaration de MyWidget contenant l’implémentation des méthodes partielles public partial class MyWidget { partial void MyWidgetStart(int count) { Console.WriteLine("In MyWidgetStart(count is {0})", count); } partial void MyWidgetEnd(int count) { Console.WriteLine("In MyWidgetEnd(count is {0})", count); } }
L’implémentation ayant été rajoutée, exécutez à nouveau le code du Listing 2.16. Vous obtiendrez l’affichage suivant dans la console : In MyWidgetStart(count is 1) In the constructor of MyWidget. In MyWidgetEnd(count is 2) count = 2
Comme vous pouvez le voir, les méthodes partielles ont été implémentées et les arguments, passés et évalués (la variable count vaut 2 à la fin de la sortie écran). Pourquoi utiliser les méthodes partielles ? Vous vous demandez peut-être pourquoi utiliser des méthodes partielles. Certains rétorqueront qu’elles s’apparentent à l’héritage et aux méthodes virtuelles. Mais, alors, pourquoi alourdir le langage avec les méthodes partielles ? Tout simplement parce qu’elles sont plus efficaces si vous prévoyez d’utiliser des procédures potentiellement
Linq.book Page 39 Mercredi, 18. février 2009 7:58 07
Chapitre 2
Améliorations de C# 3.0 pour LINQ
39
non implémentées. Elles permettent d’écrire du code pouvant être étendu par une personne tierce via le paradigme des classes partielles sans dégradation de performances. Les méthodes partielles ont certainement été ajoutées à C# pour les besoins des outils de génération de classes d’entités de LINQ to SQL. À titre d’exemple, chaque propriété mappée d’une classe d’entités possède une méthode partielle qui est appelée avant que la propriété ne change et une autre qui est appelée après que la propriété eut changé. Ceci permet d’ajouter un autre module en déclarant la même classe d’entité, d’implémenter ces méthodes partielles et d’être averti chaque fois qu’une propriété est sur le point d’être modifiée et après sa modification. Cela n’est-il pas intéressant ? Le code ne sera ni plus volumineux ni plus lent. Alors, ne vous en privez pas ! Les règles Les méthodes partielles doivent respecter quelques règles. Ces dernières ne sont pas trop contraignantes, et l’on y gagne vraiment au change en termes de flexibilité et de possibilités offertes au programmeur. Les voici : m
Elles ne doivent être définies et implémentées que dans des classes partielles.
m
Elles doivent être préfixées par le mot-clé partiel.
m
Elles sont privées mais ne doivent pas utiliser le mot-clé private, sinon une erreur sera générée à la compilation.
m
Elles doivent retourner void.
m
Elles peuvent ne pas être implémentées.
m
Elles peuvent être static.
m
Elles peuvent avoir des arguments.
Expressions de requête Un des avantages du langage C# est la déclaration foreach. Cette instruction est remplacée par le compilateur par une boucle qui appelle des méthodes telles que GetEnumerator et MoveNext. La simplicité de cette instruction l’a rendue universelle lorsqu’il s’agit d’énumérer des tableaux et collections. La syntaxe des requêtes LINQ est très proche de celle de SQL et vraiment appréciée par les développeurs. Les exemples des pages précédentes utilisent cette syntaxe, propre à C# 3.0, connue sous le nom "expressions de requêtes". Pour réaliser une requête LINQ, il n’est pas obligatoire d’utiliser une expression de requête. Une alternative consiste à utiliser la notation "à point" standard de C#, en appliquant des méthodes à des objets et des classes. Dans de nombreux cas, l’utilisation de la notation standard est favorable au niveau des instructions, car très démonstrative. Plusieurs exemples de ce livre préfèrent la syntaxe "à point" traditionnelle aux expressions
Linq.book Page 40 Mercredi, 18. février 2009 7:58 07
40
LINQ et C# 2008
Partie I
de requête. Il n’y a aucune concurrence entre ces deux types d’écritures. Cependant, la facilité avec laquelle vous écrirez vos premières expressions de requête peut se révéler enthousiasmante… Pour avoir une idée des différences entre les deux types de notations, le Listing 2.17 met en œuvre une requête fondée sur la syntaxe traditionnelle de C#. Listing 2.17 : Une requête utilisant la notation à point traditionnelle. string[] names = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable sequence = names .Where(n => n.Length < 6) .Select(n => n); foreach (string name in sequence) { Console.WriteLine("{0}", name); }
Le Listing 2.18 est la requête équivalente fondée sur les expressions de requête. Listing 2.18 : La requête équivalente fondée sur les expressions de requête. string[] names = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable sequence = from n in names where n.Length < 6 select n; foreach (string name in sequence) { Console.WriteLine("{0}", name); }
La première chose qui saute aux yeux quant à l’expression de requête est que, contrairement au SQL, la déclaration from précède le select. Une des raisons majeures ayant motivé ce changement vient de l’IntelliSense. Sans cette inversion, si vous tapiez select suivi d’une espace dans l’éditeur de Visual Studio 2008, IntelliSense n’aurait aucune idée des éléments à afficher dans la liste déroulante. En indiquant d’où proviennent les données, IntelliSense a une idée précise des variables à proposer dans la liste déroulante.
Linq.book Page 41 Mercredi, 18. février 2009 7:58 07
Chapitre 2
Améliorations de C# 3.0 pour LINQ
41
Ces deux exemples donnent le résultat suivant : Adams Bush Ford Grant Hayes Nixon Polk Taft Tyler
Grammaire des expressions de requête Les expressions de requête doivent se conformer aux règles de grammaire suivantes :
1. Une expression de requête doit toujours commencer par la clause from. 2. Peuvent ensuite venir zéro, une ou plusieurs clauses from, let et/ou where. La clause from définit une ou plusieurs énumérations qui passent en revue les éléments d’une ou de plusieurs séquences. La clause let définit une variable et lui affecte une valeur. La clause where filtre les éléments d’une séquence ou réalise une jointure de plusieurs séquences dans la séquence de sortie. 3. La suite de l’expression de requête peut contenir une clause orderby qui trie les données sur un ou plusieurs champs. Le tri peut être ascendant ( ascending) ou descendant (descending). 4. Une clause select ou group doit alors faire suite. 5. La suite de l’expression de requête peut contenir une clause de continuation optionnelle into, zéro, une ou plusieurs clauses join, ainsi qu’un ou plusieurs autres blocs syntaxiques, à partir du point numéro 2. La clause into redirige les résultats de la requête dans une séquence de sortie imaginaire. Cette séquence se comporte comme une clause from pour l’expression suivante commençant par le point numéro 2. Pour une description plus technique de la grammaire des expressions de requête, utilisez le diagramme suivant provenant de la documentation officielle MSDN sur LINQ. Expression de requête from-clause query-body Clause from from typeopt identifier in expression join-clausesopt Clauses join join-clause join-clauses join-clause Clause join join typeopt identifier in expression on expression equals expression join typeopt identifier in expression on expression equals expression into identifier Corps de la requête from-let-where-clausesopt orderby-clauseopt select-or-group-clause query-continuationopt
Linq.book Page 42 Mercredi, 18. février 2009 7:58 07
42
LINQ et C# 2008
Partie I
Clauses from, let et where from-let-where-clause from-let-where-clauses from-let-where-clause Clause from, let et where from-clause let-clause where-clause Clause let let identifier = expression Clause where where boolean-expression Clause orderby orderby orderings Tris ordering orderings , ordering Tri expression ordering-directionopt Direction du tri ascending descending Clause select ou group select-clause group-clause Clause select select expression Clause group group expression by expression Continuation de la requête into identifier join-clausesopt query-body
Traduction des expressions de requête Supposons que vous ayez créé une expression de requête syntaxiquement correcte. Pour la traduire en code "à point" C#, le compilateur recherche des "motifs". La traduction s’effectue en plusieurs étapes. Chacune d’entre elles recherche un ou plusieurs motifs spécifiques. Le compilateur réitère la traduction pour tous les motifs correspondant à l’étape actuelle avant de passer à la suivante. Par ailleurs, l’étape n de la traduction ne peut se faire que si les n–1 étapes précédentes ont été achevées.
Identificateurs transparents Certaines traductions insèrent des variables d’énumération comprenant des identificateurs transparents. Dans les descriptions de la section suivante, les identificateurs transparents sont identifiés par des astérisques (*). Ce signe ne doit pas être confondu avec le caractère de remplacement "*". Lors de la traduction, il arrive que certaines énumérations additionnelles soient générées par le compilateur et que des identificateurs transparents soient utilisés pour les énumérer (ces identificateurs n’existent que pendant le processus de traduction).
Linq.book Page 43 Mercredi, 18. février 2009 7:58 07
Chapitre 2
Améliorations de C# 3.0 pour LINQ
43
Étapes de la traduction Dans cette section, nous allons utiliser les conventions du Tableau 2.1, où des lettres représentent les variables utilisées dans des portions spécifiques d’une requête. Tableau 2.1 : Variables de traduction.
Variable
Description
Exemple
c
Variable temporaire générée par le compilateur
aucun
e
Variable d’énumération
from e in customers
f
Champ sélectionné ou nouveau type anonyme
from e in customers select f
g
Un élément groupé
from e in s group g by k
i
Un imaginaire dans une séquence
from e in s into i
k
Élément clé groupé ou joint
from e in s group g by k
l
Une variable définie avec let
from e in s let l = v
o
Un élément classé
from e in s orderby o
s
La séquence d’entrée
from e in s
v
Une valeur affectée à une variable par let
from e in s let l = v
w
Une clause where
from e in s where w
Attention ! Le processus de traduction est complexe. Que cela ne vous décourage pas ! En effet, vous n’avez pas besoin de comprendre ce qui va être dit dans les détails pour écrire des requêtes LINQ. Les informations données dans cette section sont un plus. Il y a fort à parier que vous n’en aurez que rarement besoin, voire jamais. Dans la suite, les étapes de la traduction seront spécifiées sous la forme motif –> traduction. Je vais présenter ces étapes en me conformant à l’enchaînement logique du compilateur. Il serait sans doute plus simple de comprendre le processus de traduction en utilisant l’enchaînement inverse de celui du compilateur. En effet, la première étape ne met en œuvre que le premier motif. Elle donne naissance à plusieurs autres motifs non traduits qu’il faut encore traiter. Étant donné que chaque étape de traduction nécessite que l’étape précédente soit entièrement traduite, lorsque le processus est terminé il ne reste plus aucun terme à traduire. C’est la raison pour laquelle la dernière étape de la traduction est plus aisée à comprendre que la première. Et la description inversée des étapes de traduction est également la meilleure façon de comprendre ce qui se passe. Ceci étant dit, voici les étapes de traduction, décrites dans l’ordre du compilateur.
Linq.book Page 44 Mercredi, 18. février 2009 7:58 07
44
LINQ et C# 2008
Partie I
Clauses Select et Group avec une clause into Si une expression de requête contient une clause into, la traduction suivante est effectuée : from …1 into i …2
from i in from …1 …2
–>
Voici un exemple : from c in customers group c by c.Country into g select new { Country = g.Key, CustCount = g.Count() }
from g in from c in customers group c by c.Country select new { Country = g.Key, custCount = g.Count() }
–>
Lorsque toutes les étapes de la traduction ont été passées, le code est finalement traduit en : customers.GroupBy(c => c.Country) .Select(g => new { Country = g.Key, CustCount = g.Count() })
Types explicites de variables d’énumération Si votre expression de requête contient une clause from qui spécifie explicitement le type d’une variable d’énumération, la traduction suivante sera effectuée : from T e in s
–>
from e in s.Cast()
Voici un exemple : from Customer c in customers select c
–>
from c in customers.Cast()
Lorsque toutes les étapes de la traduction ont été passées, le code est finalement traduit en : customers.Cast()
Si l’expression de requête contient une clause join qui spécifie explicitement un type de variable d’énumération, la traduction suivante est effectuée : join T e in s on k1 equals k2
–>
join e in s.Cast() on k1 equals k2
–>
from c in customers join o in orders.Cast() on c.CustomerID equals o.CustomerID select new { c.Name, o.OrderDate, o.Total }
Voici un exemple : from c in customers join Order o in orders on c.CustomerID equals o.CustomerID select new { c.Name, o.OrderDate, o.Total }
Lorsque toutes les étapes de la traduction ont été passées, le code est finalement traduit en : .Join(orders.Cast(), c => c.CustomerID, o => o.CustomerID, new { c.Name, o.OrderDate, o.Total })
Linq.book Page 45 Mercredi, 18. février 2009 7:58 07
Chapitre 2
Améliorations de C# 3.0 pour LINQ
45
ASTUCE La saisie explicite de variables d’énumération est nécessaire lorsque la collection de données énumérée est héritée des collections de C# (ArrayList, par exemple). Le casting opéré convertit la collection héritée en une séquence qui implémente IEnumerable afin d’assurer la compatibilité avec les opérateurs de requête.
Clauses join Si l’expression de requête contient une clause from suivie d’une clause join, mais pas d’une clause into suivie d’une clause select, la traduction suivante est opérée (t est une variable temporaire créée par le compilateur) : from e1 in s1 join e2 in s2 on k1 equals k2 select f
–>
from t in s1 .Join(s2, e1 => k1, e2 => k2, (e1, e2) => f) select t
Voici un exemple : from c in customers join o in orders on c.CustomerID equals o.CustomerID select new { c.Name, o.OrderDate, o.Total }
–>
from t in customers .Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) =>new { c.Name, o.orderDate o.Total }) select t
Lorsque toutes les étapes de la traduction ont été passées, le code est finalement traduit en : customers .Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c.Name, o.OrderDate, o.Total })
Si l’expression de requête contient une clause from suivie d’une clause join, puis d’une clause into suivie d’une clause select, la traduction suivante est opérée (t est une variable temporaire créée par le compilateur) : from e1 in s1 join e2 in s2 on k1 equals k2 into i select f
from t in s1 .GroupJoin(s2, e1 => k1, –> e2 => k2, (e1, i) => f) select t
Voici un exemple : from c in customers join o in orders on c.CustomerID equals o.CustomerID into co select new { c.Name, Sum = co.Sum(o => o.Total) } Sum = co.Sum( o => co.Total)
from t in customers .groupJoin(orders, –> c => c.CustomerID, o => o.CustomerID, (c, co) => new { c.Name,
Select t
Linq.book Page 46 Mercredi, 18. février 2009 7:58 07
46
LINQ et C# 2008
Partie I
En utilisant les étapes de traduction suivantes, le code est finalement traduit en : Customers .GroupJoin(orders, c => c.CustomerIDc.CustomerID, o => o.CustomerID, (c, co) => new { c.Name, Sum = co.Sum(o = o.Total) })
Si l’expression de requête contient une clause from suivie d’une clause join mais pas d’une clause into suivie par quelque chose d’autre qu’une clause select, la traduction suivante est opérée (* est un opérateur transparent) : from e1 in s1 join e2 in s2 on k1 equals k2 …
–>
from * in from e1 in s1 join e2 in s2 on k1 equals k2 select new { e1, e2 }
Le motif généré correspond au premier motif de la section "Clauses Join" : la requête contient une clause from suivie d’une clause join. La clause into est absente, mais une clause select est présente. Une nouvelle traduction sera donc opérée. Si l’expression de requête contient une clause from suivie d’une clause join, puis d’une clause into suivie par quelque chose d’autre qu’une clause select, la traduction suivante est opérée (* est un opérateur transparent) : from e1 in s1 join e2 in s2 on k1 equals k2 into i …
–>
from * from e1 in s1 join e2 in s2 on k1 equals k2 into i select new { e1, i }
Le motif généré correspond au deuxième motif de la section "Clauses Join" : on trouve une clause from suivie d’une clause join, d’une clause into puis d’une clause select. Une nouvelle traduction sera donc opérée. Les clauses Let et Where Si l’expression de requête contient une clause from suivie immédiatement d’une clause let, la traduction suivante est effectuée (* est un identificateur transparent) : from e in s let l = v
from * in from e1 in s1 select new { e, l = v }
–>
Voici un exemple (t est un identificateur généré par le compilateur. Il reste invisible et inaccessible par le code) : from c in customers let cityStateZip = c.City + ", " + c.State + " " + c.Zip select new { c.Name, cityStateZip }
select new { c.Name, cityStateZip }
–>
from * in from c in customers select new { c, cityStateZip = c.City + ", " + c.State + " " + c.Zip }
Linq.book Page 47 Mercredi, 18. février 2009 7:58 07
Chapitre 2
Améliorations de C# 3.0 pour LINQ
47
Une fois les autres étapes de traduction terminées, le code est finalement transformé en : customers .Select(c => new { c, cityStateZip = c.City + ", " + c.State + " " + c.Zip }) .Select(t => new { t.c.Name, t.cityStateZip })
Si l’expression de requête contient une clause from suivie d’une clause where, la traduction suivante est opérée : from e in s where w
from e in s .Where(e => w)
–>
Voici un exemple : from c in customers where c.Country == "USA" select new { c.Name, c.Country }
from c in customers .Where (c => c.Country == "USA") select new { c.Name, c.Country }
–>
Une fois les autres étapes de traduction terminées, le code est finalement transformé en : customers .Where(c => c.Country == "USA") .Select(c => new { c.Name, c.Country })
Clauses from multiples Si l’expression de requête contient deux clauses from suivies par une requête select, la traduction suivante est opérée : from e1 in s1 from e2 in s2 select f
–>
from c in s1 .SelectMany(e1 => from e2 in s2 select f) select c
Voici un exemple (t est une variable temporaire générée par le compilateur) : from c in customers from o in c.Orders select new { c.Name, o.OrderID, o.OrderDate }
–>
from t in customers .SelectMany(c => from o in c.Orders select new { c.Name, o.OrderID, o.OrderDate }) Select t
Une fois les autres étapes de traduction terminées, le code est finalement transformé en : customers .SelectMany(c => c.Orders.Select(o => new { c.Name, o.OrderID, o.OrderDate }))
Si l’expression de requête contient deux clauses from suivies par quelque chose d’autre qu’une clause select, la traduction suivante est opérée (* est un identificateur transparent) : from e1 in s1 from e2 in s2 …
–>
from * in from e1 in s1 from e2 in s2 select new { e1, e2 }
Linq.book Page 48 Mercredi, 18. février 2009 7:58 07
48
LINQ et C# 2008
Partie I
Voici un exemple (* est un identificateur transparent) : from c in customers from o in c.Orders orderby o.OrderDate descending select new {c.Name, o.OrderID, o.OrderDate }
–>
from * in from c in customers from o in c.Orders select new { c, o } orderby o.OrderDate descending select new { c.Name, o.OrderID, o.OrderDate }
Le code ainsi obtenu doit réitérer la première étape de traduction. En effet, le motif résultant contient une clause from suivie par une autre clause from puis par une clause select, ce qui correspond au premier modèle de la section "Clauses from multiples". Il s’agit donc d’un exemple dans lequel certaines étapes doivent être appelées plusieurs fois pour que la traduction soit complète. Une fois toutes les étapes de traduction terminées, le code est finalement transformé en : customers .SelectMany(c => c.Orders.Select(o => new { c, o })) .OrderByDescending(t => t.o.OrderDate) .Select(t => new { t.c.Name, t.o.OrderID, t.o.OrderDate})
Clauses OrderBy Les traductions suivantes prennent place dans un tri ascendant : from e in s orderby o1, o2
–>
from e in s .OrderBy(e => o1).ThenBy(e => o2)
Voici un exemple : from c in customers orderby c.Country, c. Name select new { c.Country, c.Name} select new { c.Country, c.Name }
from c in customers .OrderBy(c => c.Country) .TheBy(c.Name)
Une fois toutes les étapes de traduction terminées, le code est finalement transformé en : customers .OrderBy(c => c.Country) .ThenByDescending(c.Name) .Select(c => new { c.Country, c.Name }
Clauses Select Dans une expression de requête, si vous sélectionnez la totalité de l’élément stocké dans la séquence, l’élément sélectionné a le même identificateur que la variable d’énumération de la séquence. La traduction suivante est opérée : from e in s select f
–>
s
–>
customers
Voici un exemple : from c in customers select c
Linq.book Page 49 Mercredi, 18. février 2009 7:58 07
Chapitre 2
Améliorations de C# 3.0 pour LINQ
49
Si l’élément sélectionné n’a pas le même identificateur que la variable d’énumération de la séquence, cela signifie qu’il n’est pas sélectionné en totalité (la sélection peut porter sur un membre de l’élément ou sur un type anonyme construit à partir de plusieurs membres de l’élément). La traduction suivante est effectuée : from e in s select f
–>
s.Select(e => f)
–>
customers.Select(c => c.Name)
Voici un exemple : from c in customers select c.Name
Clauses group Dans l’expression de requête, si l’élément regroupé a le même identificateur que l’énumérateur de la séquence, cela signifie que le regroupement porte sur la totalité de l’élément stocké dans la séquence. La traduction est la suivante : from e in s group g by k
–>
s.GroupBy(e => k)
–>
customers.GroupBy(c => c.Country)
Voici un exemple : from c in customers group c by c.Country
Si l’élément regroupé n’a pas le même identificateur que l’énumérateur de la séquence, cela signifie qu’il n’est pas regroupé en totalité. La traduction suivante est effectuée : from e in s group g by k
–>
s.GroupBy(e => k, e => g)
–>
customers .GroupBy(c => c.Country, c => new { c.Country c.Name })
Voici un exemple : from c in customers group new { c.Country, c. Name} by c.Country
Toutes les étapes de la traduction ont été effectuées et l’expression de requête a été entièrement traduite en une notation "à point" traditionnelle.
Résumé De nombreuses fonctionnalités ont été ajoutées au langage C#. Bien que ces ajouts aient été dictés par l’implémentation de LINQ, vous avez tout intérêt à les utiliser en dehors du contexte LINQ. Les expressions d’initialisation d’objets et de collections sont particulièrement intéressantes, car elles réduisent la taille du code de façon drastique. Cette fonctionnalité, combinée avec le mot-clé var et aux types anonymes, facilite grandement la création de données et de types de données à la volée.
Linq.book Page 50 Mercredi, 18. février 2009 7:58 07
50
LINQ et C# 2008
Partie I
Les méthodes d’extension permettent d’ajouter des fonctionnalités aux classes scellées et aux classes dont vous n’avez pas le code source. Si elles n’éliminent pas la raison d’être des méthodes anonymes, les expressions lambda représentent une nouvelle façon de définir de nouvelles fonctionnalités, simplement et de façon concise. Lorsque vous commencerez à les utiliser, vous serez peut-être déconcerté, mais, le temps aidant, vous les apprécierez à leur juste valeur. Les arbres d’expressions permettent aux éditeurs de logiciels tiers de conserver un mode de stockage propriétaire tout en supportant les performances avancées de LINQ. Les méthodes partielles ajoutent un mécanisme de gestion d’événements ultraléger au langage C#. Elles sont utilisées pour accéder à des moments clés dans les classes d’entités LINQ to SQL. Si les expressions de requête peuvent sembler confuses de prime abord, il ne faut pas bien longtemps pour qu’un développeur se sente à l’aise à leur contact. Elles ont en effet un air de parenté avec les requêtes SQL. Chacune de ces améliorations du langage est intéressante en soi, mais c’est leur utilisation conjointe qui est à la base de LINQ. LINQ devrait être la prochaine grande tendance en programmation. Les développeurs .NET apprécieront certainement de pouvoir l’inscrire dans leur CV. En tout cas, moi, j’en suis fier ! Vous avez maintenant une idée de ce qu’est LINQ, ainsi que des fonctionnalités et syntaxes C# afférentes. Il est temps de passer à la prochaine étape. En tournant les pages, vous allez apprendre à appliquer des requêtes LINQ à des collections en mémoire (array ou arraylist, par exemple) et aux collections génériques de C# 2.0, et vous découvrirez différentes fonctions pour alimenter vos requêtes. Cette portion de LINQ est aujourd’hui connue sous le nom de "LINQ to Objects".
Linq.book Page 51 Mercredi, 18. février 2009 7:58 07
II LINQ to Objects
Linq.book Page 52 Mercredi, 18. février 2009 7:58 07
Linq.book Page 53 Mercredi, 18. février 2009 7:58 07
3 Introduction à LINQ to Objects Listing 3.1 : Une requête LINQ to Objects élémentaire. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string president = presidents.Where(p => p.StartsWith("Lin")).First(); Console.WriteLine(president);
INFO Ce code a été ajouté au prototype d’une application console Visual Studio 2008.
Le Listing 3.1 donne une idée de ce qu’est LINQ to Objects : par son intermédiaire, il est possible d’interroger des données en mémoire à l’aide de requêtes proches du langage SQL. Lancez le programme avec Ctrl+F5. Vous obtenez le résultat suivant : Lincoln
Vue d’ensemble de LINQ to Objects Si LINQ est aussi agréable et facile à utiliser, c’est en partie parce qu’il est parfaitement intégré dans le langage C#. Plutôt qu’avoir à composer avec de nouvelles classes spécifiques à LINQ, vous pouvez utiliser les mêmes collections1 et tableaux que précédemment. Vous avez donc les avantages inhérents à LINQ sans devoir retoucher (ou très 1. Les collections doivent implémenter l’interface IEnumerable ou IEnumerable pour pouvoir être interrogeables par LINQ.
Linq.book Page 54 Mercredi, 18. février 2009 7:58 07
54
LINQ to Objects
Partie II
peu) le code existant. LINQ to Objects s’exécute à travers l’interface IEnumerable, les séquences et les opérateurs de requête standard. À titre d’exemple, pour trier un tableau d’entiers, vous pouvez utiliser une requête LINQ, tout comme s’il s’agissait d’une requête SQL. Un autre exemple. Si vous voulez trouver un objet Customer spécifique dans un ArrayList of Customer, LINQ to Objects est assurément la réponse. Pour beaucoup d’entre vous, les chapitres sur LINQ to Objects seront utilisés en tant que référence. Ils ont été construits dans cette optique et je vous conseille de les parcourir en totalité. Ne vous contentez pas de lire les sections des seuls opérateurs qui vous intéressent, sans quoi votre formation sera incomplète.
IEnumerable, séquences et opérateurs de requête standard IEnumerable, prononcé "Iénumérable de T", est une interface implémentée par les tableaux et les classes de collections génériques de C# 2.0. Cette interface permet d’énumérer les éléments d’une collection.
Une séquence est un terme logique d’une collection qui implémente l’interface IEnumerable. Si vous avez une variable de type IEnumerable, vous pouvez dire que vous avez une séquence de T. Par exemple, si vous avez un IEnumerable de string, ce qui s’écrit IEnumerable, vous pouvez dire que vous avez une séquence de string. INFO Toutes les variables déclarées en tant que IEnumerable sont considérées comme séquences de T.
La plupart des opérateurs de requête standard sont des méthodes d’extension de la classe statique System.Linq.Enumerable et ont un premier argument prototypé par un IEnumerable. Étant donné que ces opérateurs sont des méthodes d’extension, il est préférable de les appeler à travers une variable de type IEnumerable plutôt que passer une variable de type IEnumerable en premier argument. Les méthodes d’opérateurs de requête standard de la classe System.Linq.Enumerable qui ne sont pas des méthodes d’extension sont des méthodes statiques. Elles doivent être appelées dans la classe System.Linq.Enumerable. La combinaison de ces méthodes d’opérateurs de requête standard vous permet d’effectuer des requêtes complexes sur une séquence IEnumerable. Les collections héritées – ces collections non génériques qui existaient avant C# 2.0 – supportent l’interface IEnumerable, et non l’interface IEnumerable. Cela signifie que vous ne pouvez pas appeler directement ces méthodes d’extension dont le premier
Linq.book Page 55 Mercredi, 18. février 2009 7:58 07
Chapitre 3
Introduction à LINQ to Objects
55
argument est un IEnumerable sur une collection héritée. Cependant, vous pouvez toujours exécuter des requêtes LINQ sur des collections héritées en invoquant l’opérateur de requête standard Cast ou OfType. Cet opérateur produira une séquence qui implémente l’interface IEnumerable, vous permettant ainsi d’accéder à la panoplie complète des opérateurs de requête standard. INFO Utilisez les opérateurs Cast ou OfType pour exécuter des requêtes LINQ sur des collections C# héritées et non génériques.
Pour accéder aux opérateurs de requête standard, vous devez ajouter une directive using System.Linq; dans votre code (si cette dernière n’est pas déjà présente). Il n’est pas nécessaire d’ajouter une référence à un assembly car le code nécessaire est contenu dans l’assembly System.Core.dll, qui est automatiquement ajouté aux projets par Visual Studio 2008.
IEnumerable, yield et requêtes différées La plupart des opérateurs de requête standard sont prototypés pour retourner un IEnumerable (une séquence). Mais, attention, les éléments de la séquence ne sont pas retournés dès l’exécution de l’opérateur : ils ne seront "cédés" que lors de l’énumération de la séquence. C’est la raison pour laquelle on dit que ces requêtes sont différées. Le terme "céder" fait référence au mot-clé yield, ajouté dans C# 2.0 pour faciliter l’écriture d’énumérateurs. Examinez le code du Listing 3.2. Listing 3.2 : Une requête triviale. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.Where(p => p.StartsWith("A")); foreach(string item in items) Console.WriteLine(item);
La requête apparaît en gras dans ce listing. Lorsque cette ligne s’exécute, elle retourne un objet. Ce n’est que pendant l’énumération de cet objet que la requête Where est réellement exécutée. Si une erreur se produit dans la requête, elle ne sera détectée qu’à l’énumération.
Linq.book Page 56 Mercredi, 18. février 2009 7:58 07
56
LINQ to Objects
Partie II
Voici le résultat de la requête : Adams Arthur
Cette requête s’est comportée comme prévu. Nous allons maintenant introduire une erreur intentionnelle dans la requête. Le code qui suit va essayer d’effectuer un tri en se basant sur le cinquième caractère du nom des présidents. Lorsque l’énumération atteint un nom dont la longueur est inférieure à cinq caractères, une exception sera générée. Rappelez-vous que l’exception ne se produira pas avant l’énumération de la séquence résultat (voir Listing 3.3). Listing 3.3 : Une requête triviale avec une exception introduite intentionnellement. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.Where(s => Char.IsLower(s[4])); Console.WriteLine("After the query."); foreach (string item in items) Console.WriteLine(item);
Ce code ne produit aucune erreur à la compilation, mais voici les résultats affichés dans la console : Adams Arthur Buchanan Unhandled Exception: System.IndexOutOfRangeException: Index was outside the bounds of the array. …
Tout se passe bien jusqu’au quatrième élément. Bush produit une exception lors de l’énumération. La leçon à tirer de cet exemple est qu’une compilation réussie ne suffit pas pour assurer qu’une requête est vierge de tout bogue. Sachez par ailleurs que, les requêtes qui retournent un IEnumerable étant différées, il suffit d’exécuter une seule fois le code de la requête. Vous pouvez ensuite énumérer les données autant de fois que vous le souhaitez. Si, entre deux énumérations, les données changent, les résultats seront différents (voir Listing 3.4). Listing 3.4 : Un exemple dans lequel les résultats de la requête changent d’une énumération à l’autre. // Création d’un tableau de int int[] intArray = new int[] { 1,2,3 };
Linq.book Page 57 Mercredi, 18. février 2009 7:58 07
Chapitre 3
Introduction à LINQ to Objects
57
IEnumerable ints = intArray.Select(i => i); // Affichage des résultats foreach(int i in ints) Console.WriteLine(i); // Modification d’un élément dans la source intArray[0] = 5; Console.WriteLine("---------"); // Nouvel affichage des résultats foreach(int i in ints) Console.WriteLine(i);
Lorsque l’opérateur Select est appelé, un objet est retourné et stocké dans la variable IEnumerable ints. La requête n’a pas encore été exécutée. Elle est juste stockée dans l’objet ints. Les résultats de la requête n’existent donc pas encore, mais l’objet ints sait comment les obtenir. Lorsque l’instruction foreach est appelée pour la première fois, ints exécute la requête et obtient successivement les différents éléments de la séquence. Un peu plus bas, un des éléments est modifié dans son tableau d’origine, intArray[]. L’instruction foreach est appelée à nouveau. Cela provoque une nouvelle exécution de la requête. Cette énumération retourne tous les éléments de intArray[] et donc également l’élément qui a été modifié. Dans cet ouvrage (et dans beaucoup d’autres relatifs à LINQ), vous pourrez lire qu’une requête retourne une séquence et non un objet qui implémente l’interface IEnumerable. Ceci est un abus de langage : les éléments de la séquence ne sont obtenus qu’à son énumération. Voici les résultats affichés par ce code : 1 2 3 --------5 2 3
La requête n’a été appelée qu’une fois et, pourtant, les résultats des deux énumérations sont différents. Cela confirme – si besoin était – que la requête est bien différée. Dans le cas contraire, les résultats des deux énumérations seraient identiques. Selon les cas, ceci peut être un avantage ou un inconvénient. Si vous ne voulez pas que la requête soit différée, utilisez un opérateur qui ne retourne pas un IEnumerable. Par exemple ToArray, ToList, ToDictionary ou ToLookup. Les résultats seront alors figés dans une mémoire cache et ne changeront pas.
Linq.book Page 58 Mercredi, 18. février 2009 7:58 07
58
LINQ to Objects
Partie II
Le Listing 3.5 est le même que le précédent, à un détail près : en utilisant un opérateur ToList, la requête retourne non pas un IEnumerable mais un List. Listing 3.5 : En retournant un objet List, la requête est exécutée immédiatement et les résultats sont mis dans un cache. // Création d’un tableau de int int[] intArray = new int[] { 1,2,3 }; List ints = intArray.Select(i => i).ToList; // Affichage des résultats foreach(int i in ints) Console.WriteLine(i); // Modification d’un élément dans la source intArray[0] = 5; Console.WriteLine("---------"); // Nouvel affichage des résultats foreach(int i in ints) Console.WriteLine(i);
Voici les résultats : 1 2 3 --------1 2 3
Comme on pouvait s’y attendre, les résultats ne changent pas d’une énumération à la suivante. La requête est donc bien exécutée immédiatement. L’opérateur Select est différé, et l’opérateur ToList ne l’est pas. En appliquant ToList au résultat du Select, l’objet retourné par Select est énuméré et la requête n’est plus différée. Délégués Func Plusieurs des opérateurs de requête standard sont prototypés pour accepter un délégué Func comme argument. Cela vous évite d’avoir à déclarer des délégués explicitement. Voici les déclarations de délégués Func : public public public public public
delegate delegate delegate delegate delegate
TR TR TR TR TR
Func
(); Func(T0 a0); Func(T0 a0, T1 a1); Func(T0 a0, T1 a1, T2 a2); Func(T0 a0, T1 a1, T2 a2, T3 a3);
Dans ces déclarations, TR fait référence au type de donnée retournée. Cet argument est toujours le dernier de la liste. Quant à T0 à T3, ils représentent les paramètres passés à la méthode. Plusieurs déclarations sont nécessaires, car tous les opérateurs de requête
Linq.book Page 59 Mercredi, 18. février 2009 7:58 07
Chapitre 3
Introduction à LINQ to Objects
59
standard n’utilisent pas le même nombre de paramètres en entrée. Dans tous les cas, les délégués admettent un nombre maximal de 4 paramètres. Examinons un des prototypes de l’opérateur Where : public static IEnumerable Where( this IEnumerable source, Func predicate);
En observant le prédicat Func, vous pouvez en déduire que la méthode ou l’expression lambda n’accepte qu’un seul argument, T, et retourne un booléen. Cette dernière déduction vient du fait que le type de retour est toujours le dernier paramètre de la liste. Vous utiliserez la déclaration Func, comme indiqué dans le Listing 3.6. Listing 3.6 : Cet exemple utilise une déclaration de délégué Func. // Création d’un tableau d’entiers int[] ints = new int[] { 1,2,3,4,5,6 }; // Déclaration du délégué Func GreaterThanTwo = i => i > 2; // Mise en place (et non exécution) de la requête IEnumerable intsGreaterThanTwo = ints.Where(GreaterThanTwo); // Affichage des résultats foreach(int i in intsGreaterThanTwo) Console.WriteLine(i);
L’exécution de ce code produit les résultats suivants : 2 4 5 6
Les opérateurs de requête standard Le Tableau 3.1 dresse la liste alphabétique des principaux opérateurs de requête standard. Les prochains chapitres vont séparer les opérateurs différés des opérateurs non différés. Ce tableau facilitera donc votre repérage dans le livre. Tableau 3.1 : Les opérateurs de requête standard
Opérateur
Objet
Aggregate
Agrégat
All
Dénombrement
Any
Dénombrement
AsEnumerable
Conversion
Différé
u
Supporte l’expression de requête
Linq.book Page 60 Mercredi, 18. février 2009 7:58 07
60
LINQ to Objects
Partie II
Tableau 3.1 : Les opérateurs de requête standard (suite)
Opérateur
Objet
Différé
Average
Agrégat
Cast
Conversion
u
Concat
Concaténation
u
Contains
Dénombrement
Count
Agrégat
DefaultIfEmpty
Élément
u
Distinct
Ensemble
u
ElementAt
Élément
Supporte l’expression de requête
ElementAtOrDefault Élément Empty
Génération
u
Except
Ensemble
u
First
Élément
FirstOrDefault
Élément
GroupBy
Regroupement
u
u
GroupJoin
Jointure
u
u
Intersect
Ensemble
u
Join
Jointure
u
Last
Élément
LastOrDefault
Élément
LongCount
Agrégat
Max
Agrégat
Min
Agrégat
OfType
Conversion
u
OrderBy
Tri
u
u
OrderByDescending
Tri
u
u
Range
Génération
u
Repeat
Génération
u
Reverse
Tri
u
Select
Projection
u
u
u
Linq.book Page 61 Mercredi, 18. février 2009 7:58 07
Chapitre 3
Introduction à LINQ to Objects
61
Tableau 3.1 : Les opérateurs de requête standard (suite)
Opérateur
Objet
Différé
Supporte l’expression de requête
SelectMany
Projection
u
u
SequenceEqual
Égalité
Single
Élément
SingleOrDefault
Élément
Skip
Partage
u
SkipWhile
Partage
u
Sum
Agrégat
Take
Partage
u
TakeWhile
Partage
u
ThenBy
Tri
u
u
ThenByDescending
Tri
u
u
ToArray
Conversion
ToDictionary
Conversion
ToList
Conversion
ToLookup
Conversion
Union
Ensemble
u
Where
Restriction
u
u
Résumé Ce chapitre a introduit le terme "séquence" et le type de données associé, IEnumerable. Si vous n’êtes pas à l’aise avec ces expressions, soyez rassuré : elles deviendront vite une seconde nature pour vous ! Pour l’instant, contentez-vous de voir les IEnumerable comme une séquence d’objets auxquels vous allez appliquer des traitements via des méthodes. Ce chapitre a mis en évidence l’importance de l’exécution différée des requêtes. Selon les cas, elle peut constituer un avantage ou un inconvénient. Cette caractéristique est vraiment importante. C’est pourquoi nous allons séparer les opérateurs différés (au Chapitre 4) des opérateurs non différés (au Chapitre 5) dans la suite de cet ouvrage.
Linq.book Page 62 Mercredi, 18. février 2009 7:58 07
Linq.book Page 63 Mercredi, 18. février 2009 7:58 07
4 Les opérateurs différés Au chapitre précédent, nous nous sommes intéressés aux séquences, aux types de données qui les représentent et aux conséquences de leur exécution différée. Conscient de l’importance de ce dernier point, j’ai choisi de traiter des opérateurs différés et non différés dans deux chapitres séparés. Ce chapitre va s’intéresser aux opérateurs différés, par groupes fonctionnels. Il est facile de reconnaître un tel opérateur : il retourne un IEnumerable ou un IOrderEnumerable. Attention, pour pouvoir exécuter les exemples de ce chapitre, assurez-vous que vous avez référencé les espaces de noms (directive using), les assemblies et les codes communs nécessaires !
Espaces de noms référencés Les exemples de ce chapitre vont utiliser les espaces de noms System.Linq, System.Collections, System.Collections.Generic et System.Data.Linq. Si elles ne sont pas déjà présentes, vous devez donc ajouter les directives using suivantes dans votre code : using System.Linq; using System.Collections; using System.Collections.Generic; using System.Data.Linq;
Si vous parcourez le code source (disponible sur le site www.pearson.fr), vous verrez que j’ai également ajouté une directive using sur l’espace de noms System.Diagnostic. Cette directive n’est pas nécessaire si vous saisissez directement les exemples de ce chapitre. Elle n’est là que pour les besoins propres du code source.
Linq.book Page 64 Mercredi, 18. février 2009 7:58 07
64
LINQ to Objects
Partie II
Assemblies référencés Pour que le code de ce chapitre fonctionne, vous devez également référencer l’assembly System.Data.Linq.dll.
Classes communes Certains exemples de ce chapitre nécessitent des classes additionnelles pour fonctionner en totalité. En voici la liste. La classe Employee permet de travailler sur les employés d’une entreprise. Elle contient des méthodes statiques qui retournent un tableau d’employés de type ArrayList. public class Employee { public int id; public string firstName; public string lastName; public static ArrayList GetEmployeesArrayList() { ArrayList al = new ArrayList(); al.Add(new Employee al.Add(new Employee al.Add(new Employee al.Add(new Employee al.Add(new Employee return (al);
{ { { { {
id id id id id
= = = = =
1, firstName = 2, firstName = 3, firstName = 4, firstName = 101, firstName
"Joe", lastName = "Rattz" }); "William", lastName = "Gates" }); "Anders", lastName = "Hejlsberg" }); "David", lastName = "Lightman" }); = "Kevin", lastName = "Flynn" });
} public static Employee[] GetEmployeesArray() { return ((Employee[])GetEmployeesArrayList().ToArray()); } }
La classe EmployeeOptionEntry représente le montant des stock-options des employés. Elle contient une méthode statique qui retourne un tableau de stock-options. public class EmployeeOptionEntry { public int id; public long optionsCount; public DateTime dateAwarded; public static EmployeeOptionEntry[] GetEmployeeOptionEntries() { EmployeeOptionEntry[] empOptions = new EmployeeOptionEntry[] { new EmployeeOptionEntry { id = 1, optionsCount = 2, dateAwarded = DateTime.Parse("1999/12/31") }, new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("1992/06/30") }, new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("1994/01/01") }, new EmployeeOptionEntry { id = 3, optionsCount = 5000, dateAwarded = DateTime.Parse("1997/09/30") },
Linq.book Page 65 Mercredi, 18. février 2009 7:58 07
Chapitre 4
new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("2003/04/01") new EmployeeOptionEntry { id = 3, optionsCount = 7500, dateAwarded = DateTime.Parse("1998/09/30") new EmployeeOptionEntry { id = 3, optionsCount = 7500, dateAwarded = DateTime.Parse("1998/09/30") new EmployeeOptionEntry { id = 4, optionsCount = 1500, dateAwarded = DateTime.Parse("1997/12/31") new EmployeeOptionEntry { id = 101, optionsCount = 2, dateAwarded = DateTime.Parse("1998/12/31") };
Les opérateurs différés
65
},
},
},
},
}
return (empOptions); } }
Les opérateurs différés, par groupes fonctionnels Dans les pages qui suivent, nous avons organisé les différents opérateurs de requête standard différés par grands groupes fonctionnels. Restriction Les opérateurs de restriction sont utilisés pour ajouter ou enlever des éléments dans une séquence d’entrée. L’opérateur Where L’opérateur Where est utilisé pour filtrer des éléments d’une séquence.
Prototypes Deux prototypes de l’opérateur Where seront étudiés dans ce livre. Premier prototype public static IEnumerable Where( this IEnumerable source, Func predicate);
Ce prototype demande deux paramètres : une séquence d’entrée et un prédicat (délégué générique). Il renvoie un objet énumérable dont seuls les éléments pour lesquels le prédicat renvoie true sont accessibles. INFO Comme Where est une méthode d’extension, la séquence d’entrée n’est pas réellement passée dans le premier argument : tant que Where est appliqué sur un objet du même type que le premier argument, ce dernier peut être remplacé par le mot-clé this.
Linq.book Page 66 Mercredi, 18. février 2009 7:58 07
66
LINQ to Objects
Partie II
Lorsque vous appelez la méthode Where, un délégué est passé à un prédicat. Cette dernière doit accepter une entrée de type T (où T est le type des éléments contenus dans la séquence d’entrée) et retourner un booléen. L’opérateur Where communique chacun des éléments contenus dans la séquence d’entrée au prédicat. L’élément n’est retourné dans la séquence de sortie que dans le cas où le prédicat retourne la valeur true. Second prototype public static IEnumerable Where( this IEnumerable source, Func predicate);
Ce second prototype est semblable au premier si ce n’est que le prédicat reçoit un argument complémentaire entier. Cet argument correspond à l’index de l’élément dans la séquence. Il commence à zéro et se termine au nombre d’éléments de la séquence moins un. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Le Listing 4.1 est un exemple d’appel du premier prototype Where. Listing 4.1 : Un exemple d’appel du premier prototype Where. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable sequence = presidents.Where(p => p.StartsWith("J")); foreach (string s in sequence) Console.WriteLine("{0}", s);
Cet exemple applique la méthode Where à la séquence d’entrée et définit une expression lambda. Cette dernière retourne un booléen dont la valeur indique si l’élément doit ou ne doit pas être inclus dans la séquence de sortie. Dans cet exemple, seuls les éléments qui commencent par la lettre "J" seront retournés. Voici les résultats affichés dans la console lorsque vous appuyez sur Ctrl+F5 : Jackson Jefferson Johnson
Le Listing 4.2 est un exemple d’appel du second prototype Where. Ce code se contente d’utiliser l’index i pour filtrer les éléments de la séquence. Seuls les éléments d’indice impair seront retournés.
Linq.book Page 67 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
67
Listing 4.2 : Un exemple d’appel du second prototype Where. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable sequence = presidents.Where((p, i) => (i & 1) == 1); foreach (string s in sequence) Console.WriteLine("{0}", s);
L’exécution de ce code produit la sortie suivante dans la console : Arthur Bush Cleveland Coolidge Fillmore Garfield Harding Hayes Jackson Johnson Lincoln McKinley Nixon Polk Roosevelt Taylor Tyler Washington
Projection Les opérateurs de projection retournent une séquence d’éléments sélectionnés dans la séquence d’entrée ou instanciés à partir de portions d’éléments de la séquence d’entrée. Le type des éléments de la séquence de sortie peut être différent du type des éléments de la séquence d’entrée. L’opérateur Select L’opérateur Select est utilisé pour créer une séquence de sortie S d’un type d’élément en partant d’une séquence d’entrée T d’un autre type d’élément. Ces deux types ne sont pas forcément identiques.
Prototypes Deux prototypes de l’opérateur Select seront étudiés dans ce livre.
Linq.book Page 68 Mercredi, 18. février 2009 7:58 07
68
LINQ to Objects
Partie II
Premier prototype public static IEnumerable Select( this IEnumerable source, Func selector);
Ce prototype admet deux arguments en entrée : une séquence source et un délégué. Il retourne un objet dont l’énumération produit une séquence d’éléments de type S. Comme signalé précédemment, les types T et S ne sont pas forcément identiques. Pour utiliser ce prototype, vous devez passer un délégué à une méthode de sélection via l’argument selector. Ce dernier doit accepter un élément de type T (où T est le type des éléments contenus dans la séquence d’entrée) et retourner un élément de type S. L’opérateur Select appelle la méthode selector pour chacun des éléments de la séquence d’entrée. La méthode selector choisit une portion de l’élément passé, crée un nouvel élément, éventuellement d’un autre type (y compris le type anonyme) et le retourne. Second prototype public static IEnumerable Select( this IEnumerable source, Func selector);
Ce second prototype est semblable au premier si ce n’est qu’un argument complémentaire de type entier est passé au délégué. Cet argument correspond à l’index de l’élément dans la séquence (l’index du premier élément est 0). Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Le Listing 4.3 est un exemple d’appel du premier prototype. Listing 4.3 : Un exemple d’utilisation du premier prototype. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable nameLengths = presidents.Select(p => p.Length); foreach (int item in nameLengths) Console.WriteLine(item);
La méthode selector est passée par l’intermédiaire d’une expression lambda. Cette dernière retourne la longueur des éléments de la séquence d’entrée. Remarquez que les types des séquences d’entrée et de sortie diffèrent : string pour la première, integer pour la deuxième.
Linq.book Page 69 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
69
Voici le résultat de ce code lorsque vous appuyez sur Ctrl+F5 : 5 6 8 4 6 9 7 8 10 8 4 8 5 7 8 5 6 7 9 7 7 7 7 8 6 5 6 4 6 9 4 6 6 5 9 10 6
Cet exemple est très simple, puisqu’il ne génère aucune classe. Le Listing 4.4 donne un exemple plus élaboré du premier prototype de l’opérateur Select. Listing 4.4 : Un autre exemple d’utilisation du premier prototype. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; var nameObjs = presidents.Select(p => new { p, p.Length }); foreach (var item in nameObjs) Console.WriteLine(item);
Ici, l’expression lambda instancie un nouveau type anonyme. Le compilateur génère dynamiquement un objet de type anonyme qui contient un string p et un int
Linq.book Page 70 Mercredi, 18. février 2009 7:58 07
70
LINQ to Objects
Partie II
p.Length, et la méthode selector retourne cet objet. Étant donné que l’élément retourné est de type anonyme, il n’existe aucun type pour y faire référence. Contrairement à l’exemple précédent, où la séquence de sortie avait été affectée à un IEnumerable, il est impossible d’affecter la séquence de sortie à un IEnumerable d’un type connu. C’est la raison pour laquelle le mot-clé var a été utilisé. INFO Les opérateurs de projection dont les méthodes selector instancient des types anonymes doivent affecter leur séquence de sortie à une variable déclarée avec le mot-clé var.
Voici la sortie dans la console lorsque vous appuyez sur Ctrl+F5 : { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { {
p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
Adams, Length = 5 } Arthur, Length = 6 } Buchanan, Length = 8 } Bush, Length = 4 } Carter, Length = 6 } Cleveland, Length = 9 } Clinton, Length = 7 } Coolidge, Length = 8 } Eisenhower, Length = 10 } Fillmore, Length = 8 } Ford, Length = 4 } Garfield, Length = 8 } Grant, Length = 5 } Harding, Length = 7 } Harrison, Length = 8 } Hayes, Length = 5 } Hoover, Length = 6 } Jackson, Length = 7 } Jefferson, Length = 9 } Johnson, Length = 7 } Kennedy, Length = 7 } Lincoln, Length = 7 } Madison, Length = 7 } McKinley, Length = 8 } Monroe, Length = 6 } Nixon, Length = 5 } Pierce, Length = 6 } Polk, Length = 4 } Reagan, Length = 6 } Roosevelt, Length = 9 } Taft, Length = 4 } Taylor, Length = 6 } Truman, Length = 6 } Tyler, Length = 5 } Van Buren, Length = 9 } Washington, Length = 10 } Wilson, Length = 6 }
Dans son état actuel, ce code a un inconvénient : il ne permet pas d’agir sur les membres de la classe anonyme générée dynamiquement. Cependant, grâce à la fonctionnalité d’initialisation d’objets de C# 3.0, il est possible de spécifier les noms des membres de la classe anonyme dans une expression lambda (voir Listing 4.5).
Linq.book Page 71 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
71
Listing 4.5 : Un troisième exemple du premier prototype de l’opérateur Select. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; var nameObjs = presidents.Select(p => new { LastName = p, Length = p.Length }); foreach (var item in nameObjs) Console.WriteLine("{0} contient {1} caractères", item.LastName, item.Length);
Comme vous pouvez le voir, le nom des membres a été spécifié dans l’expression lambda, et on a accédé aux membres par leurs noms dans la méthode Console.WriteLine. Voici le résultat de ce code : Adams contient 5 caractères Arthur contient 6 caractères Buchanan contient 8 caractères Bush contient 4 caractères Carter contient 6 caractères Cleveland contient 9 caractères Clinton contient 7 caractères Coolidge contient 8 caractères Eisenhower contient 10 caractères Fillmore contient 8 caractères Ford contient 4 caractères Garfield contient 8 caractères Grant contient 5 caractères Harding contient 7 caractères Harrison contient 8 caractères Hayes contient 5 caractères Hoover contient 6 caractères Jackson contient 7 caractères Jefferson contient 9 caractères Johnson contient 7 caractères Kennedy contient 7 caractères Lincoln contient 7 caractères Madcontienton contient 7 caractères McKinley contient 8 caractères Monroe contient 6 caractères Nixon contient 5 caractères Pierce contient 6 caractères Polk contient 4 caractères Reagan contient 6 caractères Roosevelt contient 9 caractères Taft contient 4 caractères Taylor contient 6 caractères Truman contient 6 caractères Tyler contient 5 caractères Van Buren contient 9 caractères Washington contient 10 caractères Wilson contient 6 caractères
Pour illustrer le second prototype, nous allons insérer l’index passé à la méthode selector dans la séquence de sortie (voir Listing 4.6).
Linq.book Page 72 Mercredi, 18. février 2009 7:58 07
72
LINQ to Objects
Partie II
Listing 4.6 : Un exemple d’utilisation du second prototype de l’opérateur Select. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; var nameObjs = presidents.Select((p, i) => new { Index = i, LastName = p }); foreach (var item in nameObjs) Console.WriteLine("{0}. {1}", item.Index + 1, item.LastName);
Pour chaque élément de la séquence d’entrée, cet exemple affiche la valeur de l’index augmentée de 1, puis le nom de l’élément. Voici les résultats affichés dans la console : 1. Adams 2. Arthur 3. Buchanan 4. Bush 5. Carter … 34. Tyler 35. Van Buren 36. Washington 37. Wilson
Opérateur SelectMany L’opérateur SelectMany est utilisé pour créer une ou plusieurs séquences à partir de la séquence passée en entrée. Contrairement à l’opérateur Select, qui retourne un élément en sortie pour chaque élément en entrée, SelectMany peut retourner zéro, un ou plusieurs éléments en sortie pour chaque élément en entrée.
Prototypes Deux prototypes de l’opérateur Select seront étudiés dans ce livre. Premier prototype public static IEnumerable SelectMany( this IEnumerable source, Func selector);
Ce prototype admet deux entrées : une séquence source d’éléments de type T et un délégué pour effectuer la sélection des données. Il retourne un objet dont l’énumération passe chaque élément de la séquence d’entrée au délégué. Lors de l’énumération de la méthode selector, zéro, un ou plusieurs éléments de type S sont retournés dans une séquence de sortie intermédiaire. L’opérateur SelectMany retourne les différentes séquences de sortie concaténées.
Linq.book Page 73 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
73
Second prototype public static IEnumerable SelectMany( this IEnumerable source, Func selector);
Ce prototype est en tout point semblable au précédent, si ce n’est qu’un index des éléments de la séquence d’entrée est passé à la méthode selector (l’index du premier élément est 0). Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Le Listing 4.7 donne un exemple d’appel du premier prototype. Listing 4.7 : Un exemple du premier prototype de l’opérateur SelectMany. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable chars = presidents.SelectMany(p => p.ToArray()); foreach (char ch in chars) Console.WriteLine(ch);
Dans cet exemple, la méthode de sélection reçoit un paramètre string. En lui appliquant la méthode ToArray, on obtient un tableau de chaînes qui est transformé en une chaîne de sortie de type char. Pour une unique séquence en entrée (ici, un string), le sélecteur retourne une séquence de caractères. L’opérateur SelectMany concatène toutes ces séquences de caractères dans une seule qui devient la séquence de sortie. Voici le texte affiché dans la console suite à l’exécution du code : A d a m s A r t h u r
Linq.book Page 74 Mercredi, 18. février 2009 7:58 07
74
LINQ to Objects
Partie II
B u c h a n a nB u s h … W a s h i n g t o n W i l s o n
Cette requête est simple à comprendre, mais pas très démonstrative de la façon dont l’opérateur SelectMany est généralement utilisé. Dans le prochain exemple, nous utiliserons les classes communes Employee et EmployeeOptionEntry pour être plus proches de la réalité. L’opérateur SelectMany va être appliqué sur un tableau d’éléments Employee. Pour chacun de ces éléments, la méthode de sélection (le délégué) retournera zéro, un ou plusieurs éléments de la classe anonyme. Ces éléments contiendront les champs id et optionsCount du tableau d’éléments EmployeeOptionEntry de l’objet Employee (voir Listing 4.8). Listing 4.8 : Un exemple plus complexe du premier prototype de l’opérateur SelectMany. Employee[] employees = Employee.GetEmployeesArray(); EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries(); var employeeOptions = employees .SelectMany(e => empOptions .Where(eo => eo.id == e.id) .Select(eo => new { id = eo.id, optionsCount = eo.optionsCount })); foreach (var item in employeeOptions) Console.WriteLine(item);
Chaque employé du tableau Employee est passé dans l’expression lambda utilisée dans l’opérateur SelectMany. Par l’intermédiaire de l’opérateur Where, l’expression lambda
Linq.book Page 75 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
75
retrouve alors les éléments EmployeeOptionEntry dont le champ id correspond au champ id de l’employé actuel. Ce code effectue donc une jointure des tableaux Employee et EmployeeOptionEntry sur le champ id. L’opérateur Select de l’expression lambda crée alors un objet anonyme composé des membres id et optionsCount pour chacun des enregistrements sélectionnés dans le tableau EmployeeOptionEntry. L’expression lambda retourne donc une séquence de zéro, un ou plusieurs objets anonymes pour chacun des employés sélectionnés. Le résultat final est une séquence de séquences concaténées par l’opérateur SelectMany. Voici le résultat de ce code, affiché dans la console : { { { { { { { { {
id id id id id id id id id
= = = = = = = = =
1, optionsCount = 2, optionsCount = 2, optionsCount = 2, optionsCount = 3, optionsCount = 3, optionsCount = 3, optionsCount = 4, optionsCount = 101, optionsCount
2 } 10000 } 10000 } 10000 } 5000 } 7500 } 7500 } 1500 } = 2 }
Bien qu’un peu tiré par les cheveux, le Listing 4.9 donne un exemple d’appel du second prototype de l’opérateur SelectMany. Listing 4.9 : Un exemple d’appel du second prototype de l’opérateur SelectMany. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable chars = presidents .SelectMany((p, i) => i < 5 ? p.ToArray() : new char[] { }); foreach (char ch in chars) Console.WriteLine(ch);
L’expression lambda teste la valeur de l’index. S’il est inférieur à 5, le tableau de caractères de la chaîne passée en entrée est retourné. Voici le résultat affiché dans la console : A d a m s A r t h u r
Linq.book Page 76 Mercredi, 18. février 2009 7:58 07
76
LINQ to Objects
Partie II
B u c h a n a n B u s h C a r t e r
Cette expression lambda n’est pas particulièrement efficace, en particulier si le nombre d’éléments en entrée est élevé. Elle est en effet appelée pour chacun des éléments passés en entrée, y compris pour ceux dont l’index est supérieur à 5. Dans ce cas, un tableau vide est retourné. Pour une plus grande efficacité, vous préférerez l’opérateur Take (voir la section suivante). L’opérateur SelectMany peut également être utilisé lorsqu’il s’agit de concaténer plusieurs séquences. Reportez-vous à la section relative à l’opérateur Concat, un peu plus loin dans ce chapitre, pour avoir un exemple de concaténation. Partage Les opérateurs de partage retournent une séquence qui est un sous-ensemble de la séquence d’entrée. Opérateur Take L’opérateur Take retourne un certain nombre d’éléments de la séquence d’entrée, à partir du premier.
Prototype Un seul prototype de l’opérateur Take sera étudié dans ce livre : public static IEnumerable Take( this IEnumerable source, int count);
L’opérateur Take admet deux paramètres en entrée : une séquence source et l’entier count, qui indique combien d’éléments doivent être retournés. Il renvoie un objet dont l’énumération produira les count premiers éléments de la séquence d’entrée. Si count est plus grand que le nombre d’éléments contenus dans la séquence d’entrée, la totalité de la séquence d’entrée est retournée.
Linq.book Page 77 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
77
Exceptions L’exception ArgumentNullException est levée si la séquence source a pour valeur null. Exemples Le Listing 4.10 donne un exemple d’appel de l’opérateur Take. Listing 4.10 : Un exemple d’appel de l’unique prototype Take. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.Take(5); foreach (string item in items) Console.WriteLine(item);
Ce code retourne les cinq premiers éléments du tableau presidents : Adams Arthur Buchanan Bush Carter
Dans l’exemple précédent, j’ai indiqué que le code serait plus efficace si l’opérateur Take était utilisé pour limiter le nombre d’entrées soumises à l’expression lambda. Le code auquel je faisais référence se trouve dans le Listing 4.11. Listing 4.11 : Un autre exemple d’appel du prototype Take. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable chars = presidents.Take(5).SelectMany(s => s.ToArray()); foreach (char ch in chars) Console.WriteLine(ch);
La sortie console est identique à celle du Listing 4.9 : A d a m s
Linq.book Page 78 Mercredi, 18. février 2009 7:58 07
78
LINQ to Objects
Partie II
A r t h u r B u c h a n a n B u s h C a r t e r
Contrairement au Listing 4.9, seuls les cinq premiers éléments sont passés en entrée de l’opérateur SelectMany. Cette technique est bien plus efficace, en particulier si de nombreux éléments ne doivent pas être passés à SelectMany. L’opérateur TakeWhile L’opérateur TakeWhile renvoie les éléments de la séquence d’entrée, en commençant par le premier, tant qu’une condition est vérifiée. Les éléments restants sont ignorés.
Prototypes Deux prototypes de l’opérateur TakeWhile seront étudiés dans ce livre. Premier prototype public static IEnumerable TakeWhile( this IEnumerable source, Func predicate);
Dans ce prototype, l’opérateur TakeWhile admet deux paramètres en entrée : une séquence source et un prédicat. Il retourne un objet dont l’énumération fournit des éléments jusqu’à ce que le prédicat renvoie la valeur false. Les éléments suivants ne sont pas traités. Second prototype public static IEnumerable TakeWhile( this IEnumerable source, Func predicate);
Ce second prototype est semblable au premier si ce n’est que le prédicat reçoit également un entier qui correspond à l’index de l’élément dans la séquence source.
Linq.book Page 79 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
79
Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Le Listing 4.12 donne un exemple d’appel du premier prototype. Listing 4.12 : Un exemple d’appel du premier prototype de l’opérateur TakeWhile. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.TakeWhile(s => s.Length < 10); foreach (string item in items) Console.WriteLine(item);
Seuls les éléments contenant dix caractères au maximum sont retournés : Adams Arthur Buchanan Bush Carter Cleveland Clinton Coolidge
L’énumération s’est arrêtée sur le nom Eisenhower, long de 10 caractères. Voici maintenant un exemple d’appel du second prototype de l’opérateur TakeWhile. Listing 4.13 : Un exemple d’appel du second prototype TakeWhile. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents .TakeWhile((s, i) => s.Length < 10 && i < 5); foreach (string item in items) Console.WriteLine(item);
Linq.book Page 80 Mercredi, 18. février 2009 7:58 07
80
LINQ to Objects
Partie II
Cet exemple arrête l’énumération lorsqu’un élément en entrée a une longueur supérieure à 9 caractères ou lorsque la sixième entrée est atteinte. Voici le résultat : Adams Arthur Buchanan Bush Carter
Ici, l’énumération s’est arrêtée lorsque la sixième entrée a été atteinte. Opérateur Skip L’opérateur Skip saute un certain nombre d’éléments dans la séquence d’entrée et retourne les suivants.
Prototype Un seul prototype de l’opérateur Skip sera étudié dans ce livre : public static IEnumerable Skip( this IEnumerable source, int count);
L’opérateur Skip admet deux paramètres : une séquence source et l’entier count, qui indique le nombre d’éléments à sauter. Ce prototype renvoie un objet dont l’énumération exclut les count premiers éléments. Si la valeur de count est supérieure au nombre d’éléments de la séquence d’entrée, cette dernière ne sera pas énumérée et la séquence de sortie sera vide. Exceptions L’exception ArgumentNullException est levée si la séquence d’entrée a pour valeur null. Exemples Le Listing 4.14 est un exemple d’appel du prototype Skip. Listing 4.14 : Un exemple d’utilisation du prototype de l’opérateur Skip. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.Skip(1); foreach (string item in items) Console.WriteLine(item);
Linq.book Page 81 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
81
Dans cet exemple, seul le premier élément est ignoré. Tous les éléments suivants sont donc renvoyés par l’opérateur Skip : Arthur Buchanan Bush … Van Buren Washington Wilson
Opérateur SkipWhile L’opérateur SkipWhile ignore les éléments de la séquence d’entrée tant qu’une condition est vérifiée. Les éléments suivants sont alors renvoyés dans la séquence de sortie.
Prototypes Deux prototypes de l’opérateur SkipWhile seront étudiés dans ce livre. Premier prototype public static IEnumerable SkipWhile( this IEnumerable source, Func predicate);
Ce premier prototype admet deux paramètres : une séquence source et un prédicat. Il renvoie un objet dont l’énumération exclut les éléments de la séquence d’entrée tant que le prédicat retourne la valeur true. Dès qu’une valeur false est retournée, tous les éléments suivants sont envoyés dans la séquence de sortie. Second prototype public static IEnumerable SkipWhile( this IEnumerable source, Func predicate);
Ce second prototype est semblable au premier si ce n’est que le prédicat reçoit également un entier qui correspond à l’index de l’élément dans la séquence source. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Le Listing 4.15 donne un exemple d’appel du premier prototype de l’opérateur SkipWhile. Listing 4.15 : Un exemple d’appel du premier prototype de l’opérateur SkipWhile. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson",
Linq.book Page 82 Mercredi, 18. février 2009 7:58 07
82
LINQ to Objects
Partie II
"Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.SkipWhile(s => s.StartsWith("A")); foreach (string item in items) Console.WriteLine(item);
Dans cet exemple, tous les éléments qui commencent par la lettre A sont ignorés. Les éléments suivants sont passés à la séquence de sortie : Buchanan Bush Carter … Van Buren Washington Wilson
Le Listing 4.16 donne un exemple d’utilisation du second prototype de l’opérateur SkipWhile. Listing 4.16 : Un exemple d’utilisation du second prototype de l’opérateur SkipWhile. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents .SkipWhile((s, i) => s.Length > 4 && i < 10); foreach (string item in items) Console.WriteLine(item);
Dans cet exemple, tous les éléments dont la longueur est inférieure ou égale à 4 caractères ou supérieure ou égale à 10 caractères sont ignorés. Les éléments suivants constituent la séquence de sortie : Bush Carter Cleveland … Van Buren Washington Wilson
L’élément Bush compte 4 caractères. Il a donc mis fin au SkipWhile.
Linq.book Page 83 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
83
Concaténation Les opérateurs de concaténation accolent plusieurs séquences d’entrée dans la séquence de sortie. Opérateur Concat L’opérateur Concat accole deux séquences d’entrée dans la séquence de sortie.
Prototype Un seul prototype de l’opérateur Concat sera étudié dans ce livre : public static IEnumerable Concat( this IEnumerable first, IEnumerable second);
Deux séquences de même type T sont fournies en entrée de ce prototype : first et second. L’énumération de l’objet retourné renvoie tous les éléments de la première séquence d’entrée suivis de tous les éléments de la seconde séquence d’entrée. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Le Listing 4.17 donne un exemple d’utilisation des opérateurs Concat, Take et Skip. Listing 4.17 : Un exemple d’utilisation du prototype de l’opérateur Concat. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.Take(5).Concat(presidents.Skip(5)); foreach (string item in items) Console.WriteLine(item);
Ce code concatène les cinq premiers éléments de la séquence d’entrée presidents aux éléments de cette même séquence d’entrée, en excluant les cinq premiers. Le résultat contient donc tous les éléments de la séquence d’entrée : Adams Arthur Buchanan Bush Carter Cleveland Clinton
Linq.book Page 84 Mercredi, 18. février 2009 7:58 07
84
LINQ to Objects
Partie II
Coolidge Eisenhower Fillmore Ford Garfield Grant Harding Harrison Hayes Hoover Jackson Jefferson Johnson Kennedy Lincoln Madison McKinley Monroe Nixon Pierce Polk Reagan Roosevelt Taft Taylor Truman Tyler Van Buren Washington Wilson
Pour effectuer une concaténation, vous pouvez également utiliser l’opérateur SelectMany (voir Listing 4.18). Listing 4.18 : Un exemple effectuant une concaténation sans l’opérateur Concat. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = new[] { presidents.Take(5), presidents.Skip(5) } .SelectMany(s => s); foreach (string item in items) Console.WriteLine(item);
Le tableau item a été instancié par l’intermédiaire de deux séquences : une créée avec l’opérateur Take et une autre, avec l’opérateur Skip. Cet exemple est comparable au précédent mais, ici, on fait appel à l’opérateur SelectMany.
Linq.book Page 85 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
85
ASTUCE Si vous devez concaténer plusieurs séquences, vous utiliserez l’opérateur SelectMany. L’opérateur Concat, quant à lui, est limité à la concaténation de deux séquences.
Voici le résultat affiché dans la console : Adams Arthur Buchanan Bush Carter Cleveland Clinton Coolidge Eisenhower Fillmore Ford Garfield Grant Harding Harrison Hayes Hoover Jackson Jefferson Johnson Kennedy Lincoln Madison McKinley Monroe Nixon Pierce Polk Reagan Roosevelt Taft Taylor Truman Tyler Van Buren Washington Wilson
Tri Les opérateurs de tri permettent de classer des séquences. Les opérateurs OrderBy et OrderByDescending nécessitent tous deux une séquence d’entrée de type IEnumerable et retournent une séquence de type IOrderedEnumerable. Il est impossible de passer un IOrderedEnumerable en entrée des opérateurs OrderBy et OrderByDescending. Tout chaînage est donc impossible. Si vous avez besoin de trier conjointement plusieurs éléments, utilisez les opérateurs ThenBy ou ThenByDescending. Ces opérateurs peuvent être chaînés car ils admettent et retournent des IOrderedEnumerable.
Linq.book Page 86 Mercredi, 18. février 2009 7:58 07
86
LINQ to Objects
Partie II
À titre d’exemple, cet appel n’est pas valide : inputSequence.OrderBy(s => s.LastName).OrderBy(s => s.FirstName)…
Pour effectuer ce traitement, vous utiliserez la syntaxe suivante : inputSequence.OrderBy(s => s.LastName).ThenBy(s => s.FirstName)…
L’opérateur OrderBy L’opérateur OrderBy trie une séquence d’entrée en utilisant la méthode keySelector. Cette méthode retourne une valeur clé pour chaque élément en entrée et une séquence de sortie de type IOrderedEnumerable. Dans cette dernière, les éléments seront classés dans un ordre croissant, en se basant sur les valeurs clés retournées.
Le tri effectué par l’opérateur OrderBy est connu pour être "instable" : si deux éléments ayant la même valeur clé sont passés à OrderBy, leur ordre initial peut aussi bien être maintenu qu’inversé. Vous ne devez donc jamais vous fier à l’ordre des éléments issus de ces opérateurs OrderBy et OrderByDescending pour les champs qui ne sont pas spécifiés dans la méthode. ATTENTION Le tri effectué par les opérateurs OrderBy et OrderByDescending est "instable".
Prototypes Deux prototypes de l’opérateur OrderBy seront étudiés dans ce livre. Premier prototype public static IOrderedEnumerable OrderBy( this IEnumerable source, Func keySelector) where K : IComparable;
Ce prototype admet deux entrées : une séquence source et le délégué keySelector. L’énumération de l’objet retourné passe tous les éléments de la séquence d’entrée à la méthode KeySelector afin d’obtenir leurs clés et de procéder à leur tri. La méthode KeySelector se voit passer un élément de type T. Elle retourne la valeur clé de type K. Les types T et K peuvent être similaires ou différents. En revanche, le type de la valeur retournée par la méthode KeySelector doit implémenter l’interface IComparable. Second prototype public static IOrderedEnumerable OrderBy( this IEnumerable source, Func keySelector, IComparer comparer);
Ce prototype est le même que le précédent, si ce n’est qu’un objet comparer complémentaire lui est passé. Si vous utilisez cette version de l’opérateur OrderBy, le type K n’est pas forcé d’implémenter l’interface IComparable.
Linq.book Page 87 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
87
Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Le Listing 4.19 est un exemple d’utilisation du premier prototype. Listing 4.19 : Un exemple du premier prototype de l’opérateur OrderBy. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.OrderBy(s => s.Length); foreach (string item in items) Console.WriteLine(item);
Cet exemple classe les présidents par la longueur de leurs noms. Voici les résultats : Bush Ford Polk Taft Adams Grant Hayes Nixon Tyler Arthur Carter Hoover Monroe Pierce Reagan Taylor Truman Wilson Clinton Harding Jackson Johnson Kennedy Lincoln Madison Buchanan Coolidge Fillmore Garfield Harrison McKinley Cleveland Jefferson Roosevelt Van Buren Eisenhower Washington
Linq.book Page 88 Mercredi, 18. février 2009 7:58 07
88
LINQ to Objects
Partie II
Nous allons maintenant donner un exemple d’utilisation du deuxième prototype. Mais, auparavant, prenons quelques instants pour examiner l’interface IComparer : interface IComparer { int Compare(T x, T y); }
Cette interface utilise la méthode Compare. Cette dernière admet deux arguments de type T en entrée et retourne une valeur int. Sa valeur est : m
négative si le premier argument est inférieur au second ;
m
nulle si les deux arguments sont égaux ;
m
positive si le second argument est supérieur au premier.
Remarquez à quel point les génériques de C# 2.0 sont utiles dans cette interface et ce prototype. Pour faire fonctionner cet exemple, une classe spécifique qui implémente l’interface IComparer a été créée. Cette classe réarrangera les éléments par rapport à leur ratio nombre de voyelles/nombre de consonnes. Implémentation de l’interface IComparer pour illustrer le second prototype OrderBy public class MyVowelToConsonantRatioComparer : IComparer { public int Compare(string s1, string s2) { int vCount1 = 0; int cCount1 = 0; int vCount2 = 0; int cCount2 = 0; GetVowelConsonantCount(s1, ref vCount1, ref cCount1); GetVowelConsonantCount(s2, ref vCount2, ref cCount2); double dRatio1 = (double)vCount1/(double)cCount1; double dRatio2 = (double)vCount2/(double)cCount2; if(dRatio1 < dRatio2) return(-1); else if (dRatio1 > dRatio2) return(1); else return(0); } // Cette méthode est publique. Le code qui utilise ce comparateur // pourra donc y accéder si cela est nécessaire public void GetVowelConsonantCount(string s, ref int vowelCount, ref int consonantCount) { string vowels = "AEIOUY"; // Initialize the counts. vowelCount = 0; consonantCount = 0;
Linq.book Page 89 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
89
// Conversion en majuscules pour ne pas être sensible à la casse string sUpper = s.ToUpper(); foreach(char ch in sUpper) { if(vowels.IndexOf(ch) < 0) consonantCount++; else vowelCount++; } return; } }
Cette classe contient deux méthodes : Compare et GetVowelConsonantCount. La méthode Compare est nécessaire pour l’interface IComparer. La méthode GetConsonantVowelCount calcule le nombre de voyelles et de consonnes de la chaîne qui lui est passée. Par son intermédiaire, il est ainsi possible d’obtenir les valeurs à afficher lors de l’énumération de la séquence réordonnée. La logique utilisée à l’intérieur de la méthode n’a pas d’importance. Il est en effet peu probable que vous ayez un jour à classer des données en tenant compte de leur ratio nombre de voyelles/nombre de consonnes, et encore moins de comparer deux chaînes selon ce ratio. Ce qui est important, en revanche, c’est la technique qui a permis de créer une classe qui implémente l’interface IComparer en implémentant la méthode Compare. Pour cela, examinez le bloc if … else à la fin de la méthode Compare. Comme vous le voyez, les valeurs retournées sont -1, 1 ou 0, ce qui assure la compatibilité avec l’interface IComparer. Le Listing 4.20 donne un exemple d’appel du code. Listing 4.20 : Un exemple d’appel du second prototype OrderBy. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; MyVowelToConsonantRatioComparer myComp = new MyVowelToConsonantRatioComparer(); IEnumerable namesByVToCRatio = presidents .OrderBy((s => s), myComp); foreach (string item in namesByVToCRatio) { int vCount = 0; int cCount = 0; myComp.GetVowelConsonantCount(item, ref vCount, ref cCount); double dRatio = (double)vCount / (double)cCount; Console.WriteLine(item + " - " + dRatio + " - " + vCount + ":" + cCount); }
Linq.book Page 90 Mercredi, 18. février 2009 7:58 07
90
LINQ to Objects
Partie II
L’objet mycomp a été instancié avant d’appeler l’opérateur OrderBy. Une référence est donc créée, et il est possible de l’utiliser dans la boucle foreach. Voici les résultats de ce code : Grant - 0.25 - 1:4 Bush - 0.333333333333333 - 1:3 Ford - 0.333333333333333 - 1:3 Polk - 0.333333333333333 - 1:3 Taft - 0.333333333333333 - 1:3 Clinton - 0.4 - 2:5 Harding - 0.4 - 2:5 Jackson - 0.4 - 2:5 Johnson - 0.4 - 2:5 Lincoln - 0.4 - 2:5 Washington - 0.428571428571429 - 3:7 Arthur - 0.5 - 2:4 Carter - 0.5 - 2:4 Cleveland - 0.5 - 3:6 Jefferson - 0.5 - 3:6 Truman - 0.5 - 2:4 Van Buren - 0.5 - 3:6 Wilson - 0.5 - 2:4 Buchanan - 0.6 - 3:5 Fillmore - 0.6 - 3:5 Garfield - 0.6 - 3:5 Harrison - 0.6 - 3:5 McKinley - 0.6 - 3:5 Adams - 0.666666666666667 - 2:3 Nixon - 0.666666666666667 - 2:3 Tyler - 0.666666666666667 - 2:3 Kennedy - 0.75 - 3:4 Madison - 0.75 - 3:4 Roosevelt - 0.8 - 4:5 Coolidge - 1 - 4:4 Eisenhower - 1 - 5:5 Hoover - 1 - 3:3 Monroe - 1 - 3:3 Pierce - 1 - 3:3 Reagan - 1 - 3:3 Taylor - 1 - 3:3 Hayes - 1.5 - 3:2
Les présidents sont classés par ratio voyelle/consonne croissant. L’opérateur OrderByDescending Cet opérateur a les mêmes prototypes et comportement que OrderBy, excepté que les éléments sont classés dans un ordre décroissant.
Prototypes Deux prototypes de l’opérateur OrderByDescending seront étudiés dans ce livre. Premier prototype public static IOrderedEnumerable OrderByDescending( this IEnumerable source, Func keySelector) where K : IComparable;
Linq.book Page 91 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
91
ATTENTION Le tri effectué par les opérateurs OrderBy et OrderByDescending est "instable".
Second prototype public static IOrderedEnumerable OrderByDescending( this IEnumerable source, Func keySelector, IComparer comparer);
Ce prototype est le même que le précédent, si ce n’est qu’un objet comparer complémentaire lui est passé. Si vous utilisez cette version de l’opérateur OrderByDescending, le type K n’est pas forcé d’implémenter l’interface IComparable. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Dans l’exemple du Listing 4.21, nous allons classer les présidents des États-Unis en utilisant un ordre inverse alphabétique sur leurs noms. Listing 4.21 : Un exemple d’utilisation du premier prototype d’OrderDescending. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.OrderByDescending(s => s); foreach (string item in items) Console.WriteLine(item);
Les présidents sont bien classés en utilisant un ordre inverse alphabétique sur leurs noms. Wilson Washington Van Buren Tyler Truman Taylor Taft Roosevelt Reagan Polk Pierce Nixon Monroe McKinley Madison Lincoln
Linq.book Page 92 Mercredi, 18. février 2009 7:58 07
92
LINQ to Objects
Partie II
Kennedy Johnson Jefferson Jackson Hoover Hayes Harrison Harding Grant Garfield Ford Fillmore Eisenhower Coolidge Clinton Cleveland Carter Bush Buchanan Arthur Adams
Nous allons maintenant donner un exemple d’appel du second prototype d’ OrderByDescending. Nous utiliserons le même code (y compris au niveau du comparateur MyVowelToConsonantRatioComparer) que dans la section relative à l’opérateur OrderBy. Mais, ici, c’est l’opérateur OrderByDescending qui sera appelé (voir Listing 4.22). Listing 4.22 : Un exemple d’appel du second prototype d’OrderByDescending. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; MyVowelToConsonantRatioComparer myComp = new MyVowelToConsonantRatioComparer(); IEnumerable namesByVToCRatio = presidents .OrderByDescending((s => s), myComp); foreach (string item in namesByVToCRatio) { int vCount = 0; int cCount = 0; myComp.GetVowelConsonantCount(item, ref vCount, ref cCount); double dRatio = (double)vCount / (double)cCount; Console.WriteLine(item + " - " + dRatio + " - " + vCount + ":" + cCount); }
Voici les résultats de cet exemple : Hayes - 1.5 - 3:2 Coolidge - 1 - 4:4 Eisenhower - 1 - 5:5
Linq.book Page 93 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
93
Hoover - 1 - 3:3 Monroe - 1 - 3:3 Pierce - 1 - 3:3 Reagan - 1 - 3:3 Taylor - 1 - 3:3 Roosevelt - 0.8 - 4:5 Kennedy - 0.75 - 3:4 Madison - 0.75 - 3:4 Adams - 0.666666666666667 - 2:3 Nixon - 0.666666666666667 - 2:3 Tyler - 0.666666666666667 - 2:3 Buchanan - 0.6 - 3:5 Fillmore - 0.6 - 3:5 Garfield - 0.6 - 3:5 Harrison - 0.6 - 3:5 McKinley - 0.6 - 3:5 Arthur - 0.5 - 2:4 Carter - 0.5 - 2:4 Cleveland - 0.5 - 3:6 Jefferson - 0.5 - 3:6 Truman - 0.5 - 2:4 Van Buren - 0.5 - 3:6 Wilson - 0.5 - 2:4 Washington - 0.428571428571429 - 3:7 Clinton - 0.4 - 2:5 Harding - 0.4 - 2:5 Jackson - 0.4 - 2:5 Johnson - 0.4 - 2:5 Lincoln - 0.4 - 2:5 Bush - 0.333333333333333 - 1:3 Ford - 0.333333333333333 - 1:3 Polk - 0.333333333333333 - 1:3 Taft - 0.333333333333333 - 1:3 Grant - 0.25 - 1:4
Ces résultats sont les mêmes que dans l’exemple de la section précédente mais, ici, le classement a été effectué du plus grand au plus petit ratio voyelles/consonnes. Opérateur ThenBy L’opérateur ThenBy trie une séquence de type IOrderedEnumerable en se basant sur une méthode keySelector qui lui retourne une valeur clé. Il renvoie une séquence de sortie de type IOrderedEnumerable. INFO Les opérateurs ThenBy et ThenByDescending demandent tous deux un paramètre dont le type est inhabituel : IOrderedEnumerable. L’opérateur OrderBy ou OrderByDescending doit être appelé en premier lieu pour créer un objet IOrderedEnumerable.
INFO Contrairement aux opérateurs OrderBy et OrderByDescending, ThenBy et ThenByDescending sont stables. Ils préservent donc l’ordre original des éléments qui possèdent la même clé.
Linq.book Page 94 Mercredi, 18. février 2009 7:58 07
94
LINQ to Objects
Partie II
Prototypes Deux prototypes de l’opérateur ThenBy seront étudiés dans ce livre. Premier prototype public static IOrderedEnumerable ThenBy( this IOrderedEnumerable source, Func keySelector) where K : IComparable;
Dans ce prototype, l’opérateur ThenBy reçoit une séquence d’entrée de type IOrderedEnumerable et un délégué keySelector. Ce dernier se voit passer l’élément d’entrée de type T et retourne le champ de type K de cet élément qui sera utilisé comme valeur clé. Les types T et K peuvent être identiques ou différents. La valeur retournée par la méthode KeySelector doit implémenter l’interface ICompare. L’opérateur ThenBy classe la séquence d’entrée par ordre croissant selon la clé retournée par keySelector. Second prototype public static IOrderedEnumerable ThenBy( this IOrderedEnumerable source, Func keySelector, IComparer comparer);
Ce prototype est identique au précédent, si ce n’est qu’un objet comparer complémentaire lui est passé. Si vous utilisez cette version de l’opérateur ThenBy, le type K n’est pas forcé d’implémenter l’interface IComparable. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null (voir Listing 4.23). Exemples Listing 4.23 : Un exemple d’appel du premier prototype. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.OrderBy(s => s.Length).ThenBy(s => s); foreach (string item in items) Console.WriteLine(item);
Dans un premier temps, ce code classe les éléments (ici, les noms des présidents des États-Unis) selon leur longueur. Dans un second temps, les éléments sont classés dans
Linq.book Page 95 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
95
un ordre alphabétique. Si plusieurs noms ont la même longueur, ils apparaîtront donc dans l’ordre alphabétique. Bush Ford Polk Taft Adams Grant Hayes Nixon Tyler Arthur Carter Hoover Monroe Pierce Reagan Taylor Truman Wilson Clinton Harding Jackson Johnson Kennedy Lincoln Madison Buchanan Coolidge Fillmore Garfield Harrison McKinley Cleveland Jefferson Roosevelt Van Buren Eisenhower Washington
Pour illustrer le second prototype de l’opérateur ThenBy, nous allons utiliser le comparateur MyVowelConsonantRatioComparer, introduit quelques pages précédemment. Pour être en mesure d’appeler l’opérateur ThenBy, il faut au préalable appeler l’opérateur OrderBy ou OrderByDescending. Le but de cet exemple est de classer les noms par longueurs croissantes puis, à l’intérieur de chaque groupe de longueurs, par ratio voyelles/consonnes (voir Listing 4.24). Listing 4.24 : Un exemple d’appel du second prototype de ThenBy. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft",
Linq.book Page 96 Mercredi, 18. février 2009 7:58 07
96
LINQ to Objects
Partie II
"Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; MyVowelToConsonantRatioComparer myComp = new MyVowelToConsonantRatioComparer(); IEnumerable namesByVToCRatio = presidents .OrderBy(n => n.Length) .ThenBy((s => s), myComp); foreach (string item in namesByVToCRatio) { int vCount = 0; int cCount = 0; myComp.GetVowelConsonantCount(item, ref vCount, ref cCount); double dRatio = (double)vCount / (double)cCount; Console.WriteLine(item + " - " + dRatio + " - " + vCount + ":" + cCount); }
Voici le résultat de ce code : Bush - 0.333333333333333 - 1:3 Ford - 0.333333333333333 - 1:3 Polk - 0.333333333333333 - 1:3 Taft - 0.333333333333333 - 1:3 Grant - 0.25 - 1:4 Adams - 0.666666666666667 - 2:3 Nixon - 0.666666666666667 - 2:3 Tyler - 0.666666666666667 - 2:3 Hayes - 1.5 - 3:2 Arthur - 0.5 - 2:4 Carter - 0.5 - 2:4 Truman - 0.5 - 2:4 Wilson - 0.5 - 2:4 Hoover - 1 - 3:3 Monroe - 1 - 3:3 Pierce - 1 - 3:3 Reagan - 1 - 3:3 Taylor - 1 - 3:3 Clinton - 0.4 - 2:5 Harding - 0.4 - 2:5 Jackson - 0.4 - 2:5 Johnson - 0.4 - 2:5 Lincoln - 0.4 - 2:5 Kennedy - 0.75 - 3:4 Madison - 0.75 - 3:4 Buchanan - 0.6 - 3:5 Fillmore - 0.6 - 3:5 Garfield - 0.6 - 3:5 Harrison - 0.6 - 3:5 McKinley - 0.6 - 3:5 Coolidge - 1 - 4:4 Cleveland - 0.5 - 3:6 Jefferson - 0.5 - 3:6 Van Buren - 0.5 - 3:6 Roosevelt - 0.8 - 4:5 Washington - 0.428571428571429 - 3:7 Eisenhower - 1 - 5:5
Comme prévu, les noms sont classés par longueurs, puis par ratio voyelles/consonnes.
Linq.book Page 97 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
97
Opérateur ThenByDescending Cet opérateur utilise les mêmes prototypes et se comporte comme l’opérateur ThenBy, mais il classe les données dans un ordre décroissant.
Prototypes Deux prototypes de l’opérateur ThenByDescending seront étudiés dans ce livre. Premier prototype public static IOrderedEnumerable ThenByDescending( this IOrderedEnumerable source, Func keySelector) where K : IComparable;
Ce prototype se comporte comme le premier prototype de l’opérateur ThenBy, mais il classe les données dans un ordre décroissant. Second prototype public static IOrderedEnumerable ThenByDescending( this IOrderedEnumerable source, Func keySelector, IComparer comparer);
Ce prototype est identique au précédent, si ce n’est qu’un objet comparer complémentaire lui est passé. Si vous utilisez cette version de l’opérateur ThenByDescending, le type K n’est pas forcé d’implémenter l’interface IComparable. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Nous allons utiliser le même exemple que dans la section précédente, mais ici l’opérateur ThenByDescending sera utilisé à la place de ThenBy (voir Listing 4.25). Listing 4.25 : Un exemple d’appel du premier prototype de l’opérateur ThenByDescending. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.OrderBy(s => s.Length).ThenByDescending(s => s); foreach (string item in items) Console.WriteLine(item);
Linq.book Page 98 Mercredi, 18. février 2009 7:58 07
98
LINQ to Objects
Partie II
Ce code classe les noms des présidents par longueur croissante puis, à l’intérieur de chaque groupe, par ordre inverse alphabétique. Taft Polk Ford Bush Tyler Nixon Hayes Grant Adams Wilson Truman Taylor Reagan Pierce Monroe Hoover Carter Arthur Madison Lincoln Kennedy Johnson Jackson Harding Clinton McKinley Harrison Garfield Fillmore Coolidge Buchanan Van Buren Roosevelt Jefferson Cleveland Washington Eisenhower
Pour illustrer le second prototype de l’opérateur ThenByDescending, nous utiliserons le même code que dans l’exemple du second prototype de l’opérateur ThenBy, à ceci près que l’opérateur ThenByDescending remplacera l’opérateur ThenBy (voir Listing 4.26). Listing 4.26 : Un exemple d’appel du second prototype de l’opérateur ThenByDescending. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};
Linq.book Page 99 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
99
MyVowelToConsonantRatioComparer myComp = new MyVowelToConsonantRatioComparer(); IEnumerable namesByVToCRatio = presidents .OrderBy(n => n.Length) .ThenByDescending((s => s), myComp); foreach (string item in namesByVToCRatio) { int vCount = 0; int cCount = 0; myComp.GetVowelConsonantCount(item, ref vCount, ref cCount); double dRatio = (double)vCount / (double)cCount; Console.WriteLine(item + " - " + dRatio + " - " + vCount + ":" + cCount); }
Voici les informations affichées dans la console suite à l’exécution de ce code : Bush - 0.333333333333333 - 1:3 Ford - 0.333333333333333 - 1:3 Polk - 0.333333333333333 - 1:3 Taft - 0.333333333333333 - 1:3 Hayes - 1.5 - 3:2 Adams - 0.666666666666667 - 2:3 Nixon - 0.666666666666667 - 2:3 Tyler - 0.666666666666667 - 2:3 Grant - 0.25 - 1:4 Hoover - 1 - 3:3 Monroe - 1 - 3:3 Pierce - 1 - 3:3 Reagan - 1 - 3:3 Taylor - 1 - 3:3 Arthur - 0.5 - 2:4 Carter - 0.5 - 2:4 Truman - 0.5 - 2:4 Wilson - 0.5 - 2:4 Kennedy - 0.75 - 3:4 Madison - 0.75 - 3:4 Clinton - 0.4 - 2:5 Harding - 0.4 - 2:5 Jackson - 0.4 - 2:5 Johnson - 0.4 - 2:5 Lincoln - 0.4 - 2:5 Coolidge - 1 - 4:4 Buchanan - 0.6 - 3:5 Fillmore - 0.6 - 3:5 Garfield - 0.6 - 3:5 Harrison - 0.6 - 3:5 McKinley - 0.6 - 3:5 Roosevelt - 0.8 - 4:5 Cleveland - 0.5 - 3:6 Jefferson - 0.5 - 3:6 Van Buren - 0.5 - 3:6 Eisenhower - 1 - 5:5 Washington - 0.428571428571429 - 3:7
Comme vous pouvez le voir, les noms sont classés par longueur croissante, puis par ratio voyelles/consonnes décroissant.
Linq.book Page 100 Mercredi, 18. février 2009 7:58 07
100
LINQ to Objects
Partie II
Opérateur Reverse Cet opérateur renvoie une séquence du même type que celle passée en entrée, mais en inversant ses éléments.
Prototype Un seul prototype de l’opérateur Reverse sera étudié dans ce livre : public static IEnumerable Reverse( this IEnumerable source);
Ce prototype retourne une séquence IEnumerable dont l’énumération produit l’ordre inverse des éléments de la séquence d’entrée. Exceptions L’exception ArgumentNullException est levée si l’argument a pour valeur null (voir Listing 4.27). Exemples Listing 4.27 : Un exemple d’appel de l’opérateur Reverse. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.Reverse(); foreach (string item in items) Console.WriteLine(item);
Ce code affiche les informations suivantes dans la fenêtre Console. Comme on pouvait s’y attendre, les noms des présidents apparaissent dans l’ordre inverse de ceux passés en entrée : Wilson Washington Van Buren … Bush Buchanan Arthur Adams
Opérateurs de jointure Les opérateurs de jointure effectuent un assemblage de plusieurs séquences.
Linq.book Page 101 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
101
Opérateur Join L’opérateur Join effectue une jointure entre deux séquences, en se basant sur les clés extraites des différents éléments des deux séquences.
Prototype Un seul prototype de l’opérateur Join sera abordé dans cet ouvrage : public static IEnumerable Join( this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector);
Le premier élément de la méthode a pour nom outer (extérieur). Comme il s’agit d’une méthode d’extension, on parlera de "séquence extérieure" pour faire référence à la séquence sur laquelle l’opérateur Join est appelé. L’opérateur Join retourne un objet. Son énumération produit, dans un premier temps, une séquence inner d’éléments de type U. Pour ce faire, la méthode innerKeySelector est appelée sur chaque élément de la séquence inner et un tableau de référencement est créé pour mémoriser les couples élément/valeur clé. Dans un second temps, l’objet retourné énumère la séquence outer d’éléments de type T. Pour ce faire, la méthode outerKeySelector est appelée sur chaque élément de la séquence outer afin d’obtenir sa clé et de retrouver la séquence inner correspondante dans le tableau de référencement. Pour chaque élément de la paire séquence outer/séquence inner, l’objet retourné appelle enfin la méthode resultSelector, en lui passant les éléments outer et inner. Un objet instancié de type V est alors retourné par la méthode resultSelector, puis placé dans la séquence de sortie de type V. L’ordre des éléments de la séquence outer est préservé, ainsi que celui des éléments inner de chaque séquence outer. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Cet exemple utilise les deux classes communes définies au début de ce chapitre : Employee et EmployeeOptionEntry. Le code du Listing 4.28 a été mis en forme un peu différemment afin d’améliorer la lisibilité des arguments de l’opérateur Join. Listing 4.28 : Un exemple d’appel de l’opérateur Join. Employee[] employees = Employee.GetEmployeesArray(); EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries(); var employeeOptions = employees .Join( empOptions, // séquence inner
Linq.book Page 102 Mercredi, 18. février 2009 7:58 07
102
LINQ to Objects
Partie II
e => e.id, // outerKeySelector o => o.id, // innerKeySelector (e, o) => new // resultSelector { id = e.id, name = string.Format("{0} {1}", e.firstName, e.lastName), options = o.optionsCount }); foreach (var item in employeeOptions) Console.WriteLine(item);
Ce code effectue une jointure sur deux tableaux de données en utilisant deux classes communes. L’opérateur Join étant appliqué au tableau employees, ce dernier joue le rôle de la séquence externe. Quant à empOptions, il correspond à la séquence interne. Voici les résultats de la jointure : { { { { { { { { {
id id id id id id id id id
= = = = = = = = =
1, name = 2, name = 2, name = 2, name = 3, name = 3, name = 3, name = 4, name = 101, name
Joe Rattz, options = 2 } William Gates, options = 10000 } William Gates, options = 10000 } William Gates, options = 10000 } Anders Hejlsberg, options = 5000 } Anders Hejlsberg, options = 7500 } Anders Hejlsberg, options = 7500 } David Lightman, options = 1500 } = Kevin Flynn, options = 2 }
La méthode resultSelector crée une classe anonyme du même type que la séquence de sortie. Il est facile de voir qu’il s’agit d’une classe anonyme, car aucun nom de classe n’est spécifié dans l’instruction new. Par ailleurs, le mot-clé var est utilisé, ce qui confirme nos soupçons. Il n’est pas possible de le déclarer en tant qu’ IEnumerable, puisque aucun type nommé ne donne les précisions nécessaires pour le déclarer comme tel. ASTUCE Lorsque le dernier opérateur appelé retourne une séquence de type anonyme, vous devez utiliser le mot-clé var pour mémoriser la séquence dans un objet.
L’opérateur GroupJoin L’opérateur GroupJoin effectue une jointure sur deux séquences en se basant sur les clés extraites de chacun des éléments des deux séquences.
Cet opérateur travaille d’une manière comparable à l’opérateur Join, à ceci près que l’opérateur Join ne passe qu’un seul élément de la séquence externe et un élément de la séquence interne à la méthode resultSelector. Cela signifie que, si plusieurs éléments de la séquence interne correspondent à un élément de la séquence interne, plusieurs appels à resultSelect seront nécessaires. Avec l’opérateur GroupJoin, tous les éléments de la séquence interne qui correspondent à un élément de la séquence externe
Linq.book Page 103 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
103
sont passés conjointement sous la forme d’une séquence à resultSelector. Un seul appel à cette méthode est donc nécessaire. Prototype Un seul prototype de l’opérateur GroupJoin sera étudié dans cet ouvrage : public static IEnumerable GroupJoin( this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector);
Le premier élément de la méthode a pour nom outer (extérieur). Comme il s’agit d’une méthode d’extension, on parlera de "séquence extérieure" pour faire référence à la séquence sur laquelle l’opérateur Join est appelé. L’opérateur GroupJoin retourne un objet. Son énumération produit, dans un premier temps, une séquence inner d’éléments de type U. Pour ce faire, la méthode innerKeySelector est appelée sur chaque élément de la séquence inner et un tableau de référencement est créé pour mémoriser les couples élément/valeur clé. Dans un second temps, l’objet retourné énumère la séquence outer d’éléments de type T. Pour ce faire, la méthode outerKeySelector est appelée sur chaque élément de la séquence outer afin d’obtenir sa clé et de retrouver la séquence inner correspondante dans le tableau de référencement. Pour chaque élément de la paire séquence outer/séquence inner, l’objet retourné appelle enfin la méthode resultSelector, en lui passant l’élément outer et la séquence des éléments inner correspondants. Un objet instancié de type V est alors retourné par la méthode resultSelector, puis placé dans la séquence de sortie de type V. L’ordre des éléments de la séquence outer est préservé, ainsi que celui des éléments inner de chaque séquence outer. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Nous utiliserons les classes Employee et EmployeeOptionEntry déjà évoquées dans la section précédente. Le code du Listing 4.29 réalise une jointure entre les employés et les options et calcule la somme des options de chacun des employés en utilisant l’opérateur GroupJoin. Listing 4.29 : Un exemple d’utilisation de l’opérateur GroupJoin. Employee[] employees = Employee.GetEmployeesArray(); EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries(); var employeeOptions = employees .GroupJoin(
Linq.book Page 104 Mercredi, 18. février 2009 7:58 07
104
LINQ to Objects
Partie II
empOptions, e => e.id, o => o.id, (e, os) => new { id = e.id, name = string.Format("{0} {1}", e.firstName, e.lastName), options = os.Sum(o => o.optionsCount) }); foreach (var item in employeeOptions) Console.WriteLine(item);
Ce code est très proche du précédent. Cependant, si vous examinez le deuxième argument passé à l’expression lambda (issu de la méthode resultSelector), vous verrez que l’argument o de l’exemple sur l’opérateur Join est remplacé par os. Cette différence s’explique par le fait que l’opérateur Join travaille sur un seul objet option, alors que l’opérateur GroupJoin travaille sur une séquence d’objets option. L’opérateur Sum initialise donc le dernier membre de l’objet anonyme instancié avec la somme des objets option. Pour l’instant, il vous suffit de savoir que cet opérateur est en mesure de calculer la somme des éléments (ou d’un membre des éléments) qui lui sont passés. Pour en savoir plus sur l’opérateur non différé Join, reportez-vous au Chapitre 5. Voici le résultat du code précédent : { { { { {
id id id id id
= = = = =
1, name = 2, name = 3, name = 4, name = 101, name
Joe Rattz, options = 2 } William Gates, options = 30000 } Anders Hejlsberg, options = 20000 } David Lightman, options = 1500 } = Kevin Flynn, options = 2 }
Dans ces résultats, les valeurs options correspondent à la somme de tous les champs option de chaque employé. Ces résultats sont différents de ceux issus de l’opérateur Join, où une ligne était créée pour chacune des options de chaque employé. Opérateurs de regroupement Ces opérateurs permettent de regrouper les éléments d’une séquence qui possèdent une même clé. Opérateur GroupBy Cet opérateur est utilisé pour regrouper les éléments d’une séquence d’entrée.
Prototypes Tous les prototypes de l’opérateur GroupBy retournent une séquence d’éléments IGrouping. L’interface IGrouping est définie comme suit : public interface IGrouping : IEnumerable { K Key { get; } }
Linq.book Page 105 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
105
Un IGrouping est donc une séquence de type T avec une clé de type K. Quatre prototypes de GroupBy seront étudiés dans cet ouvrage. Premier prototype public static IEnumerable GroupBy( this IEnumerable source, Func keySelector);
Ce prototype retourne un objet dont l’énumération passe en revue les éléments de la séquence d’entrée, appelle la méthode keySelector, mémorise chaque élément avec sa clé et produit une séquence d’instances IGrouping dans laquelle chaque élément IGrouping est une séquence d’éléments qui partagent la même clé. Les clés sont comparées par l’intermédiaire du comparateur d’égalité par défaut, EqualityComparerDefault. Pour dire les choses autrement, la valeur retournée par la méthode GroupBy est une séquence d’objets IGrouping. Chacun d’entre eux contient une clé et une séquence d’éléments issus de la séquence d’entrée et partageant la même clé. L’ordre des instances IGrouping est le même que celui des clés dans la séquence d’entrée. Quant à l’ordre des éléments d’une séquence IGrouping, il est identique à celui des éléments dans la séquence d’entrée. Deuxième prototype public static IEnumerable GroupBy( this IEnumerable source, Func keySelector, IEqualityComparer comparer);
Ce prototype est identique au premier, à ceci près qu’il est possible de choisir le comparateur à utiliser. Troisième prototype public static IEnumerable GroupBy( this IEnumerable source, Func keySelector, Func elementSelector);
Ce prototype est identique au premier mais, ici, la méthode elementSelector est utilisée pour choisir les éléments de la séquence d’entrée qui doivent apparaître dans la séquence de sortie. Quatrième prototype public static IEnumerable GroupBy( this IEnumerable source, Func keySelector, Func elementSelector, IEqualityComparer comparer);
Ce prototype regroupe les possibilités offertes par les deuxième et troisième prototypes : il est donc possible de choisir un comparateur avec l’argument comparer et de limiter les éléments de la séquence de sortie avec l’argument elementSelector.
Linq.book Page 106 Mercredi, 18. février 2009 7:58 07
106
LINQ to Objects
Partie II
Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Le premier exemple (voir Listing 4.30) utilise la classe commune EmployeeOptionEntries. Les employés seront regroupés par id et affichés. Listing 4.30 : Un exemple d’utilisation du premier prototype. EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries(); IEnumerable outerSequence = empOptions.GroupBy(o => o.id); // Première énumération de la séquence extérieure de IGroupings foreach (IGrouping keyGroupSequence in outerSequence) { Console.WriteLine("Enregistrements Option pour l’employé " + keyGroupSequence.Key); // Énumération des séquences IGrouping d’éléments EmployeeOptionEntry foreach (EmployeeOptionEntry element in keyGroupSequence) Console.WriteLine("id={0} : optionsCount={1} : dateAwarded={2:d}", element.id, element.optionsCount, element.dateAwarded); }
Ce code énumère la séquence outerSequence. Les éléments obtenus sont des objets qui implémentent l’interface IGrouping. Ils contiennent une clé et une séquence d’éléments EmployeeOptionEntry qui partagent cette même clé. Voici les résultats : Enregistrements Option pour l’employé 1 id=1 : optionsCount=2 : dateAwarded=12/31/1999 Enregistrements Option pour l’employé 2 id=2 : optionsCount=10000 : dateAwarded=6/30/1992 id=2 : optionsCount=10000 : dateAwarded=1/1/1994 id=2 : optionsCount=10000 : dateAwarded=4/1/2003 Enregistrements Option pour l’employé 3 id=3 : optionsCount=5000 : dateAwarded=9/30/1997 id=3 : optionsCount=7500 : dateAwarded=9/30/1998 id=3 : optionsCount=7500 : dateAwarded=9/30/1998 Enregistrements Option pour l’employé 4 id=4 : optionsCount=1500 : dateAwarded=12/31/1997 Enregistrements Option pour l’employé 101 id=101 : optionsCount=2 : dateAwarded=12/31/1998
Pour illustrer le deuxième prototype de l’opérateur GroupBy, nous allons supposer que tous les employés dont le champ id est inférieur à 100 sont des membres fondateurs de l’entreprise. Nous allons lister tous les enregistrements option regroupés selon l’état fondateur/non fondateur des employés.
Linq.book Page 107 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
107
Pour ce faire, nous devons définir un comparateur spécifique qui de plus doit implémenter l’interface IEqualityComparer. Avant de parler du comparateur, jetons un œil à cette interface : interface IEqualityComparer { bool Equals(T x, T y); int GetHashCode(T x); }
Cette interface nécessite l’implémentation de deux méthodes : Equals et GetHashCode. La méthode Equals reçoit deux objets de type T. Elle retourne la valeur true si les deux objets sont considérés comme égaux et la valeur false dans le cas contraire. La méthode GetHashCode reçoit un objet de type T et retourne un code (appelé hash code ou clé) de type entier pour cet objet. Le hash code est une valeur numérique qui identifie (généralement) de manière unique un objet. Ordinairement calculé à partir du contenu de l’objet, il est utilisé comme un index qui permettra de retrouver facilement une structure de données. Voici la classe qui implémente l’interface IEqualityComparer : public class MyFounderNumberComparer : IEqualityComparer { public bool Equals(int x, int y) { return(isFounder(x) == isFounder(y)); } public int GetHashCode(int i) { int f = 1; int nf = 100; return (isFounder(i) ? f.GetHashCode() : nf.GetHashCode()); } public bool isFounder(int id) { return(id < 100); } }
La méthode IsFounder a été ajoutée aux méthodes Equals et GetHashCode. Elle détermine si un employé est un fondateur en se basant sur son champ id. Ceci facilite la compréhension du code. La méthode IsFounder est publique. Il est donc possible de l’appeler en dehors de l’interface. Nous verrons cela un peu plus loin dans le code de l’exemple. Le comparateur d’égalité considère que tout entier inférieur à 100 représente un membre fondateur. Si deux entiers font partie d’une de ces deux catégories, ils sont considérés comme égaux. La fonction GetHashCode retourne un entier égal à 1 si l’employé est un membre fondateur ou égal à 100 dans le cas contraire. Listing 4.31 : Un exemple d’utilisation du deuxième prototype. MyFounderNumberComparer comp = new MyFounderNumberComparer(); EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries();
Linq.book Page 108 Mercredi, 18. février 2009 7:58 07
108
LINQ to Objects
Partie II
IEnumerable opts = empOptions .GroupBy(o => o.id, comp); // Énumération de la séquence d’IGrouping foreach (IGrouping keyGroup in opts) { Console.WriteLine("Options pour les " + (comp.isFounder(keyGroup.Key) ? "fondateurs" : "non fondateurs ")); // Énumération de la séquence d’éléments EmployeeOptionEntry foreach (EmployeeOptionEntry element in keyGroup) Console.WriteLine("id={0} : optionsCount={1} : dateAwarded={2:d}", element.id, element.optionsCount, element.dateAwarded); }
Dans cet exemple, le comparateur est instancié en dehors de la méthode GroupBy. La méthode IsFounder peut ainsi être appelée dans la boucle d’affichage foreach. Voici les résultats affichés par ce code : Options pour les fondateurs id=1 : optionsCount=2 : dateAwarded=12/31/1999 id=2 : optionsCount=10000 : dateAwarded=6/30/1992 id=2 : optionsCount=10000 : dateAwarded=1/1/1994 id=3 : optionsCount=5000 : dateAwarded=9/30/1997 id=2 : optionsCount=10000 : dateAwarded=4/1/2003 id=3 : optionsCount=7500 : dateAwarded=9/30/1998 id=3 : optionsCount=7500 : dateAwarded=9/30/1998 id=4 : optionsCount=1500 : dateAwarded=12/31/1997 Options pour les non fondateurs id=101 : optionsCount=2 : dateAwarded=12/31/1998
Comme vous le voyez, les employés dont le champ id est inférieur à 100 sont regroupés sous le libellé "Options pour les fondateurs" et les autres sous le libellé "Options pour les non fondateurs". Pour illustrer le troisième prototype, nous allons extraire les dates de délivrance des options. Le code sera très proche de celui utilisé pour illustrer le premier prototype. Contrairement au Listing 4.30, qui retournait un regroupement d’objets EmployeeOptionEntry, le Listing 4.32 retourne un regroupement de dates. Listing 4.32 : Un exemple d’utilisation du troisième prototype. EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries(); IEnumerable opts = empOptions .GroupBy(o => o.id, e => e.dateAwarded); // Énumération de la séquence de IGrouping foreach (IGrouping keyGroup in opts) { Console.WriteLine("Enregistrements Option pour l’employé " + keyGroup.Key); // Énumération des éléments DateTime foreach (DateTime date in keyGroup) Console.WriteLine(date.ToShortDateString()); }
Linq.book Page 109 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
109
Dans l’appel à l’opérateur GroupBy, remarquez que le deuxième argument ne retourne que la date de l’option (dateAwarded). Le IGrouping est donc de type DateTime (et non EmployeeOptionEntry). Voici le résultat de l’exécution : Enregistrements 12/31/1999 Enregistrements 6/30/1992 1/1/1994 4/1/2003 Enregistrements 9/30/1997 9/30/1998 9/30/1998 Enregistrements 12/31/1997 Enregistrements 12/31/1998
Option pour l’employé 1 Option pour l’employé 2
Option pour l’employé 3
Option pour l’employé 4 Option pour l’employé 101
Pour illustrer le quatrième prototype, nous allons utiliser la méthode elementSelector et un objet comparer. Cela revient à utiliser une combinaison des exemples du deuxième et du troisième prototypes. Dans le Listing 4.33, nous regroupons les dates des options dans deux groupes : les fondateurs (id < 100) et les non-fondateurs (id > 100). Listing 4.33 : Un exemple d’utilisation du quatrième prototype. MyFounderNumberComparer comp = new MyFounderNumberComparer(); EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries(); IEnumerable opts = empOptions .GroupBy(o => o.id, o => o.dateAwarded, comp); // Énumération de la séquence de IGrouping foreach (IGrouping keyGroup in opts) { Console.WriteLine("Enregistrements Option pour les " + (comp.isFounder(keyGroup.Key) ? "fondateurs" : "non fondateurs")); // Énumération de la séquence des éléments EmployeeOptionEntry foreach (DateTime date in keyGroup) Console.WriteLine(date.ToShortDateString()); }
La sortie console n’affiche que des dates regroupées par fondateurs et non-fondateurs : Enregistrements Option pour les fondateurs 12/31/1999 6/30/1992 1/1/1994 9/30/1997 4/1/2003 9/30/1998 9/30/1998 12/31/1997 Enregistrements Option pour les non fondateurs 12/31/1998
Linq.book Page 110 Mercredi, 18. février 2009 7:58 07
110
LINQ to Objects
Partie II
Opérateurs d’initialisation Les opérateurs d’initialisation sont utilisés pour obtenir des valeurs calculées à partir de séquences. ASTUCE Les prototypes des opérateurs d’initialisation passés en revue dans cet ouvrage ne sont pas adaptés aux DataSets. Préférez-leur les prototypes présentés au Chapitre 10.
Opérateur Distinct L’opérateur Distinct supprime les doublons dans la séquence d’entrée.
Prototype Un seul prototype de l’opérateur Distinct sera étudié dans cet ouvrage : public static IEnumerable Distinct( this IEnumerable source);
Cet opérateur retourne un objet dont l’énumération exclut les doublons de la séquence d’entrée. Le critère d’égalité entre deux éléments est déterminé avec les méthodes GetHashCode et Equals. Exceptions L’exception ArgumentNullException est levée si la source a pour valeur null. Exemples Cet exemple fonctionne selon les cinq étapes suivantes : m
affichage du nombre d’éléments contenus dans le tableau presidents ;
m
duplication des éléments du tableau ;
m
affichage de la séquence résultante ;
m
appel de l’opérateur Distinct sur la séquence concaténée ;
m
affichage du nombre d’éléments en sortie de l’opérateur.
Si tout fonctionne correctement, le nombre d’éléments renvoyés par l’opérateur Distinct devrait être égal au nombre initial d’éléments du tableau presidents. Pour obtenir le nombre d’éléments des deux séquences, nous utiliserons l’opérateur de requête standard non différé Count. Si nécessaire, reportez-vous au chapitre suivant pour avoir de plus amples informations sur cet opérateur. Listing 4.34 : Un exemple d’utilisation de l’opérateur Distinct. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield",
Linq.book Page 111 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
111
"Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; // Affichage du nombre d’éléments du tableau presidents Console.WriteLine("Nombre de présidents : " + presidents.Count()); // Duplication des éléments du tableau presidents IEnumerable presidentsWithDupes = presidents.Concat(presidents); // Affichage du nombre d’éléments du tableau presidents Console.WriteLine("Nombre de présidents après la duplication : " + presidentsWithDupes.Count()); // Suppression des doublons et affichage du nombre d’éléments IEnumerable presidentsDistinct = presidentsWithDupes.Distinct(); Console.WriteLine("Nombre de présidents distincts : " + presidentsDistinct.Count());
Voici le résultat de ce code : Nombre de présidents : 37 Nombre de présidents après la duplication : 74 Nombre de présidents distincts : 37
Opérateur Union L’opérateur Union retourne la réunion de deux séquences d’entrée.
Prototype Nous étudierons un seul prototype de l’opérateur Union dans cet ouvrage : public static IEnumerable Union( this IEnumerable first, IEnumerable second);
Ce prototype fournit un objet dont l’énumération retourne les éléments de la première séquence, privés de leurs doublons, suivis des éléments de la seconde séquence, également privés de leurs doublons. L’égalité des éléments est déterminée par les méthodes HashCode et Equals. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Pour montrer la différence entre les opérateurs Union et Concat (voir Listing 4.35), nous allons créer les séquences first et second à partir du tableau presidents. Ces deux séquences auront en commun le cinquième élément du tableau presidents. Nous afficherons le nombre d’éléments du tableau presidents, des séquences premier et second et des séquences premier et second soumises aux opérateurs Concat et Union.
Linq.book Page 112 Mercredi, 18. février 2009 7:58 07
112
LINQ to Objects
Partie II
Listing 4.35 : Un exemple d’utilisation de l’opérateur Union. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable first = presidents.Take(5); IEnumerable second = presidents.Skip(4); // Seul le cinquième élément du tableau presidents // est commun aux séquences premier et second IEnumerable concat = first.Concat(second); IEnumerable union = first.Union(second); Console.WriteLine("Nombre ➥presidents.Count()); Console.WriteLine("Nombre Console.WriteLine("Nombre Console.WriteLine("Nombre ➥concat.Count()); Console.WriteLine("Nombre ➥union.Count());
d’éléments du tableau presidents : " + d’éléments de la première séquence : " + first.Count()); d’éléments de la deuxième séquence : " + second.Count()); d’éléments après concaténation des deux séquences : " + d’éléments après union des deux séquences : " +
Ce code affiche le texte ci-après dans la fenêtre Console : Nombre Nombre Nombre Nombre Nombre
d’éléments d’éléments d’éléments d’éléments d’éléments
du tableau presidents : 37 de la première séquence : 5 de la deuxième séquence : 33 après concaténation des deux séquences : 38 après union des deux séquences : 37
Comme on pouvait s’y attendre : m
La séquence issue de l’opérateur Concat a un élément de plus que le tableau presidents.
m
La séquence issue de l’opérateur Union a le même nombre d’éléments que le tableau presidents.
Opérateur Intersect L’opérateur Intersect retourne l’intersection des deux séquences passées en entrée.
Prototype Nous étudierons un seul prototype de l’opérateur Intersect dans cet ouvrage : public static IEnumerable Intersect( this IEnumerable first, IEnumerable second);
Linq.book Page 113 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
113
Cet opérateur retourne un objet dont l’énumération est obtenue : 1. en dressant la liste des singletons de la première séquence ; 2. en énumérant les éléments de la deuxième séquence et en marquant ceux qui se trouvent dans la liste de la première étape ; 3. en énumérant les éléments marqués dans l’ordre où ils ont été collectés à l’étape 2. L’égalité des éléments est déterminée à l’aide des méthodes GetHashCode et Equals. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Nous allons utiliser les opérateurs Take et Skip pour générer deux séquences qui possèdent un seul élément en commun. Lorsque nous appliquerons l’opérateur Intersect à ces deux séquences, seul cet élément sera retourné. Listing 4.36 : Un exemple d’utilisation de l’opérateur Intersect. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable first = presidents.Take(5); IEnumerable second = presidents.Skip(4); // Seul le cinquième élément du tableau presidents // est commun aux séquences premier et second IEnumerable intersect = first.Intersect(second); Console.WriteLine("Nombre ➥presidents.Count()); Console.WriteLine("Nombre Console.WriteLine("Nombre ➥second.Count()); Console.WriteLine("Nombre ➥intersect.Count());
d’éléments dans le tableau presidents : " + d’éléments dans la première séquence : " + first.Count()); d’éléments dans la deuxième séquence : " + d’éléments après intersection des deux séquences : " +
// Affichage de la séquence résultant de l’opérateur Intersect foreach (string name in intersect) Console.WriteLine(name);
Voici le résultat de l’exécution de ce code : Nombre Nombre Nombre Nombre Carter
d’éléments d’éléments d’éléments d’éléments
dans le tableau presidents : 37 dans la première séquence : 5 dans la deuxième séquence : 33 après intersection des deux séquences : 1
Linq.book Page 114 Mercredi, 18. février 2009 7:58 07
114
LINQ to Objects
Partie II
Opérateur Except Cet opérateur retourne une séquence qui contient tous les éléments de la première séquence qui n’apparaissent pas dans la seconde.
Prototype Nous étudierons un seul prototype de l’opérateur Except dans cet ouvrage : public static IEnumerable Except( this IEnumerable first, IEnumerable second);
Cet opérateur retourne un objet dont l’énumération effectue les actions suivantes : 1. Énumération des éléments de la première séquence en éliminant les doublons. 2. Énumération des éléments de la deuxième séquence en ne conservant que les éléments qui n’ont pas été retenus à la première étape. 3. Création d’une collection de sortie qui contient les éléments retenus à la deuxième étape. L’égalité des éléments est déterminée à l’aide des méthodes GetHashCode et Equals. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Une fois encore, nous utiliserons le tableau presidents. Supposons que vous effectuiez un traitement sur les éléments du tableau presidents et que les éléments obtenus soient placés dans une séquence. Si de nouveaux éléments sont ajoutés à cette séquence, il sera inutile d’appliquer le traitement aux éléments qui l’ont déjà subi. Pour ne sélectionner que les nouveaux éléments, il suffira de transmettre l’ancienne liste et la nouvelle liste à l’opérateur Except. Vous pourrez alors appliquer le traitement aux nouveaux venus. Dans cet exemple, nous allons supposer que les quatre premiers éléments ont déjà subi le traitement. Pour obtenir la liste des autres éléments, il suffit d’utiliser l’opérateur Except (voir Listing 4.37). Listing 4.37 : Un exemple d’utilisation de l’opérateur Except. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};
Linq.book Page 115 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
115
// Définition de la séquence processed IEnumerable processed = presidents.Take(4); IEnumerable exceptions = presidents.Except(processed); foreach (string name in exceptions) Console.WriteLine(name);
Comme on pouvait s’y attendre, les noms des présidents affichés dans la console commencent au cinquième : Carter Cleveland Clinton Coolidge Eisenhower Fillmore Ford Garfield Grant Harding Harrison Hayes Hoover Jackson Jefferson Johnson Kennedy Lincoln Madison McKinley Monroe Nixon Pierce Polk Reagan Roosevelt Taft Taylor Truman Tyler Van Buren Washington Wilson
Opérateurs de conversion Les opérateurs de conversion représentent une façon simple et pratique de convertir des séquences en des collections d’un autre type. Opérateur Cast L’opérateur Cast convertit tous les éléments de la séquence d’entrée dans le type spécifié et les place dans la séquence de sortie.
Prototype Nous étudierons un seul prototype de l’opérateur Cast dans cet ouvrage : public static IEnumerable Cast( this IEnumerable source);
Linq.book Page 116 Mercredi, 18. février 2009 7:58 07
116
LINQ to Objects
Partie II
Contrairement à la majorité des opérateurs de requête standard différés, le premier argument de l’opérateur Cast est de type IEnumerable, et non IEnumerable. Ceci s’explique par le fait que l’opérateur Cast a été défini pour être appelé sur des classes qui implémentent l’interface IEnumerable. En particulier les collections C# héritées, définies avant la sortie de C# 2.0 et des génériques. Vous pouvez utiliser l’opérateur Cast sur toute collection C# héritée, à condition qu’elle implémente l’interface IEnumerable. Une séquence IEnumerable sera alors créée. Étant donné que la plupart des opérateurs de requête standard ne travaillent qu’avec des séquences de type IEnumerable, vous devrez utiliser l’opérateur Cast ou OfType (voir la section suivante) pour obtenir un type IEnumerable compatible. Ayez bien cela en tête si vous prévoyez d’appliquer des opérateurs de requête standard sur des collections héritées. L’opérateur Cast retourne un objet dont l’énumération transforme les éléments de la séquence d’entrée pour qu’ils soient du type T. Si un élément ne peut pas être converti, une exception est levée. Il est donc important de n’utiliser cet opérateur que lorsque l’on est sûr que tous les éléments de la séquence d’entrée peuvent être convertis. ASTUCE Lorsque vous appliquez une requête LINQ à une collection héritée, n’oubliez pas d’utiliser un opérateur Cast ou OfType pour convertir la collection héritée en une séquence IEnumerable compatible avec les opérateurs de requête standard.
Exceptions L’exception ArgumentNullException est levée si l’argument source a pour valeur null. L’exception InvalidCastException est levée si un des éléments de la séquence d’entrée ne peut pas être converti dans le type T. Exemples Dans cet exemple, nous utiliserons la méthode GetEmployeesArrayList de la classe commune Employee pour obtenir un objet ArrayList hérité (non générique). Cet objet sera alors converti en un IEnumerable avec l’opérateur Cast (voir Listing 4.38). Listing 4.38 : Ce code convertit un ArrayList en un IEnumerable qui peut être utilisé avec les opérateurs de requête standard. ArrayList employees = Employee.GetEmployeesArrayList(); Console.WriteLine("Le type de l’objet employees est " + employees.GetType()); var seq = employees.Cast(); Console.WriteLine("Le type de l’objet seq est " + seq.GetType()); var emps = seq.OrderBy(e => e.lastName); foreach (Employee emp in emps) Console.WriteLine("{0} {1}", emp.firstName, emp.lastName);
Linq.book Page 117 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
117
La première ligne utilise la méthode GetEmployeesArrayList pour obtenir un ArrayList d’objets Employee. Le type de l’objet ainsi obtenu est affiché par la deuxième ligne. Dans la troisième ligne, l’objet employees est converti en une séquence IEnumerable en appelant l’opérateur Cast. Le type de l’objet obtenu est affiché dans la quatrième ligne. Les autres lignes énumèrent l’objet IEnumerable afin de prouver que la conversion a réussi. Voici le résultat de l’exécution de ce code : Le type de l’objet employees est System.Collections.ArrayList Le type de l’objet seq est System.Linq.Enumerable+d__b0`1[LINQChapter4.Employee] Kevin Flynn William Gates Anders Hejlsberg David Lightman Joe Rattz
Le type de l’objet employees est clairement identifiable. Il en va autrement de celui de l’objet seq. Ce que nous pouvons dire, c’est qu’il est différent du précédent et qu’il ressemble à une séquence. Nous pouvons également remarquer le mot CastIterator dans l’intitulé de son type. Vous rappelez-vous de ce qui a été dit à propos des opérateurs différés : ces opérateurs retournent non pas une séquence en sortie, mais un objet dont l’énumération fournit les éléments de la séquence de sortie. L’objet seq est précisément de ce type. ATTENTION L’opérateur Cast essaye de convertir tous les éléments de la séquence d’entrée dans le type spécifié. Si un de ces éléments ne peut pas être converti, une exception InvalidCastException est levée. Si une telle situation est possible, préférez l’opérateur OfType à l’opérateur Cast.
Opérateur OfType Cet opérateur change le type des éléments de la séquence d’entrée qui le permettent et les place dans la séquence de sortie.
Prototype Nous étudierons un seul prototype de l’opérateur OfType dans cet ouvrage : public static IEnumerable OfType( this IEnumerable source);
Le premier argument de l’opérateur OfType est de type IEnumerable, et non IEnumerable. Tout comme Cast, OfType est destiné à être appelé sur des classes qui implémentent l’interface IEnumerable. En particulier les collections C# héritées, définies avant la sortie de C# 2.0 et des génériques.
Linq.book Page 118 Mercredi, 18. février 2009 7:58 07
118
LINQ to Objects
Partie II
Vous pouvez utiliser l’opérateur OfType sur toute collection C# héritée, à condition qu’elle implémente l’interface IEnumerable. Une séquence IEnumerable sera alors créée. Étant donné que la plupart des opérateurs de requête standard ne travaillent qu’avec des séquences de type IEnumerable, vous devrez utiliser l’opérateur OfType ou Cast (voir la section précédente) pour obtenir un type IEnumerable compatible. Ayez bien cela en tête si vous prévoyez d’appliquer des opérateurs de requête standard sur des collections héritées. L’opérateur OfType retourne un objet dont l’énumération transforme les éléments de la séquence d’entrée pour qu’ils soient du type T (seuls les éléments qui supportent la conversion sont convertis). Exceptions L’exception ArgumentNullException est levée si l’argument source a pour valeur null. Exemples Dans l’exemple du Listing 4.39, nous créons un ArrayList contenant des objets issus des classes communes Employee et EmployeeOptionEntry. Appliqué à cet objet, l’opérateur Cast ne parvient pas à effectuer la conversion de type. Quelques lignes plus bas, l’opérateur OfType, appliqué à ce même objet, passe haut la main la conversion. Listing 4.39 : Un exemple d’appel des opérateurs Cast et OfType. ArrayList al = new ArrayList(); al.Add(new Employee { id = 1, firstName = "Joe", lastName = "Rattz" }); al.Add(new Employee { id = 2, firstName = "William", lastName = "Gates" }); al.Add(new EmployeeOptionEntry { id = 1, optionsCount = 0 }); al.Add(new EmployeeOptionEntry { id = 2, optionsCount = 99999999999 }); al.Add(new Employee { id = 3, firstName = "Anders", lastName = "Hejlsberg" }); al.Add(new EmployeeOptionEntry { id = 3, optionsCount = 848475745 }); var items = al.Cast(); Console.WriteLine("Tentative d’énumération de la séquence issue de l’opérateur Cast ..."); try { foreach (Employee item in items) Console.WriteLine("{0} {1} {2}", item.id, item.firstName, item.lastName); } catch (Exception ex) { Console.WriteLine("{0}{1}", ex.Message, System.Environment.NewLine); } Console.WriteLine("Tentative d’énumération de la séquence issue de l’opérateur ➥OfType ..."); var items2 = al.OfType(); foreach (Employee item in items2) Console.WriteLine("{0} {1} {2}", item.id, item.firstName, item.lastName);
Le premier bloc d’instructions crée et remplit l’objet ArrayList al. L’opérateur Cast est alors appliqué à cet objet. Le bloc d’instructions suivant tente d’énumérer les éléments de la séquence issue de l’opérateur Cast (sans ces instructions, l’erreur de
Linq.book Page 119 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
119
conversion n’aurait pas été identifiée). L’énumération est protégée par une structure try/catch. Ainsi, un message est affiché lorsqu’une erreur de conversion est détectée. Le code se poursuit par l’application de l’opérateur OfType sur la séquence al. Ici, aucune erreur de conversion n’étant possible, les éléments de la séquence retournée par OfType sont simplement énumérés (la structure try/catch ne devrait pas être retirée d’un code dont la portée dépasse le cadre pédagogique). Voici les résultats de ce code : Tentative d’énumération de la séquence issue de l’opérateur Cast ... 1 Joe Rattz 2 William Gates Unable to cast object of type ’LINQChapter4.EmployeeOptionEntry’ to type ’LINQChapter4.Employee’. Tentative d’énumération de la séquence issue de l’opérateur OfType ... 1 Joe Rattz 2 William Gates 3 Anders Hejlsberg
Il n’a pas été possible d’énumérer tous les résultats de la séquence retournée par l’opérateur Cast sans qu’une exception ne soit générée. En revanche, tous les résultats de la séquence retournée par l’opérateur OfType ont pu être énumérés, et seuls les éléments de type employee ont été inclus dans la séquence de sortie. ASTUCE Si vous voulez convertir une collection non générique (une collection héritée, par exemple) en une séquence IEnumerable, utilisez l’opérateur OfType et non l’opérateur Cast si les données à convertir peuvent être de plusieurs types différents.
Opérateur AsEnumerable L’opérateur AsEnumerable retourne la séquence d’entrée IEnumerable en tant qu’IEnumerable.
Prototype Un seul prototype de l’opérateur AsEnumerable sera étudié dans cet ouvrage : public static IEnumerable AsEnumerable( this IEnumerable source);
Un rapide coup d’œil à ce prototype montre qu’AsEnumerable utilise la séquence d’entrée IEnumerable source et la retourne typée en IEnumerable. Cela peut sembler quelque peu étrange. En effet, quel est l’intérêt de transformer un IEnumerable en un autre IEnumerable ? Les opérateurs de requête standard sont définis pour opérer sur des séquences LINQ to Objects "normales", c’est-à-dire qui implémentent l’interface IEnumerable.
Linq.book Page 120 Mercredi, 18. février 2009 7:58 07
120
LINQ to Objects
Partie II
D’autres types de collections, par exemple celles qui accèdent à des bases de données, peuvent utiliser des séquences et des opérateurs qui leur sont propres. Généralement, lorsque vous appliquez un opérateur de requête sur ces types de collections, cet opérateur est spécifique à la collection. En utilisant l’opérateur AsEnumerable, vous allez pouvoir convertir une séquence d’entrée en une séquence IEnumerable "normale", directement utilisable dans un opérateur de requête standard. À titre d’exemple, lorsque nous nous intéresserons à LINQ to SQL un peu plus loin dans ce livre, vous verrez que cette partie de LINQ utilise des séquences de type IQueryable et implémente ses propres opérateurs. Ces derniers sont spécifiques aux séquences IQueryable. Lorsque vous appelez l’opérateur Where sur une séquence IQueryable, c’est la méthode Where de LINQ to SQL qui est invoquée, et non l’opérateur de requête standard Where de LINQ to Objects ! Si vous essayez d’invoquer un opérateur de requête standard sur un objet IQueryable, une exception sera générée, à moins qu’un opérateur LINQ to SQL de même nom n’existe. L’opérateur AsEnumerable permet de convertir une séquence IQueryable en une séquence IEnumerable, permettant ainsi l’utilisation des opérateurs de requête standard. AsEnumerable se révèle très pratique si vous devez contrôler dans quelle API un opérateur doit être appelé. Exceptions Aucune exception n’est générée par cet opérateur. Exemples Pour mieux comprendre comment fonctionne cet opérateur, nous allons raisonner sur un cas pratique. Nous utiliserons l’exemple LINQ to SQL donné au Chapitre 1. Voici le code utilisé : using using using using
System; System.Linq; System.Data.Linq; nwind;
Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); var custs = from c in db.Customers where c.City == "Rio de Janeiro" select c; foreach (var cust in custs) Console.WriteLine("{0}", cust.CompanyName);
Et voici les résultats de cet exemple : Hanari Carnes Que Delícia Ricardo Adocicados
Linq.book Page 121 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
121
Pour que ce code soit en mesure de fonctionner, vous devez ajouter à votre projet : m
l’assembly System.Data.Linq.dll ;
m
une directive using qui pointe sur l’espace de noms nwind ;
m
les classes d’entités générées, qui seront étudiées dans les chapitres relatifs à LINQ to SQL.
Supposons que vous deviez inverser l’ordre des enregistrements issus de la base de données. Vous utiliserez l’opérateur Reverse, abordé un peu plus loin dans ce chapitre. Le Listing 4.40 représente le code précédent, modifié pour appeler l’opérateur Reverse. Listing 4.40 : Appel de l’opérateur Reverse. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); var custs = (from c in db.Customers where c.City == "Rio de Janeiro" select c) .Reverse(); foreach (var cust in custs) Console.WriteLine("{0}", cust.CompanyName);
Comme vous pouvez le voir, l’unique modification a consisté à appeler la méthode Reverse. Voici les résultats renvoyés dans la console : Exception non gérée : System.NotSupportedException : L’opérateur ’Reverse’ n’est pas ➥supporté. …
Que s’est-il passé ? Étant donné qu’il n’existe aucune méthode Reverse pour l’interface IQueryable, une exception a été générée. C’est là qu’intervient la méthode AsEnumerable. Grâce à elle, la séquence IQueryable va être convertie en une séquence IEnumerable, et il sera possible de lui appliquer la méthode Reverse. Voici dans le Listing 4.41 le code modifié. Listing 4.41 : Appel de l’opérateur AsEnumerable avant l’opérateur Reverse. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); var custs = (from c in db.Customers where c.City == "Rio de Janeiro" select c) .AsEnumerable() .Reverse(); foreach (var cust in custs) Console.WriteLine("{0}", cust.CompanyName);
Linq.book Page 122 Mercredi, 18. février 2009 7:58 07
122
LINQ to Objects
Partie II
La méthode AsEnumerable est appelée avant l’opérateur Reverse. C’est donc l’opérateur Reverse de LINQ to Objects qui va être invoqué. Voici les résultats affichés dans la console : Ricardo Adocicados Que Delícia Hanari Carnes
Ces résultats sont bien affichés dans l’ordre inverse de la séquence originale. L’opérateur Reverse a donc bien fonctionné. Opérateurs dédiés aux éléments Ces opérateurs permettent d’extraire des éléments dans la séquence d’entrée. Opérateur DefaultIfEmpty L’opérateur DefaultIfEmpty retourne une séquence qui contient un élément par défaut si la séquence d’entrée est vide.
Prototypes Deux prototypes de l’opérateur DefaultIfEmpty seront étudiés dans cet ouvrage. Premier prototype public static IEnumerable DefaultIfEmpty( this IEnumerable source);
Ce prototype retourne un objet dont l’énumération renvoie chacun des éléments de la séquence d’entrée. Si cette dernière est vide, une séquence de type default(T) contenant un seul élément est retournée. Pour les références et les types nullables, la valeur par défaut est null. Contrairement aux autres opérateurs dédiés aux éléments, DefaultIfEmpty retourne une séquence de type IEnumerable et non de type T. Il existe d’autres opérateurs de type, mais nous ne les étudierons pas dans ce chapitre, car ils ne sont pas différés. Le second prototype permet de spécifier la valeur par défaut. Second prototype public static IEnumerable DefaultIfEmpty( this IEnumerable source, T defaultValue);
Cet opérateur est utile aux opérateurs qui génèrent des exceptions lorsque la séquence d’entrée est vide. Il permet également à l’opérateur GroupJoin de générer des jointures externes à gauche (left outer join).
Linq.book Page 123 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
123
Exceptions Une exception ArgumentNullException est levée si l’argument source a pour valeur null. Exemples Dans ce premier exemple, nous allons rechercher le nom "Jones" dans le tableau presidents (voir Listing 4.42). Un message indiquera si ce nom a été ou n’a pas été trouvé. Listing 4.42 : Premier exemple du prototype DefaultIfEmpty, sans l’opérateur DefaultIfEmpty. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string jones = presidents.Where(n => n.Equals("Jones")).First(); if (jones != null) Console.WriteLine("Jones was found"); else Console.WriteLine("Jones was not found");
Voici les résultats affichés dans la console : Exception non gérée : System.InvalidOperationException : La séquence ne contient ➥aucun élément …
Le nom "Jones" n’ayant pas été trouvé, une séquence vide est passée à l’opérateur First. Ce dernier n’appréciant pas les séquences vides, il a généré une exception. Nous allons maintenant ajouter un appel à l’opérateur DefaultIfEmpty entre les opérateurs Where et First. Ainsi, c’est non pas une séquence vide, mais une séquence contenant un élément null qui sera passée à l’opérateur First (voir Listing 4.43). Listing 4.43 : Second exemple du premier prototype, cette fois en utilisant DefaultIfEmpty. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string jones = presidents.Where(n => n.Equals("Jones")).DefaultIfEmpty().First(); if (jones != null) Console.WriteLine("Jones was found."); else Console.WriteLine("Jones was not found.");
Linq.book Page 124 Mercredi, 18. février 2009 7:58 07
124
LINQ to Objects
Partie II
Voici le résultat : Jones n’a pas été trouvé.
Voici maintenant un exemple pour le second prototype (voir Listing 4.44). Ici, nous pouvons choisir la valeur retournée lorsque la séquence d’entrée est vide. Listing 4.44 : Un exemple du second prototype de l’opérateur DefaultIfEmpty. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.Where(n => n.Equals("Jones")).DefaultIfEmpty("Missing").First(); Console.WriteLine(name);
Voici le résultat : Absent
Nous allons maintenant réaliser une jointure externe à gauche en utilisant les opérateurs GroupJoin et DefaultIfEmpty. Nous travaillerons avec deux classes communes, Employee et EmployeeOptionEntry. Dans le Listing 4.45, l’opérateur DefaultIfEmpty n’est pas utilisé. Listing 4.45 : Un exemple sans l’opérateur DefaultIfEmpty. ArrayList employeesAL = Employee.GetEmployeesArrayList(); // Ajout d’un nouvel employé sans enregistrement EmployeeOptionEntry correspondant employeesAL.Add(new Employee { id = 102, firstName = "Michael", lastName = "Bolton" }); Employee[] employees = employeesAL.Cast().ToArray(); EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries(); var employeeOptions = employees .GroupJoin( empOptions, e => e.id, o => o.id, (e, os) => os .Select(o => new { id = e.id, name = string.Format("{0} {1}", e.firstName, e.lastName), options = o != null ? o.optionsCount : 0 })) .SelectMany(r => r); foreach (var item in employeeOptions) Console.WriteLine(item);
Linq.book Page 125 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
125
Quelques précisions à propos de cet exemple : m
Le code est très proche de celui qui a été utilisé pour illustrer l’opérateur GroupJoin.
m
Étant donné que chaque employé de la classe commune employee a une correspondance dans la classe commune EmployeeOptionEntry, nous allons ajouter un nouvel employé, Michael Bolton, à l’objet ArrayList des employés, de telle sorte qu’aucun objet EmployeeOptionEntry ne lui corresponde.
m
L’opérateur DefaultIfEmpty ne sera pas appelé dans cet exemple.
Voici les résultats de la requête : { { { { { { { { {
id id id id id id id id id
= = = = = = = = =
1, name = 2, name = 2, name = 2, name = 3, name = 3, name = 3, name = 4, name = 101, name
Joe Rattz, options = 2 } William Gates, options = 10000 } William Gates, options = 10000 } William Gates, options = 10000 } Anders Hejlsberg, options = 5000 } Anders Hejlsberg, options = 7500 } Anders Hejlsberg, options = 7500 } David Lightman, options = 1500 } = Kevin Flynn, options = 2 }
Comme aucun objet ne correspond à l’employé Michael Bolton dans le tableau EmployeeOptionArray, aucune information concernant cet employé n’est affichée dans la console. Nous allons maintenant utiliser l’opérateur DefaultIfEmpty pour créer un enregistrement par défaut pour cet employé (voir Listing 4.46). Listing 4.46 : Un exemple d’utilisation de l’opérateur DefaultIfEmpty. ArrayList employeesAL = Employee.GetEmployeesArrayList(); // Ajout d’un nouvel employé sans enregistrement EmployeeOptionEntry correspondant employeesAL.Add(new Employee { id = 102, firstName = "Michael", lastName = "Bolton" }); Employee[] employees = employeesAL.Cast().ToArray(); EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries(); var employeeOptions = employees .GroupJoin( empOptions, e => e.id, o => o.id, (e, os) => os .DefaultIfEmpty() .Select(o => new { id = e.id,
Linq.book Page 126 Mercredi, 18. février 2009 7:58 07
126
LINQ to Objects
Partie II
name = string.Format("{0} {1}", e.firstName, e.lastName), options = o != null ? o.optionsCount : 0 })) .SelectMany(r => r); foreach (var item in employeeOptions) Console.WriteLine(item);
Le premier bloc de code ajoute l’employé Michael Bolton sans lui associer un objet EmployeeOptionEntry. Le deuxième bloc de code effectue une requête sur les données en faisant appel à l’opérateur DefaultIfEmpty. Voici les résultats : { { { { { { { { { {
id id id id id id id id id id
= = = = = = = = = =
1, name = 2, name = 2, name = 2, name = 3, name = 3, name = 3, name = 4, name = 101, name 102, name
Joe Rattz, options = 2 } William Gates, options = 10000 } William Gates, options = 10000 } William Gates, options = 10000 } Anders Hejlsberg, options = 5000 } Anders Hejlsberg, options = 7500 } Anders Hejlsberg, options = 7500 } David Lightman, options = 1500 } = Kevin Flynn, options = 2 } = Michael Bolton, options = 0 }
L’opérateur DefaultIfEmpty a bien ajouté un objet EmployeeOptionEntry pour l’employé Michael Bolton. Opérateurs de génération Ces opérateurs sont utilisés pour générer des séquences. Opérateur Range L’opérateur Range génère une séquence d’entiers.
Prototype Un seul prototype de l’opérateur Range sera étudié dans cet ouvrage : public static IEnumerable Range( int start, int count);
Ce prototype génère une séquence de count entiers à partir de start. L’opérateur Range n’est pas une méthode d’extension. Il n’étend pas le type IEnumerable. INFO L’opérateur Range n’est pas une méthode d’extension. C’est une méthode statique appelée dans l’assembly System.Linq.Enumerable.
Linq.book Page 127 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
127
Exceptions Une exception ArgumentOutOfRangeException est levée si count est inférieur à zéro ou si start+count-1 est supérieur à int.MaxValue. Exemples Listing 4.47 : Un exemple d’appel de l’opérateur Range. IEnumerable ints = Enumerable.Range(1, 10); foreach(int i in ints) Console.WriteLine(i);
Je tiens à rappeler que l’opérateur Range n’est pas appliqué à une séquence : il s’agit d’une méthode statique de la classe System.Linq.Enumerable. Voici les résultats affichés dans la console : 1 2 3 4 5 6 7 8 9 10
Opérateur Repeat L’opérateur Repeat génère une séquence en répétant plusieurs fois un même élément.
Prototype Un seul prototype de l’opérateur Repeat sera étudié dans cet ouvrage : public static IEnumerable Repeat( T element, int count);
Ce prototype retourne un objet dont l’énumération produit count éléments T. L’opérateur Repeat n’est pas une méthode d’extension. Il n’étend pas le type IEnumerable. INFO L’opérateur Repeat n’est pas une méthode d’extension. C’est une méthode statique appelée dans l’assembly System.Linq.Enumerable.
Exceptions Une exception ArgumentOutOfRangeException est levée si count est inférieur à zéro.
Linq.book Page 128 Mercredi, 18. février 2009 7:58 07
128
LINQ to Objects
Partie II
Exemples Listing 4.48 : Génération d’une séquence de dix éléments Integer initialisés à la valeur 2. IEnumerable ints = Enumerable.Repeat(2, 10); foreach(int i in ints) Console.WriteLine(i);
Voici les résultats affichés dans la console : 2 2 2 2 2 2 2 2 2 2
Opérateur Empty L’opérateur Empty génère une séquence vide de type T.
Prototype Un seul prototype de l’opérateur Empty sera étudié dans cet ouvrage : public static IEnumerable Empty();
Ce prototype renvoie un objet dont l’énumération produit 0 élément de type T. L’opérateur Empty n’est pas une méthode d’extension. Il n’étend pas le type IEnumerable. INFO L’opérateur Empty n’est pas une méthode d’extension. C’est une méthode statique appelée dans l’assembly System.Linq.Enumerable.
Exceptions Aucune. Exemples Cet exemple génère une séquence de type String par l’intermédiaire de l’opérateur Empty. La séquence générée ainsi que son nombre d’éléments sont ensuite affichés dans la console. Listing 4.49 : Génération d’une séquence vide de String. IEnumerable strings = Enumerable.Empty(); foreach(string s in strings)
Linq.book Page 129 Mercredi, 18. février 2009 7:58 07
Chapitre 4
Les opérateurs différés
129
Console.WriteLine(s); Console.WriteLine(strings.Count());
Voici le résultat affiché dans la console : 0
Comme vous le voyez, la boucle foreach ne produit aucun résultat. Ceci est normal, puisqu’il n’y a aucun élément à afficher.
Résumé Ce chapitre a illustré la plupart des prototypes des opérateurs différés, du plus simple au plus complexe. En isolant les opérateurs de requête standard différés de leurs acolytes non différés, j’ai mis l’accent sur l’impact que pouvait avoir l’exécution non instantanée d’une requête. Au chapitre suivant, vous découvrirez les opérateurs de requête standard non différés. Ce sera le dernier chapitre dédié à LINQ to Objects.
Linq.book Page 130 Mercredi, 18. février 2009 7:58 07
Linq.book Page 131 Mercredi, 18. février 2009 7:58 07
5 Les opérateurs non différés Au chapitre précédent, nous nous sommes intéressés aux opérateurs de requête différés. Ces opérateurs sont faciles à identifier, car ils retournent un IEnumerable ou un OrderedSequence. Nous allons maintenant nous intéresser aux opérateurs de requête standard non différés. Ces opérateurs sont faciles à reconnaître, car le résultat retourné a un type différent de IEnumerable et OrderedSequence. Pour pouvoir exécuter les exemples de ce chapitre, assurez-vous que vous avez référencé les espaces de noms (directive using), les assemblies et les codes communs nécessaires.
Espaces de noms référencés Les exemples de ce chapitre vont utiliser les espaces de noms System.Linq, System.Collections et System.Collections.Generic. Si elles ne sont pas déjà présentes, vous devez donc ajouter les directives using suivantes dans votre code : using System.Linq; using System.Collections; using System.Collections.Generic;
Si vous parcourez le code source mis à disposition sur le site www.pearson.fr, vous verrez que j’ai également ajouté une directive using sur l’espace de noms System.Diagnostic. Cette directive n’est pas nécessaire si vous saisissez directement les exemples de ce chapitre. Elle n’est là que pour les besoins propres du code source.
Classes communes Pour fonctionner entièrement, certains exemples de ce chapitre nécessitent des classes additionnelles. Cette section décrit les quatre classes qui seront utilisées par certains exemples de ce chapitre.
Linq.book Page 132 Mercredi, 18. février 2009 7:58 07
132
LINQ to Objects
Partie II
La classe Employee permet de travailler sur les employés d’une entreprise. Elle contient des méthodes statiques qui retournent un tableau d’employés de type ArrayList. public class Employee { public int id; public string firstName; public string lastName; public static ArrayList GetEmployeesArrayList() { ArrayList al = new ArrayList(); al.Add(new Employee al.Add(new Employee al.Add(new Employee al.Add(new Employee al.Add(new Employee return (al);
{ { { { {
id id id id id
= = = = =
1, firstName = 2, firstName = 3, firstName = 4, firstName = 101, firstName
"Joe", lastName = "Rattz" }); "William", lastName = "Gates" }); "Anders", lastName = "Hejlsberg" }); "David", lastName = "Lightman" }); = "Kevin", lastName = "Flynn" });
} public static Employee[] GetEmployeesArray() { return ((Employee[])GetEmployeesArrayList().ToArray()); } }
La classe EmployeeOptionEntry représente le montant des stock-options des employés. Elle contient une méthode statique qui retourne un tableau de stock-options. public class EmployeeOptionEntry { public int id; public long optionsCount; public DateTime dateAwarded; public static EmployeeOptionEntry[] GetEmployeeOptionEntries() { EmployeeOptionEntry[] empOptions = new EmployeeOptionEntry[] { new EmployeeOptionEntry { id = 1, optionsCount = 2, dateAwarded = DateTime.Parse("1999/12/31") }, new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("1992/06/30") }, new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("1994/01/01") }, new EmployeeOptionEntry { id = 3, optionsCount = 5000, dateAwarded = DateTime.Parse("1997/09/30") }, new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("2003/04/01") }, new EmployeeOptionEntry { id = 3, optionsCount = 7500, dateAwarded = DateTime.Parse("1998/09/30") },
Linq.book Page 133 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
133
new EmployeeOptionEntry { id = 3, optionsCount = 7500, dateAwarded = DateTime.Parse("1998/09/30") }, new EmployeeOptionEntry { id = 4, optionsCount = 1500, dateAwarded = DateTime.Parse("1997/12/31") }, new EmployeeOptionEntry { id = 101, optionsCount = 2, dateAwarded = DateTime.Parse("1998/12/31") } }; return (empOptions); } }
Plusieurs opérateurs utilisent des classes qui implémentent l’interface IEqualityComparer. Ceci afin de tester l’égalité entre deux éléments. Cette interface est utile lorsque le terme "égalité" doit être pris au sens large. Par exemple, deux chaînes peuvent être considérées égales, même si leur casse diffère. L’interface IEqualityComparer ayant été abordée en détail au chapitre précédent, nous n’y reviendrons pas. Dans les exemples de ce chapitre, nous aurons besoin d’une classe permettant de comparer plusieurs nombres stockés dans des chaînes de caractères. Ainsi, par exemple, les chaînes "17" et "00017" seront considérées comme égales. La classe MyStringifieldNumberComparer se chargera de ce type de comparaison. public class MyStringifiedNumberComparer : IEqualityComparer { public bool Equals(string x, string y) { return(Int32.Parse(x) == Int32.Parse(y)); } public int GetHashCode(string obj) { return Int32.Parse(obj).ToString().GetHashCode(); } }
Cette implémentation de l’interface IEqualityComparer ne fonctionne que sur des variables de type string. La technique utilisée consiste à convertir les valeurs string en int32. Ainsi, par exemple, la valeur "002" sera convertie en un entier de valeur 2, et les éventuels zéros en tête de la chaîne n’affecteront pas la conversion. Dans plusieurs exemples de ce chapitre, nous aurons besoin d’une classe dans laquelle le champ clé des enregistrements n’est pas forcément unique. La classe Actor a été créée dans ce but (le champ birthYear sera utilisé comme clé). public class Actor { public int birthYear; public string firstName; public string lastName;
Linq.book Page 134 Mercredi, 18. février 2009 7:58 07
134
LINQ to Objects
public static Actor[] GetActors() { Actor[] actors = new Actor[] { new Actor { birthYear = 1964, new Actor { birthYear = 1968, new Actor { birthYear = 1960, new Actor { birthYear = 1964, };
Partie II
firstName firstName firstName firstName
= = = =
"Keanu", lastName = "Reeves" }, "Owen", lastName = "Wilson" }, "James", lastName = "Spader" }, "Sandra", lastName = "Bullock" },
return (actors); } }
Les opérateurs non différés, par groupes fonctionnels Dans cette section, nous avons organisé les différents opérateurs de requête standard non différés par grands groupes fonctionnels. Opérateurs de conversion Les opérateurs de conversion sont utilisés pour convertir des séquences dans des collections d’un autre type. L’opérateur ToArray L’opérateur ToArray crée un tableau de type T à partir d’une séquence d’entrée de type T.
Prototype Un seul prototype de l’opérateur ToArray sera étudié dans ce livre : public static T[] ToArray( this IEnumerable source);
Ce prototype admet un seul paramètre : une séquence source d’éléments de type T. Il renvoie un tableau d’éléments de type T. Exceptions L’exception ArgumentNullExpression est levée si l’argument a pour valeur null. Exemples Nous allons créer une séquence IEnumerable en appliquant l’opérateur OfType à un tableau. Une fois la séquence obtenue, nous la passerons à l’opérateur ToArray pour placer les différents éléments dans un tableau (voir Listing 5.1). Listing 5.1 : Un exemple d’appel à l’opérateur ToArray. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson",
Linq.book Page 135 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
135
"Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string[] names = presidents.OfType().ToArray(); foreach (string name in names) Console.WriteLine(name);
Dans un premier temps, le tableau presidents est converti en une séquence IEnumerable avec l’opérateur OfType. Dans un second temps, cette séquence est convertie en un tableau en utilisant l’opérateur ToArray. Le tableau est immédiatement initialisé, car ToArray est un opérateur non différé. Voici le résultat affiché dans la console : Adams Arthur Buchanan Bush Carter Cleveland Clinton Coolidge Eisenhower Fillmore Ford Garfield Grant Harding Harrison Hayes Hoover Jackson Jefferson Johnson Kennedy Lincoln Madison McKinley Monroe Nixon Pierce Polk Reagan Roosevelt Taft Taylor Truman Tyler Van Buren Washington Wilson
Vous aurez certainement remarqué que ce code est redondant. En effet, le tableau presidents est déjà une séquence, puisque dans C# 3.0 les tableaux implémentent l’interface IEnumerable. L’appel à l’opérateur ToArray aurait donc pu être évité.
Linq.book Page 136 Mercredi, 18. février 2009 7:58 07
136
LINQ to Objects
Partie II
Mais alors qu’auriez-vous pensé de ce code qui se serait contenté de convertir un tableau en… un tableau ? L’opérateur ToArray a deux avantages : il permet de mémoriser une séquence jusqu’à son énumération et de s’assurer que plusieurs énumérations du tableau travailleront sur les mêmes données. Opérateur ToList L’opérateur ToList crée une liste d’éléments de type T à partir d’une séquence d’entrée de type T.
Prototype Un seul prototype de l’opérateur ToList sera étudié dans ce livre : public static List ToList( this IEnumerable source);
Cet opérateur admet un argument : une séquence d’entrée source de type T. Il renvoie une liste d’éléments de type T. Exceptions L’exception ArgumentNullExpression est levée si l’argument source a pour valeur null. Exemples Listing 5.2 : Un appel à l’opérateur ToList. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; List names = presidents.ToList(); foreach (string name in names) Console.WriteLine(name);
Ce code utilise les mêmes données que l’exemple précédent. Mais, ici, l’opérateur OfType n’est pas appelé pour créer une séquence intermédiaire de type IEnumerable : le tableau presidents est directement converti en une liste de type List. Voici les résultats affichés dans la console : Adams Arthur
Linq.book Page 137 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
137
Buchanan Bush Carter Cleveland Clinton Coolidge Eisenhower Fillmore Ford Garfield Grant Harding Harrison Hayes Hoover Jackson Jefferson Johnson Kennedy Lincoln Madison McKinley Monroe Nixon Pierce Polk Reagan Roosevelt Taft Taylor Truman Tyler Van Buren Washington Wilson
Tout comme ToArray, ToList a deux avantages : il permet de mémoriser une séquence jusqu’à son énumération et de s’assurer que plusieurs énumérations travailleront sur les mêmes données. Opérateur ToDictionary Cet opérateur admet au minimum deux paramètres en entrée : une séquence d’entrée de type T et une clé de type K. Il crée un dictionnaire de type .
Si l’argument facultatif elementSelector est spécifié dans le prototype, le dictionnaire créé est de type . Les valeurs stockées sont de type E, différent du type d’entrée T. INFO Si la classe C# Dictionary ne vous est pas familière, sachez qu’elle permet de mémoriser des couples élément/clé (où clé est unique pour chaque élément). Pour retrouver un élément dans la liste, il suffit d’indexer le tableau en utilisant la clé.
Linq.book Page 138 Mercredi, 18. février 2009 7:58 07
138
LINQ to Objects
Partie II
Prototypes Quatre prototypes de l’opérateur ToDictionary seront étudiés dans ce livre. Premier prototype public static Dictionary ToDictionary( this IEnumerable source, Func keySelector);
Ce prototype crée un dictionnaire de type en énumérant la séquence d’entrée source. Le délégué keySelector est appelé pour obtenir une valeur clé pour chaque élément (c’est cette valeur qui sera inscrite dans le dictionnaire). Les éléments stockés dans le dictionnaire sont de même type que ceux de la séquence d’entrée. Aucun comparateur n’étant spécifié dans le prototype, c’est le comparateur par défaut, EqualityComparer.Default, qui sera utilisé. Le deuxième prototype est semblable au premier, mais il permet de spécifier le comparateur à utiliser. Deuxième prototype public static Dictionary ToDictionary( this IEnumerable source, Func keySelector, IEqualityComparer comparer);
Vous utiliserez ce prototype si le comparateur par défaut, EqualityComparer.Default, ne convient pas. Dans ce cas, le comparateur IEqualityComparer sera utilisé pour tout ajout ou lecture d’élément dans le dictionnaire. La classe StringComparer implémente plusieurs classes de comparaison. L’une d’entre elles, par exemple, ignore la casse des chaînes comparées. Ainsi, les clés "Joe" et "joe" seront considérées comme égales. Le troisième prototype est semblable au premier, mais il ajoute un sélectionneur d’élément. Par son intermédiaire, les valeurs stockées dans le dictionnaire peuvent être d’un autre type que celles de la séquence d’entrée. Troisième prototype public static Dictionary ToDictionary( this IEnumerable source, Func keySelector, Func elementSelector);
L’argument elementSelector fait référence à un délégué qui retourne un fragment de l’élément soumis, ou un objet d’un tout autre type. C’est cet élément qui sera stocké dans le dictionnaire. Le quatrième prototype cumule les avantages des deux précédents. Par son intermédiaire, vous pouvez spécifier un elementSelector et un comparateur. Quatrième prototype public static Dictionary ToDictionary( this IEnumerable source, Func keySelector,
Linq.book Page 139 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
139
Func elementSelector, IEqualityComparer comparer);
Exceptions L’exception ArgumentNullExpression est levée si l’argument source, keySelector ou elementSelector, a pour valeur null ou si la clé retournée par keySelector a pour valeur null. L’exception ArgumentException est levée si un sélecteur retourne la même clé pour deux éléments. Exemples Dans cet exemple, nous utiliserons la classe commune Employee. Nous allons créer un dictionnaire de type Dictionary. La clé int représentera l’identifiant id de l’employé et l’objet Employee, l’élément stocké dans le dictionnaire. Listing 5.3 : Un exemple d’utilisation du premier prototype de l’opérateur ToDictionary. Dictionary eDictionary = Employee.GetEmployeesArray().ToDictionary(k => k.id); Employee e = eDictionary[2]; Console.WriteLine("Employé dont le champ id vaut 2 : {0} {1}", e.firstName, ➥e.lastName);
Le champ id est utilisé comme clé. Le premier argument de Dictionary est donc de type int. Ce prototype étant limité à l’enregistrement intégral des données qui lui sont passées, le deuxième argument est de type Employee. En fournissant l’identifiant d’un employé, le prototype Dictionary donne donc accès aux données correspondantes. Voici le résultat affiché dans la console : Employé dont le champ id vaut 2 : William Gates
Pour illustrer le deuxième prototype, nous avons besoin d’une situation dans laquelle l’utilisation d’un comparateur personnalisé se justifie. Supposons que la clé soit une valeur numérique stockée dans une chaîne. Les valeurs "1", "01", "001", etc. ne sont pas identiques, même si elles représentent le même nombre. Nous devons donc utiliser un comparateur qui autorise ce type de "largesse d’écriture". Nous allons légèrement modifier la classe commune Employee pour qu’elle admette une clé de type string. Cette modification va donner naissance à la classe Employee2. La classe utilisée par le deuxième prototype de l’opérateur ToDictionary public class Employee2 { public string id; public string firstName; public string lastName; public static ArrayList GetEmployeesArrayList()
Linq.book Page 140 Mercredi, 18. février 2009 7:58 07
140
LINQ to Objects
Partie II
{ ArrayList al = new ArrayList(); al.Add(new Employee2 { id = "1", firstName = "Joe", lastName = "Rattz" }); al.Add(new Employee2 { id = "2", firstName = "William", lastName = "Gates" }); al.Add(new Employee2 { id = "3", firstName = "Anders", lastName = "Hejlsberg" }); al.Add(new Employee2 { id = "4", firstName = "David", lastName = "Lightman" }); al.Add(new Employee2 { id = "101", firstName = "Kevin", lastName = "Flynn" }); return (al); } public static Employee2[] GetEmployeesArray() { return ((Employee2[])GetEmployeesArrayList().ToArray(typeof(Employee2))); } }
Le type de la clé a été modifié dans un but purement démonstratif, afin d’étayer le fonctionnement du comparateur MyStringifieldNumberComparer. Ce dernier considérera comme égales deux clés qui, littéralement, ne le sont pas. Voyons maintenant comment utiliser la classe Employee2 (voir Listing 5.4). Listing 5.4 : Un exemple d’utilisation du deuxième prototype de l’opérateur ToDictionary. Dictionary eDictionary = Employee2.GetEmployeesArray() .ToDictionary(k => k.id, new MyStringifiedNumberComparer()); Employee2 e = eDictionary["2"]; Console.WriteLine("Employé dont le champ id vaut \"2\" : {0} {1}", e.firstName, e.lastName); e = eDictionary["000002"]; Console.WriteLine("Employé dont le champ id vaut \"000002\" : {0} {1}", e.firstName, e.lastName);
Dans cet exemple, nous tentons d’accéder à l’élément du dictionnaire dont la clé a pour valeur "2", puis "000002". Si la classe de comparaison fonctionne, ces deux clés devraient pointer vers le même employé. Voici les résultats : Employé dont le champ id vaut "2" : William Gates Employé dont le champ id vaut "000002" : William Gates
Les deux clés ayant une même valeur numérique, elles renvoient vers la même entrée dans le dictionnaire. Le troisième prototype permet de stocker dans le dictionnaire un élément d’un autre type que celui de la séquence d’entrée. Pour illustrer son fonctionnement, nous allons travailler avec la classe Employee (voir Listing 5.5). Listing 5.5 : Un exemple d’utilisation du troisième prototype de l’opérateur ToDictionary. Dictionary eDictionary = Employee.GetEmployeesArray() .ToDictionary(k => k.id,
Linq.book Page 141 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
141
i => string.Format("{0} {1}", // elementSelector i.firstName, i.lastName)); string name = eDictionary[2]; Console.WriteLine("Employé dont le champ id vaut 2 : {0}", name);
Dans cet exemple, une expression lambda concatène les champs firstName et lastName et les stocke dans une chaîne. La séquence d’entrée est de type Employee, mais c’est un type string qui est stocké dans le dictionnaire. Voici le résultat : Employé dont le champ id vaut 2 : William Gates
Pour illustrer le quatrième prototype, nous allons utiliser la classe Employee2 et la classe commune MyStringfieldNumberComparer (voir Listing 5.6). Listing 5.6 : Un exemple d’utilisation du quatrième prototype de l’opérateur ToDictionary. Dictionary eDictionary = Employee2.GetEmployeesArray() .ToDictionary(k => k.id, // sélection de la clé i => string.Format("{0} {1}", // sélection de l’élément i.firstName, i.lastName), new MyStringifiedNumberComparer()); // comparateur string name = eDictionary["2"]; Console.WriteLine("Employé dont le champ id vaut \"2\" : {0}", name); name = eDictionary["000002"]; Console.WriteLine("Employé dont le champ id vaut \"000002\" : {0}", name);
Dans ce code : m
le sélecteur elementSelector stocke des valeurs chaînes dans le dictionnaire ;
m
le comparateur MyStringifiedNumberComparer est utilisé pour trouver un élément dans le dictionnaire.
Les deux derniers blocs recherchent l’employé dont l’identifiant vaut " 2", puis "000002". Les chaînes renvoyées sont identiques puisque le comparateur considère ces deux chaînes comme égales. Voici le résultat : Employé dont le champ id vaut 2 : William Gates Employé dont le champ id vaut 000002 : William Gates
Opérateur ToLookup Cet opérateur admet au minimum deux paramètres en entrée : une séquence d’entrée de type T et une clé de type K. Il crée un objet Lookup de type .
Si l’argument facultatif elementSelector est spécifié dans le prototype, l’objet Lookup créé est de type . Les valeurs stockées sont de type E, différent du type d’entrée T. Tous les prototypes de l’opérateur ToLookup créent un objet Lookup qui implémente l’interface ILookup. Nous leur ferons souvent référence en utilisant le simple mot "Lookup".
Linq.book Page 142 Mercredi, 18. février 2009 7:58 07
142
LINQ to Objects
Partie II
INFO Si la classe C# Lookup ne vous est pas familière, sachez qu’elle permet de mémoriser des couples élément/clé (où clé n’est pas forcément unique pour chaque élément). Pour retrouver le ou les éléments qui correspondent à une clé, il suffit d’indexer le tableau en utilisant cette clé.
Prototypes Quatre prototypes de l’opérateur ToLookup seront étudiés dans ce livre. Premier prototype public static ILookup ToLookup( this IEnumerable source, Func keySelector);
Ce prototype crée un Lookup de type en énumérant la séquence d’entrée, source. Le délégué keySelector est appelé pour extraire la valeur clé de chaque élément (c’est cette valeur qui sera inscrite dans le Lookup). Les éléments stockés dans le Lookup sont de même type que ceux de la séquence d’entrée. Aucun comparateur n’étant spécifié dans le prototype, c’est le comparateur par défaut, EqualityComparer.Default, qui sera utilisé. Le deuxième prototype est semblable au premier, mais il permet de spécifier le comparateur à utiliser. Deuxième prototype public static ILookup ToLookup( this IEnumerable source, Func keySelector, IEqualityComparer comparer);
Vous utiliserez ce prototype si le comparateur par défaut, EqualityComparer.Default, ne convient pas. Dans ce cas, le comparateur IEqualityComparer sera utilisé pour tout ajout ou lecture d’élément dans le Lookup. La classe StringComparer implémente plusieurs classes de comparaison. L’une d’entre elles, par exemple, ignore la casse des chaînes comparées. Ainsi, les clés "Joe" et "joe" seront considérées comme égales. Le troisième prototype est semblable au premier, mais il ajoute un sélectionneur d’élément. Par son intermédiaire, les valeurs stockées dans le dictionnaire peuvent être d’un autre type que celles de la séquence d’entrée. Troisième prototype public static ILookup ToLookup( this IEnumerable source, Func keySelector, Func elementSelector);
Linq.book Page 143 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
143
L’argument elementSelector fait référence à un délégué qui retourne un fragment de l’élément soumis, ou un objet d’un tout autre type. C’est cet élément qui sera stocké dans le dictionnaire. Le quatrième prototype cumule les avantages des deux précédents. Par son intermédiaire, vous pouvez spécifier un elementSelector et un comparateur. Quatrième prototype public static ILookup ToLookup( this IEnumerable source, Func keySelector, Func elementSelector, IEqualityComparer comparer);
Exceptions L’exception ArgumentNullExpression est levée si l’argument source, keySelector ou elementSelector, a pour valeur null ou si la clé retournée par keySelector a pour valeur null. L’exception ArgumentException est levée si un sélecteur retourne la même clé pour deux éléments. Exemples Pour illustrer le premier prototype de l’opérateur ToLookup, nous avons besoin d’une classe dont les éléments contiennent des membres qui peuvent être utilisés comme clés, mais qui ne sont pas forcément uniques. Nous utiliserons pour cela la classe Actor (voir Listing 5.7). Listing 5.7 : Un exemple d’appel du premier prototype de l’opérateur ToLookup. ILookup lookup = Actor.GetActors().ToLookup(k => k.birthYear); // Recherche d’un acteur né en 1964 IEnumerable actors = lookup[1964]; foreach (var actor in actors) Console.WriteLine("{0} {1}", actor.firstName, actor.lastName);
La première instruction crée un Lookup en utilisant le membre Actor.birthYear comme clé. La deuxième instruction indexe le Lookup en utilisant la clé. Il ne reste plus qu’à énumérer l’objet actors pour afficher le ou les résultats : Keanu Reeves Sandra Bullock
Pour illustrer le deuxième prototype, nous allons légèrement modifier la classe Actor. Son membre birthYear, initialement de type int, sera de type string dans la classe modifiée.
Linq.book Page 144 Mercredi, 18. février 2009 7:58 07
144
LINQ to Objects
Partie II
La classe utilisée par le deuxième prototype de l’opérateur ToLookup public class Actor2 { public string birthYear; public string firstName; public string lastName; public static Actor2[] GetActors() { Actor2[] actors = new Actor2[] { new Actor2 { birthYear = "1964", firstName = "Keanu", lastName = "Reeves" }, new Actor2 { birthYear = "1968", firstName = "Owen", lastName = "Wilson" }, new Actor2 { birthYear = "1960", firstName = "James", lastName = "Spader" }, // Une date exprimée sur 5 chiffres new Actor2 { birthYear = "01964", firstName = "Sandra", lastName = "Bullock" }, }; return(actors); } }
Le membre birthYear est maintenant une chaîne de caractères. Il ne reste plus qu’à appeler l’opérateur ToLookup (voir Listing 5.8). Listing 5.8 : Un exemple d’utilisation du deuxième prototype de l’opérateur ToLookup. ILookup lookup = Actor2.GetActors() .ToLookup(k => k.birthYear, new MyStringifiedNumberComparer()); // Recherche d’un acteur né en 1964 IEnumerable actors = lookup["0001964"]; foreach (var actor in actors) Console.WriteLine("{0} {1}", actor.firstName, actor.lastName);
La méthode de comparaison est la même que celle qui avait été utilisée pour illustrer l’opérateur Dictionary. En effet, l’éventuel ou les éventuels "0" en tête de clé n’étant pas significatifs, il est nécessaire de tester l’égalité "au sens large". Voici les résultats : Keanu Reeves Sandra Bullock
La recherche d’éléments dont la clé vaut "0001964" retourne les acteurs Keanu Reeves et Sandra Bullock, dont les clés respectives valent "1964" et "01964". L’objet de comparaison a donc bien fonctionné. Pour illustrer le troisième prototype, nous ferons appel à la classe Actor, qui avait déjà été utilisée dans l’exemple du premier prototype (voir Listing 5.9). Listing 5.9 : Un exemple d’utilisation du troisième prototype de l’opérateur ToLookup. ILookup lookup = Actor.GetActors() .ToLookup(k => k.birthYear, a => string.Format("{0} {1}", a.firstName, a.lastName)); // Recherche d’un acteur né en 1964 IEnumerable actors = lookup[1964];
Linq.book Page 145 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
145
foreach (var actor in actors) Console.WriteLine("{0}", actor);
Dans cet exemple, l’argument elementSelector est une expression lambda qui concatène les champs firstName et lastName. Voici le résultat : Keanu Reeves Sandra Bullock
En utilisant cette troisième variante de l’opérateur ToLookup, le type de données mémorisées dans l’objet Lookup (string) est différent de celui des éléments passés en entrée (Actor). Pour illustrer le quatrième prototype, nous allons utiliser la classe Actor2 et la classe commune MyStringfieldNumberComparer (voir Listing 5.10). Listing 5.10 : Un exemple d’appel du quatrième prototype de l’opérateur ToLookup. ILookup lookup = Actor2.GetActors() .ToLookup(k => k.birthYear, a => string.Format("{0} {1}", a.firstName, a.lastName), new MyStringifiedNumberComparer()); // Recherche d’un acteur né en 1964 IEnumerable actors = lookup["0001964"]; foreach (var actor in actors) Console.WriteLine("{0}", actor);
Voici le résultat : Keanu Reeves Sandra Bullock
Cet exemple recherche des éléments dont la clé vaut "0001964". Les acteurs Keanu Reeves et Sandra Bullock, dont les clés respectives valent "1964" et "01964", correspondent au critère. La comparaison a donc bien fonctionné. Par ailleurs, seules les chaînes nécessaires à la requête (firstName et lastName) sont stockées dans le Lookup. Opérateurs d’égalité Les opérateurs de cette catégorie sont utilisés pour tester l’égalité entre deux séquences. Opérateur SequenceEqual L’opérateur SequenceEqual détermine si deux séquences d’entrée sont égales.
Prototypes Deux prototypes de l’opérateur SequenceEqual seront étudiés dans ce livre. Premier prototype public static bool SequenceEqual( this IEnumerable first, IEnumerable second);
Linq.book Page 146 Mercredi, 18. février 2009 7:58 07
146
LINQ to Objects
Partie II
Cet opérateur énumère les deux séquences en parallèle et compare leurs éléments en utilisant la méthode System.Object.Equals. Si tous les éléments sont égaux et si les deux séquences ont le même nombre d’éléments, l’opérateur retourne la valeur true. Dans le cas contraire, il retourne la valeur false. Second prototype public static bool SequenceEqual( this IEnumerable first, IEnumerable second, IEqualityComparer comparer);
Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. Exemples Listing 5.11 : Un exemple d’utilisation du premier prototype de l’opérateur SequenceEqual. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool eq = presidents.SequenceEqual(presidents); Console.WriteLine(eq);
Voici le résultat : True
Ceci vous semble un peu trop simple ? Nous allons légèrement compliquer les choses dans le Listing 5.12. Listing 5.12 : Un autre exemple d’utilisation du premier prototype de l’opérateur SequenceEqual. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool eq = presidents.SequenceEqual(presidents.Take(presidents.Count())); Console.WriteLine(eq);
Linq.book Page 147 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
147
Si l’écriture vous semble plus complexe, un rapide examen va vous persuader du contraire. L’opérateur Take limite la comparaison à… tous les éléments du tableau presidents (presidents.Count()). Ce code est donc strictement équivalent au précédent et, bien entendu, il produit le même résultat : True
Nous allons maintenant comparer le tableau presidents avec ses presidents.Count() – 1 premiers éléments (voir Listing 5.13). Listing 5.13 : Un autre exemple d’utilisation du premier prototype de l’opérateur SequenceEqual. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool eq = presidents.SequenceEqual(presidents.Take(presidents.Count() - 1)); Console.WriteLine(eq);
Voici le résultat : False
Les deux séquences n’ayant pas le même nombre d’éléments, il est tout à fait normal que la valeur False soit retournée. Au chapitre précédent, lors de l’étude des opérateurs Take et Skip, il a été dit que, si ces opérateurs étaient utilisés correctement, ils permettaient de retrouver la séquence originale. Nous allons maintenant le prouver en leur adjoignant les opérateurs Concat et SequenceEqual (voir Listing 5.14). Listing 5.14 : Un exemple plus complexe du premier prototype de l’opérateur SequenceEqual. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool eq = presidents.SequenceEqual(presidents.Take(5).Concat(presidents.Skip(5))); Console.WriteLine(eq);
Dans cet exemple, Take(5) extrait les cinq premiers éléments de la séquence originale. Ces éléments sont alors concaténés à la séquence originale (Concat) en sautant les cinq
Linq.book Page 148 Mercredi, 18. février 2009 7:58 07
148
LINQ to Objects
Partie II
premiers éléments (Skip(5)). La séquence obtenue est comparée à la séquence originale (presidents.SequenceEqual()). Comme il se doit, la valeur True est retournée par l’opérateur SequenceEqual : True
Pour illustrer le second prototype, nous allons utiliser deux tableaux de string dont chaque élément est un nombre exprimé sous la forme d’une chaîne. Les éléments des deux tableaux seront définis de telle sorte qu’ils soient égaux, après conversion en entiers. Pour effectuer la comparaison, nous utiliserons la classe MyStringifieldNumberComparer (voir Listing 5.15). Listing 5.15 : Un exemple du second prototype de l’opérateur SequenceEqual. string[] stringifiedNums1 = { "001", "49", "017", "0080", "00027", "2" }; string[] stringifiedNums2 = { "1", "0049", "17", "080", "27", "02" }; bool eq = stringifiedNums1.SequenceEqual(stringifiedNums2, new MyStringifiedNumberComparer()); Console.WriteLine(eq);
En examinant rapidement les deux tableaux, vous pouvez voir que leurs éléments sont égaux, après conversion en entiers. Voici le résultat : True
Opérateurs agissant au niveau des éléments Cette catégorie d’opérateurs vous permet d’obtenir des éléments à partir de la séquence d’entrée. Opérateur First Selon le prototype utilisé, l’opérateur First retourne le premier élément de la séquence d’entrée ou de la séquence correspondant à un prédicat.
Prototypes Deux prototypes de l’opérateur First seront étudiés dans ce livre. Premier prototype public static T First( this IEnumerable source);
Ce prototype retourne le premier élément de la séquence d’entrée source.
Linq.book Page 149 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
149
Second prototype public static T First( this IEnumerable source, Func predicate);
Ce prototype retourne le premier élément de la séquence d’entrée pour lequel le prédicat vaut true. Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. L’exception InvalidOperationException est levée si le prédicat ne retourne la valeur true pour aucun des éléments de la séquence d’entrée. Exemples Listing 5.16 : Un exemple d’utilisation du premier prototype de l’opérateur First. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.First(); Console.WriteLine(name);
Voici le résultat : Adams
Vous vous demandez peut-être si les opérateurs First et Take(1) sont différents ? Eh bien, oui ! L’opérateur Take retourne une séquence d’éléments (y compris dans le cas où cette séquence ne contient qu’un seul élément). En revanche, l’opérateur First retourne un élément ou génère une exception si aucun élément ne peut être retourné. Listing 5.17 : Un exemple d’utilisation du deuxième prototype de l’opérateur First. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.First(p => p.StartsWith("H")); Console.WriteLine(name);
Linq.book Page 150 Mercredi, 18. février 2009 7:58 07
150
LINQ to Objects
Partie II
Ce code devrait retourner le premier élément de la séquence d’entrée qui commence par la lettre "H". Voici le résultat : Harding
Si aucun élément ne peut être renvoyé par l’opérateur First, une exception InvalidOperationException est levée. Pour éviter ce problème, utilisez l’opérateur FirstOrDefault. Opérateur FirstOrDefault L’opérateur FirstOrDefault est semblable à l’opérateur First, excepté en ce qui concerne son comportement lorsque aucun élément n’est trouvé.
Prototypes Deux prototypes de l’opérateur FirstOrDefault seront étudiés dans ce livre. Premier prototype public static T FirstOrDefault( this IEnumerable source);
Ce prototype retourne le premier élément de la séquence d’entrée source. Si la séquence d’entrée est vide, l’objet default(T) est retourné (la valeur par défaut des types référence et nullable est null). Second prototype public static T FirstOrDefault( this IEnumerable source, Func predicate);
Ce prototype retourne le premier élément de la séquence d’entrée pour lequel le prédicat vaut true. Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. Exemples Listing 5.18 : Appel du premier prototype de l’opérateur FirstOrDefault. L’élément recherché n’est pas trouvé. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};
Linq.book Page 151 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
151
string name = presidents.Take(0).FirstOrDefault(); Console.WriteLine(name == null ? "NULL" : name);
Voici le résultat : NULL
Listing 5.19 : Appel du premier prototype de l’opérateur FirstOrDefault. L’élément recherché est trouvé. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.FirstOrDefault(); Console.WriteLine(name == null ? "NULL" : name);
Voici le résultat : Adams
Pour illustrer le second prototype de l’opérateur FirstOrDefault, nous allons rechercher le premier élément qui commence par la lettre "B". Listing 5.20 : Appel du second prototype. L’élément recherché est trouvé. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.FirstOrDefault(p => p.StartsWith("B")); Console.WriteLine(name == null ? "NULL" : name);
Voici le résultat : Buchanan
Listing 5.21 : Appel du second prototype. L’élément recherché n’est pas trouvé. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.FirstOrDefault(p => p.StartsWith("Z")); Console.WriteLine(name == null ? "NULL" : name);
Linq.book Page 152 Mercredi, 18. février 2009 7:58 07
152
LINQ to Objects
Partie II
Aucune réponse n’étant trouvée, voici le résultat : NULL
Opérateur Last Selon le prototype utilisé, l’opérateur Last retourne le dernier élément de la séquence d’entrée ou de la séquence correspondant à un prédicat.
Prototypes Deux prototypes de l’opérateur Last seront étudiés dans ce livre. Premier prototype public static T Last( this IEnumerable source);
Ce prototype retourne le dernier élément de la séquence d’entrée source. Second prototype public static T Last( this IEnumerable source, Func predicate);
Ce prototype retourne le dernier élément de la séquence d’entrée pour lequel le prédicat vaut true. Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. L’exception InvalidOperationException est levée si le prédicat ne retourne la valeur true pour aucun des éléments de la séquence d’entrée. Exemples Listing 5.22 : Un exemple d’utilisation du premier prototype de l’opérateur Last. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.Last(); Console.WriteLine(name);
Voici le résultat : Wilson
Linq.book Page 153 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
153
Listing 5.23 : Un exemple d’utilisation du second prototype de l’opérateur Last. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.Last(p => p.StartsWith("H")); Console.WriteLine(name);
Ce code devrait retourner le dernier élément de la séquence d’entrée qui commence par la lettre "H". Voici le résultat : Hoover
Si aucun élément ne peut être renvoyé par l’opérateur Last, une exception InvalidOperationException est levée. Pour éviter ce problème, utilisez l’opérateur LastOrDefault. Opérateur LastOrDefault L’opérateur LastOrDefault est semblable à l’opérateur Last, excepté en ce qui concerne son comportement lorsque aucun élément n’est trouvé.
Prototypes Deux prototypes de l’opérateur LastOrDefault seront étudiés dans ce livre. Premier prototype public static T LastOrDefault( this IEnumerable source);
Ce prototype retourne le dernier élément de la séquence d’entrée source. Si la séquence d’entrée est vide, l’objet default(T) est retourné (la valeur par défaut des types référence et nullable est null). Second prototype public static T LastOrDefault( this IEnumerable source, Func predicate);
Ce prototype retourne le dernier élément de la séquence d’entrée pour lequel le prédicat vaut true. Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null.
Linq.book Page 154 Mercredi, 18. février 2009 7:58 07
154
LINQ to Objects
Partie II
Exemples Listing 5.24 : Appel du premier prototype de l’opérateur LastOrDefault. L’élément recherché n’est pas trouvé. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.Take(0).LastOrDefault(); Console.WriteLine(name == null ? "NULL" : name);
Voici le résultat : NULL
Listing 5.25 : Appel du premier prototype de l’opérateur LastOrDefault. L’élément recherché est trouvé. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.LastOrDefault(); Console.WriteLine(name == null ? "NULL" : name);
Voici le résultat : Wilson
Pour illustrer le second prototype de l’opérateur LastOrDefault, nous allons rechercher le dernier élément qui commence par la lettre "B". Listing 5.26 : Appel du second prototype. L’élément recherché est trouvé. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.LastOrDefault(p => p.StartsWith("B")); Console.WriteLine(name == null ? "NULL" : name);
Voici le résultat : Bush
Linq.book Page 155 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
155
Listing 5.27 : Appel du second prototype. L’élément recherché n’est pas trouvé. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.LastOrDefault(p => p.StartsWith("Z")); Console.WriteLine(name == null ? "NULL" : name);
Aucune réponse n’étant trouvée, voici le résultat : NULL
Opérateur Single Selon le prototype utilisé, l’opérateur Single retourne le seul élément de la séquence d’entrée, ou le seul élément de la séquence d’entrée correspondant à un prédicat.
Prototypes Deux prototypes de l’opérateur Single seront étudiés dans ce livre. Premier prototype public static T Single( this IEnumerable source);
Ce prototype énumère la séquence d’entrée source et renvoie l’unique élément trouvé. Second prototype public static T Single( this IEnumerable source, Func predicate);
Ce second prototype retourne l’unique élément pour lequel le prédicat a pour valeur true. L’exception InvalidOperationException est levée si le prédicat ne retourne la valeur true pour aucun ou pour plusieurs des éléments de la séquence d’entrée. Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. L’exception InvalidOperationException est levée si le prédicat ne retourne la valeur true pour aucun ou pour plusieurs des éléments de la séquence d’entrée, ou si la séquence d’entrée est vide. Exemples Listing 5.28 : Un exemple d’utilisation du premier prototype de l’opérateur Single sur la classe commune Employee. Employee emp = Employee.GetEmployeesArray() .Where(e => e.id == 3).Single(); Console.WriteLine("{0} {1}", emp.firstName, emp.lastName);
Linq.book Page 156 Mercredi, 18. février 2009 7:58 07
156
LINQ to Objects
Partie II
La requête retourne un seul et unique élément. Dans cet exemple, tout se passe bien, car un seul employé a un identifiant égal à 3 (Where(e => e.id == 3)). Voici le résultat : Anders Hejlsberg
Listing 5.29 : Un exemple d’appel du second prototype de l’opérateur Single. Employee emp = Employee.GetEmployeesArray() .Single(e => e.id == 3); Console.WriteLine("{0} {1}", emp.firstName, emp.lastName);
Ce code est équivalent au précédent. Mais, ici, au lieu d’invoquer l’opérateur Where pour s’assurer de l’unicité de la réponse, un prédicat est passé en argument de l’opérateur Single. Voici le résultat : Anders Hejlsberg
Opérateur SingleOrDefault L’opérateur SingleOrDefault est semblable à l’opérateur Single, excepté en ce qui concerne son comportement lorsque aucun élément n’est trouvé.
Prototypes Deux prototypes de l’opérateur SingleOrDefault seront étudiés dans ce livre. Premier prototype public static T SingleOrDefault( this IEnumerable source);
Ce prototype énumère la séquence d’entrée source et renvoie l’unique élément trouvé. Si la séquence est vide, l’objet default(T) est retourné (la valeur par défaut des types référence et nullable est null). Si plusieurs éléments sont trouvés, une exception InvalidOperationException est levée. Le second prototype de l’opérateur SingleOrDefault permet de spécifier un prédicat pour indiquer quel élément doit être retourné. Second prototype public static T SingleOrDefault( this IEnumerable source, Func predicate);
Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. L’exception InvalidOperationException est levée si le prédicat retourne la valeur true pour plusieurs des éléments de la séquence d’entrée.
Linq.book Page 157 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
157
Exemples Le Listing 5.30 illustre le fonctionnement du premier prototype dans le cas où aucun élément n’est trouvé dans la séquence d’entrée. Pour ce faire, nous utilisons l’opérateur Where en spécifiant une clé inexistante. Listing 5.30 : Un exemple d’utilisation du premier prototype de l’opérateur SingleOrDefault. Ici, aucun élément n’est trouvé. Employee emp = Employee.GetEmployeesArray() .Where(e => e.id == 5).SingleOrDefault(); Console.WriteLine(emp == null ? "NULL" : string.Format("{0} {1}", emp.firstName, emp.lastName));
Ce code effectue une requête sur un employé dont l’identifiant vaut 5, en sachant pertinemment qu’un tel identifiant n’existe pas. Une séquence vide est donc retournée. Voici le résultat : NULL
Le Listing 5.31 illustre le fonctionnement du premier prototype dans le cas où un élément est trouvé dans la séquence d’entrée. Pour ce faire, nous utilisons l’opérateur Where en spécifiant une clé existante et unique. Listing 5.31 : Un exemple d’utilisation du premier prototype de l’opérateur SingleOrDefault. Ici, un élément est trouvé. Employee emp = Employee.GetEmployeesArray() .Where(e => e.id == 4).SingleOrDefault(); Console.WriteLine(emp == null ? "NULL" : string.Format("{0} {1}", emp.firstName, emp.lastName));
L’identifiant spécifié dans l’opérateur Where existe et est unique. Voici le résultat : David Lightman
Pour illustrer le fonctionnement du second prototype, nous allons cette fois passer un prédicat à l’opérateur SingleOrDefault en choisissant un identifiant qui existe. Listing 5.32 : Un exemple d’utilisation du second prototype de l’opérateur SingleOrDefault. Ici, un élément est trouvé. Employee emp = Employee.GetEmployeesArray() .SingleOrDefault(e => e.id == 4); Console.WriteLine(emp == null ? "NULL" : string.Format("{0} {1}", emp.firstName, emp.lastName));
Linq.book Page 158 Mercredi, 18. février 2009 7:58 07
158
LINQ to Objects
Partie II
Ce code est équivalent au précédent. Mais, ici, au lieu d’invoquer l’opérateur Where pour filtrer les données, un prédicat est passé comme argument de l’opérateur SingleOrDefault. Voici le résultat : David Lightman
Nous allons maintenant essayer un prédicat qui ne trouve aucune correspondance dans les données (voir Listing 5.33). Listing 5.33 : Un exemple d’utilisation du second prototype de l’opérateur SingleOrDefault. Ici, aucun élément n’est trouvé. Employee emp = Employee.GetEmployeesArray() .SingleOrDefault(e => e.id == 5); Console.WriteLine(emp == null ? "NULL" : string.Format("{0} {1}", emp.firstName, emp.lastName));
Aucune réponse n’étant trouvée, voici le résultat (remarquez qu’aucune exception n’a été générée) : NULL
Opérateur ElementAt L’opérateur ElementAt retourne l’élément de la séquence d’entrée dont l’index est spécifié.
Prototype Un seul prototype de l’opérateur ElementAt sera étudié dans ce livre : public static T ElementAt( this IEnumerable source, int index);
Si la séquence implémente IList, l’interface IList est utilisée pour retrouver l’élément indexé. Dans le cas contraire, la séquence est énumérée jusqu’à ce que l’élément indexé soit atteint. Une exception ArgumentOutOfRangeExeption est levée si l’index est négatif ou supérieur au nombre d’éléments dans la séquence. INFO Dans le langage C#, le premier élément d’un index a pour valeur zéro et le dernier, le nombre d’éléments de la séquence moins un.
Exceptions L’exception ArgumentNullExpression est levée si l’argument source a pour valeur null. L’exception ArgumentOutOfRangeExeption est levée si l’index est négatif ou supérieur au nombre d’éléments dans la séquence.
Linq.book Page 159 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
159
Exemples Listing 5.34 : Exemple d’appel de l’opérateur ElementAt. Employee emp = Employee.GetEmployeesArray() .ElementAt(3); Console.WriteLine("{0} {1}", emp.firstName, emp.lastName);
L’élément de rang 3 (c’est-à-dire le quatrième élément) a été demandé. Voici le résultat : David Lightman
Opérateur ElementAtOrDefault L’opérateur ElementAtOrDefault retourne l’élément de la séquence d’entrée dont l’index est spécifié.
Prototype Un seul prototype de l’opérateur ElementAtOrDefault sera étudié dans ce livre : public static T ElementAtOrDefault( this IEnumerable source, int index);
Si la séquence implémente IList, l’interface IList est utilisée pour retrouver l’élément indexé. Dans le cas contraire, la séquence est énumérée jusqu’à ce que l’élément indexé soit atteint. Si l’index est négatif, supérieur ou égal au nombre d’éléments dans la séquence, l’objet default(T) est retourné (la valeur par défaut des types référence et nullable est null). Cette seule caractéristique différencie les opérateurs ElementAtOrDefault et ElementAt. INFO Dans le langage C#, le premier élément d’un index a pour valeur zéro et le dernier, le nombre d’éléments de la séquence moins un.
Exceptions L’exception ArgumentNullExpression est levée si l’argument source a pour valeur null. Exemples Listing 5.35 : Exemple d’appel de l’opérateur ElementAt avec un index valide. Employee emp = Employee.GetEmployeesArray() .ElementAtOrDefault(3); Console.WriteLine(emp == null ? "NULL" : string.Format("{0} {1}", emp.firstName, emp.lastName));
Linq.book Page 160 Mercredi, 18. février 2009 7:58 07
160
LINQ to Objects
Partie II
Voici le résultat : David Lightman
L’élément dont l’index vaut 3 est bien retourné par la requête. Nous allons maintenant transmettre un index invalide à cette même requête (voir Listing 5.36). Listing 5.36 : Exemple d’appel de l’opérateur ElementAt avec un index invalide. Employee emp = Employee.GetEmployeesArray() .ElementAtOrDefault(5); Console.WriteLine(emp == null ? "NULL" :
Étant donné que l’index 5 ne correspond à aucun élément, voici le résultat retourné : NULL
Quantificateurs Les quantificateurs permettent de tester l’existence d’une valeur dans une séquence d’entrée. Opérateur Any L’opérateur Any retourne la valeur true si au moins un élément de la séquence d’entrée vérifie une condition.
Prototypes Deux prototypes de l’opérateur Any seront étudiés dans ce livre. Premier prototype public static bool Any( this IEnumerable source);
Ce prototype retourne la valeur true si la séquence d’entrée contient au moins un élément. Second prototype public static bool Any( this IEnumerable source, Func predicate);
Le second prototype énumère la séquence d’entrée source. Il retourne la valeur true si le prédicat retourne la valeur true sur au moins un élément de la séquence. L’énumération stoppe dès que cette condition est atteinte. Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null.
Linq.book Page 161 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
161
Exemples Listing 5.37 : Exemple d’appel du premier prototype avec une séquence d’entrée vide. bool any = Enumerable.Empty().Any(); Console.WriteLine(any);
Voici le résultat : False
Listing 5.38 : Exemple d’appel du premier prototype avec une séquence d’entrée non vide. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool any = presidents.Any(); Console.WriteLine(any);
Voici le résultat : True
Listing 5.39 : Exemple d’appel du second prototype. Ici, aucun élément ne correspond au prédicat. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool any = presidents.Any(s => s.StartsWith("Z")); Console.WriteLine(any);
Le prédicat limite la requête aux éléments dont le nom commence par la lettre " Z". Comme aucun élément ne correspond, la valeur False est retournée : False
Listing 5.40 : Exemple d’appel du second prototype. Ici, au moins un élément correspond au prédicat. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool any = presidents.Any(s => s.StartsWith("A")); Console.WriteLine(any);
Linq.book Page 162 Mercredi, 18. février 2009 7:58 07
162
LINQ to Objects
Partie II
Au moins un élément du tableau presidents correspondant au prédicat, la valeur True est retournée : True
Opérateur All L’opérateur All retourne la valeur true si tous les éléments de la séquence d’entrée vérifient une condition.
Prototype Un seul prototype de l’opérateur All sera étudié dans ce livre : public static bool All( this IEnumerable source, Func predicate);
L’opérateur All énumère la séquence d’entrée. Il retourne la valeur true si le prédicat est vérifié sur tous les éléments de la séquence. Si le prédicat retourne la valeur false pour un élément, l’énumération cesse immédiatement. Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. Exemples Listing 5.41 : Exemple d’appel du prototype de l’opérateur All. Ici, le prédicat ne retourne pas la valeur True pour tous les éléments. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool all = presidents.All(s => s.Length > 5); Console.WriteLine(all);
Tous les éléments du tableau presidents n’ayant pas une longueur supérieure à 5 caractères, le prédicat n’est pas toujours vérifié. Le résultat est sans appel : False
Listing 5.42 : Exemple d’appel du prototype de l’opérateur All. Ici, le prédicat retourne la valeur True pour tous les éléments. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley",
Linq.book Page 163 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
163
"Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool all = presidents.All(s => s.Length > 3); Console.WriteLine(all);
Les noms des présidents comprenant un minimum de 4 caractères, le prédicat est vérifié pour tous les éléments du tableau. Voici le résultat : True
Opérateur Contains L’opérateur Contains retourne la valeur true si un des éléments de la séquence d’entrée vérifie la condition.
Prototypes Deux prototypes de l’opérateur All seront étudiés dans ce livre. Premier prototype public static bool Contains( this IEnumerable source, T value);
Dans un premier temps, ce prototype teste si la séquence d’entrée implémente l’interface ICollection. Dans l’affirmative, la méthode Contains de cette interface est appelée. Dans le cas contraire, la séquence d’entrée est énumérée pour voir si un de ses éléments vérifie la condition. Dès qu’une telle situation est atteinte, l’énumération prend fin. La valeur spécifiée est comparée aux éléments de la séquence d’entrée en utilisant la classe de comparaison par défaut : EqualityComparer.Default. Second prototype Le second prototype est en tout point comparable au premier, si ce n’est qu’il permet de spécifier un objet IEqualityComparer. Dans ce cas, c’est ce comparateur qui est utilisé pour comparer les éléments de la séquence d’entrée : public static bool Contains( this IEnumerable source, T value, IEqualityComparer comparer);
Exceptions L’exception ArgumentNullExpression est levée si l’argument source a pour valeur null. Exemples Listing 5.43 : Exemple d’appel du premier prototype. La valeur spécifiée ne se trouve pas dans la séquence. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland",
Linq.book Page 164 Mercredi, 18. février 2009 7:58 07
164
LINQ to Objects
Partie II
"Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool contains = presidents.Contains("Rattz"); Console.WriteLine(contains);
Aucun élément contenant la valeur "Rattz" dans le tableau. Le résultat est donc le suivant : False
Listing 5.44 : Exemple d’appel du premier prototype. La valeur spécifiée se trouve dans la séquence. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool contains = presidents.Contains("Hayes"); Console.WriteLine(contains);
Un des éléments du tableau contenant la valeur "Hayes", le résultat est le suivant : True
Pour illustrer le second prototype, nous allons utiliser la classe commune MyStringifieldNumberComparer (voir Listing 5.45). La requête recherchera un nombre stocké au format chaîne et précédé de plusieurs zéros. Le comparateur ne prenant pas en considération les zéros de tête, ce nombre sera retrouvé dans le tableau. Listing 5.45 : Exemple d’appel du second prototype. La valeur spécifiée est trouvée dans la séquence. string[] stringifiedNums = { "001", "49", "017", "0080", "00027", "2" }; bool contains = stringifiedNums.Contains("0000002", new MyStringifiedNumberComparer()); Console.WriteLine(contains);
Le comparateur convertit la chaîne recherchée en un nombre. Les zéros de tête disparaissent et la valeur est trouvée dans la séquence. La variable contains devrait donc avoir pour valeur true. Voici le résultat : True
Linq.book Page 165 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
165
Nous allons maintenant rechercher un élément inexistant dans la séquence d’entrée (voir Listing 5.46). Listing 5.46 : Exemple d’appel du second prototype. La valeur spécifiée n’est pas trouvée dans la séquence. string[] stringifiedNums = { "001", "49", "017", "0080", "00027", "2" }; bool contains = stringifiedNums.Contains("000271", new MyStringifiedNumberComparer()); Console.WriteLine(contains);
L’élément "000271" n’étant pas trouvé dans la séquence d’entrée, voici le résultat : False
Fonctions de comptage Les opérateurs de ce groupe effectuent des comptes (nombre d’éléments, somme, minimum, maximum) sur les éléments de la séquence d’entrée. Opérateur Count L’opérateur Count retourne le nombre d’éléments de la séquence d’entrée.
Prototypes Deux prototypes de l’opérateur Count seront étudiés dans ce livre. Premier prototype public static int Count( this IEnumerable source);
Ce prototype teste si la séquence d’entrée implémente l’interface ICollection. Dans l’affirmative, il obtient le nombre d’éléments de la séquence en utilisant la fonction de comptage de cette interface. Dans la négative, le nombre d’éléments est obtenu en énumérant la séquence d’entrée. Le second prototype renvoie le nombre d’éléments de la séquence d’entrée pour lesquels le prédicat retourne la valeur true. Second prototype public static int Count( this IEnumerable source, Func predicate);
Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null.
Linq.book Page 166 Mercredi, 18. février 2009 7:58 07
166
LINQ to Objects
Partie II
L’exception OverflowException est levée si le nombre d’éléments est supérieur à la valeur maximale autorisée par int.MaxValue. Exemples L’exemple du Listing 5.47 compte le nombre de présidents stockés dans la séquence d’entrée. Listing 5.47 : Exemple d’appel du premier prototype. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; int count = presidents.Count(); Console.WriteLine(count);
Voici le résultat : 37
L’exemple du Listing 5.48 compte le nombre de présidents stockés dans la séquence d’entrée dont le nom commence par la lettre "J". Listing 5.48 : Exemple d’appel du second prototype. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; int count = presidents.Count(s => s.StartsWith("J")); Console.WriteLine(count);
Voici le résultat : 3
Si le nombre d’éléments dépasse la capacité de int.MaxValue, vous utiliserez l’opérateur LongCount. Opérateur LongCount L’opérateur Count retourne le nombre d’éléments de la séquence d’entrée au format long.
Linq.book Page 167 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
167
Prototypes Deux prototypes de l’opérateur LongCount seront étudiés dans ce livre. Premier prototype public static long LongCount( this IEnumerable source);
Le premier prototype énumère la séquence d’entrée et retourne le nombre d’éléments comptés. Le second prototype renvoie le nombre d’éléments de la séquence d’entrée pour lesquels le prédicat retourne la valeur true. Second prototype public static long LongCount( this IEnumerable source, Func predicate);
Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. Exemples Dans l’exemple du Listing 5.49, nous utilisons un opérateur de requête standard pour générer une séquence pour laquelle l’opérateur Count produirait une exception de type OverflowException. Au chapitre précédent, nous avons étudié l’opérateur Range, qui permettait de créer une séquence en spécifiant son nombre d’éléments sous la forme d’un int. Nous allons concaténer deux de ces séquences pour dépasser les capacités du type int, et cela va donc nécessiter l’utilisation de l’opérateur LongCount. Listing 5.49 : Exemple d’appel du premier prototype. long count = Enumerable.Range(0, int.MaxValue). Concat(Enumerable.Range(0, int.MaxValue)).LongCount(); Console.WriteLine(count);
L’opérateur Range est appelé à deux reprises pour générer deux séquences contenant chacune le nombre maximal d’éléments du type int. Ces deux séquences sont alors concaténées à l’aide de l’opérateur Concat. ATTENTION L’exécution de cet exemple est assez longue. Sur ma machine, un Pentium 4 doté de 1 Go de RAM, il a fallu attendre deux minutes et demie !
Ne soyez pas surpris si cet exemple est très long à s’exécuter : il génère en effet deux séquences de 2 147 483 647 éléments !
Linq.book Page 168 Mercredi, 18. février 2009 7:58 07
168
LINQ to Objects
Partie II
Voici le résultat : 4294967294
Si vous essayez d’exécuter cet exemple en utilisant l’opérateur Count, une exception OverflowException sera levée. Pour illustrer le second prototype, nous reprendrons le même code que dans l’exemple précédent, mais nous limiterons l’énumération aux entiers supérieurs à 1 et inférieurs à 4. Seuls les éléments 2 et 3 seront donc sélectionnés. Étant donné que le code définit deux séquences, l’énumération devrait donc compter quatre éléments (voir Listing 5.50). Listing 5.50 : Exemple d’appel du second prototype. long count = Enumerable.Range(0, int.MaxValue). Concat(Enumerable.Range(0, int.MaxValue)).LongCount(n => n > 1 && n < 4); Console.WriteLine(count);
À l’exception du prédicat, ce code est très proche du précédent. Il est également très long à exécuter, et même plus long que celui de l’exemple précédent. Voici le résultat affiché dans la console : 4
Opérateur Sum L’opérateur Sum retourne la somme des valeurs numériques contenues dans les éléments de la séquence d’entrée.
Prototypes Deux prototypes de l’opérateur Sum seront étudiés dans ce livre. Premier prototype public static Numeric Sum( this IEnumerable source);
Numeric doit être choisi parmi les types suivants : int, long, double, décimal ou un de leurs équivalents nullables, int?, long?, double? ou decimal?.
Le premier prototype retourne la somme de tous les éléments de la séquence d’entrée source. Si la séquence d’entrée est vide, la valeur retournée est 0. Les valeurs null des types nullables ne sont pas incluses dans la somme. Le second prototype est semblable au premier, mais les valeurs additionnées sont sélectionnées par l’intermédiaire d’un délégué.
Linq.book Page 169 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
169
Second prototype public static Numeric Sum( this IEnumerable source, Func selector);
Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. Si la somme des éléments dépasse la capacité du type Numeric : m
une valeur –infini ou +infini est retournée si Numeric est de type decimal ou decimal? ;
m
une exception OverflowException est levée si Numeric est d’un autre type.
Exemples L’exemple du Listing 5.51 génère une séquence d’entiers avec l’opérateur Range et calcule leur somme en utilisant le premier prototype de l’opérateur Sum. Listing 5.51 : Exemple d’appel du premier prototype. IEnumerable ints = Enumerable.Range(1, 10); foreach (int i in ints) Console.WriteLine(i); Console.WriteLine("--"); int sum = ints.Sum(); Console.WriteLine(sum);
Voici les résultats : 1 2 3 4 5 6 7 8 9 10 -55
Le Listing 5.52 illustre le second prototype. Ici, le calcul porte sur la somme des options des employés de la classe commune EmployeeOptionEntry. Listing 5.52 : Exemple d’appel du second prototype. IEnumerable options = EmployeeOptionEntry.GetEmployeeOptionEntries(); long optionsSum = options.Sum(o => o.optionsCount); Console.WriteLine("Somme des options des employés : {0}", optionsSum);
Linq.book Page 170 Mercredi, 18. février 2009 7:58 07
170
LINQ to Objects
Partie II
Plutôt que calculer la somme de tous les membres des éléments, nous utilisons ici le sélecteur du second prototype pour limiter la somme au membre OptionsCount. Voici le résultat : Somme des options des employés : 51504
Opérateur Min L’opérateur Min retourne la plus petite valeur de la séquence d’entrée.
Prototypes Quatre prototypes de l’opérateur Min seront étudiés dans ce livre. Premier prototype public static Numeric Min( this IEnumerable source);
Numeric doit être choisi parmi les types suivants : int, long, double, décimal ou un de leurs équivalents nullables, int?, long?, double? ou decimal?.
Le premier prototype retourne la plus petite valeur de la séquence d’entrée. Si les éléments implémentent l’interface IComparable, cette interface est utilisée pour comparer les éléments. Dans le cas contraire, c’est l’interface non générique IComparable qui est utilisée. La valeur null est retournée si la séquence est vide ou uniquement composée de valeurs null. Le deuxième prototype de l’opérateur Min se comporte comme le premier, mais il s’applique aux types non numériques. Deuxième prototype public static T Min( this IEnumerable source);
Le troisième prototype est dédié aux types numériques. Il implémente une méthode de sélection qui permet de limiter la comparaison à un seul membre de chaque élément. Troisième prototype public static Numeric Min( this IEnumerable source, Func selector);
Le quatrième prototype est dédié aux types non numériques. Tout comme le précédent, il implémente une méthode de sélection qui permet de limiter la comparaison à un seul membre de chaque élément. Quatrième prototype public static S Min( this IEnumerable source, Func selector);
Linq.book Page 171 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
171
Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. Si le type T est nullable (int?, long?, double? ou decimal?), les premier et troisième prototypes retournent la valeur null si la source est vide. Si le type T n’est pas nullable (int, long, double ou decimal), les premier et troisième prototypes lèvent une exception InvalidOperationException si la séquence source est vide. Exemples Dans l’exemple du Listing 5.53, la plus petite valeur stockée dans un tableau d’entiers est retournée par le premier prototype de l’opérateur Min. Listing 5.53 : Exemple d’appel du premier prototype. int[] myInts = new int[] { 974, 2, 7, 1374, 27, 54 }; int minInt = myInts.Min(); Console.WriteLine(minInt);
Voici le résultat retourné : 2
Pour illustrer le deuxième prototype, nous appliquerons l’opérateur Min sur le tableau presidents. La valeur retournée sera la "plus petite", alphabétiquement parlant. Listing 5.54 : Exemple d’appel du deuxième prototype. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string minName = presidents.Min(); Console.WriteLine(minName);
Voici le résultat : Adams
Le résultat est le même que celui qui aurait été renvoyé par l’opérateur First. Mais ceci est un cas particulier : si les éléments du tableau presidents avaient été classés dans un autre ordre ou de façon aléatoire, le résultat de la fonction Min resterait "Adams". Pour illustrer le troisième prototype, nous rechercherons la date de naissance la plus ancienne dans la classe Actor (voir Listing 5.55).
Linq.book Page 172 Mercredi, 18. février 2009 7:58 07
172
LINQ to Objects
Partie II
Listing 5.55 : Exemple d’appel du troisième prototype. int oldestActorAge = Actor.GetActors().Min(a => a.birthYear); Console.WriteLine(oldestActorAge);
Voici le résultat : 1960
Pour illustrer le quatrième prototype, nous allons rechercher le "premier" nom d’acteur (alphabétiquement parlant) dans la classe Actor (voir Listing 5.56). Listing 5.56 : Exemple d’appel du quatrième prototype. string firstAlphabetically = Actor.GetActors().Min(a => a.lastName); Console.WriteLine(firstAlphabetically);
Voici le résultat : Bullock
Opérateur Max L’opérateur Max retourne la plus grande valeur de la séquence d’entrée.
Prototypes Quatre prototypes de l’opérateur Max seront étudiés dans ce livre. Premier prototype public static Numeric Max( this IEnumerable source);
Numeric doit être choisi parmi les types suivants : int, long, double, décimal ou un de leurs équivalents nullables, int?, long?, double? ou decimal?.
Le premier prototype retourne la plus grande valeur de la séquence d’entrée. Si les éléments implémentent l’interface IComparable, cette interface est utilisée pour comparer les éléments. Dans le cas contraire, c’est l’interface non générique IComparable qui est utilisée. La valeur null est retournée si la séquence est vide ou uniquement composée de valeurs null. Le deuxième prototype de l’opérateur Max se comporte comme le premier, mais il s’applique aux types non numériques. Deuxième prototype public static T Max( this IEnumerable source);
Le troisième prototype est dédié aux types numériques. Il implémente une méthode de sélection qui permet de limiter la comparaison à un seul membre de chaque élément.
Linq.book Page 173 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
173
Troisième prototype public static Numeric Max( this IEnumerable source, Func selector);
Le quatrième prototype est dédié aux types non numériques. Tout comme le précédent, il implémente une méthode de sélection qui permet de limiter la comparaison à un seul membre de chaque élément. Quatrième prototype public static S Max( this IEnumerable source, Func selector);
Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. Si le type T est nullable (int?, long?, double? ou decimal?), les premier et troisième prototypes retournent la valeur null si la source est vide. Si le type T n’est pas nullable (int, long, double ou decimal), les premier et troisième prototypes lèvent une exception InvalidOperationException si la séquence source est vide. Exemples Dans l’exemple du Listing 5.57, la plus grande valeur stockée dans un tableau d’entiers est retournée par le premier prototype de l’opérateur Max. Listing 5.57 : Exemple d’appel du premier prototype. int[] myInts = new int[] { 974, 2, 7, 1374, 27, 54 }; int minInt = myInts.Max(); Console.WriteLine(minInt);
Voici le résultat retourné : 1374
Pour illustrer le deuxième prototype (voir Listing 5.58), nous appliquerons l’opérateur Max sur le tableau presidents. La valeur retournée sera la "plus grande", alphabétiquement parlant. Listing 5.58 : Exemple d’appel du deuxième prototype. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string minName = presidents.Max(); Console.WriteLine(minName);
Linq.book Page 174 Mercredi, 18. février 2009 7:58 07
174
LINQ to Objects
Partie II
Voici le résultat : Wilson
Le résultat est le même que celui qui aurait été renvoyé par l’opérateur Last. Mais ceci est un cas particulier : si les éléments du tableau presidents avaient été classés dans un autre ordre ou de façon aléatoire, le résultat de la fonction Max resterait "Wilson". Pour illustrer le troisième prototype, nous rechercherons la date de naissance la plus récente dans la classe Actor (voir Listing 5.59). Listing 5.59 : Exemple d’appel du troisième prototype. int oldestActorAge = Actor.GetActors().Max(a => a.birthYear); Console.WriteLine(oldestActorAge);
Voici le résultat : 1968
Pour illustrer le quatrième prototype, nous allons rechercher le "dernier" nom d’acteur (alphabétiquement parlant) dans la classe Actor (voir Listing 5.60). Listing 5.60 : Exemple d’appel du quatrième prototype. string firstAlphabetically = Actor.GetActors().Max(a => a.lastName); Console.WriteLine(firstAlphabetically);
Voici le résultat : Wilson
Opérateur Average L’opérateur Average retourne la moyenne des valeurs numériques contenues dans la séquence d’entrée.
Prototypes Deux prototypes de l’opérateur Average seront étudiés dans ce livre. Premier prototype public static Result Average( this IEnumerable source);
Numeric doit être choisi parmi les types suivants : int, long, double, décimal ou un de leurs équivalents nullables, int?, long?, double? ou decimal?. Si Numeric est de type int ou long, Result sera de type double. Si Numeric est de type int? ou long?, Result sera de type double?. Dans tous les autres cas, Result sera du même type que Numeric.
Linq.book Page 175 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
175
Le premier prototype énumère la séquence d’entrée source et calcule la moyenne des éléments de type Numeric. Le second prototype énumère la séquence d’entrée source et calcule la moyenne des éléments de type Numeric désignés par la méthode selector. Second prototype public static Result Average( this IEnumerable source, Func selector);
Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. L’exception OverflowException est levée si la somme des valeurs dont on calcule la moyenne dépasse la capacité du type long lorsque Numeric a un type int, int?, long ou long?. Exemples Afin d’illustrer le premier prototype, nous allons utiliser l’opérateur Range pour créer une séquence d’entiers dont nous calculerons la moyenne (voir Listing 5.61). Listing 5.61 : Exemple d’appel du premier prototype. IEnumerable intSequence = Enumerable.Range(1, 10); Console.WriteLine("Séquence d’entiers :"); foreach (int i in intSequence) Console.WriteLine(i); double average = intSequence.Average(); Console.WriteLine("Moyenne : {0}", average);
Voici les résultats : Séquence d’entiers : 1 2 3 4 5 6 7 8 9 10 Moyenne : 5.5
Pour illustrer le second prototype, nous travaillerons avec la classe EmployeeOptionEntry (voir Listing 5.62).
Linq.book Page 176 Mercredi, 18. février 2009 7:58 07
176
LINQ to Objects
Partie II
Listing 5.62 : Exemple d’appel du second prototype. IEnumerable options = EmployeeOptionEntry.GetEmployeeOptionEntries(); Console.WriteLine("Identifiants et options des employés :"); foreach (EmployeeOptionEntry eo in options) Console.WriteLine("Identifiant employé : {0}, Options: {1}", eo.id, ➥eo.optionsCount); // Calcul de la moyenne des options double optionAverage = options.Average(o => o.optionsCount); Console.WriteLine("La moyenne des options des employés est : {0}", optionAverage);
Dans un premier temps, l’objet options est défini et initialisé avec la méthode GetOptionEntries(). Les identifiants et options des employés sont ensuite affichés à l’aide d’une boucle foreach. Enfin, la moyenne des options est calculée avec le second prototype de l’opérateur Average, en ne travaillant que sur le membre optionsCount des éléments. Voici les résultats : Identifiants et options des employés : Identifiant employé : 1, Options : 2 Identifiant employé : 2, Options : 10000 Identifiant employé : 2, Options : 10000 Identifiant employé : 3, Options : 5000 Identifiant employé : 2, Options : 10000 Identifiant employé : 3, Options : 7500 Identifiant employé : 3, Options : 7500 Identifiant employé : 4, Options : 1500 Identifiant employé : 101, Options : 2 La moyenne des options des employés est : 5722.66666666667
Opérateur Aggregate L’opérateur Aggregate exécute une fonction spécifiée par l’utilisateur sur chacun des éléments de la séquence d’entrée. Il passe la valeur retournée par la fonction au rang précédent et retourne la valeur calculée pour le dernier élément.
Prototypes Deux prototypes de l’opérateur Average seront étudiés dans ce livre. Premier prototype public static T Aggregate( this IEnumerable source, Func func);
Dans cette version du prototype, l’opérateur Aggregate énumère les éléments de la séquence d’entrée source. Le délégué func est appelé sur chaque élément. Deux arguments lui sont passés : la valeur retournée par la fonction à l’élément précédent et l’élément lui-même. La valeur retournée par func est mémorisée dans une mémoire interne, afin d’être passée au prochain élément. C’est le premier élément qui est passé lors de la première invocation de la méthode func. Le second prototype est identique au premier mais, ici, la valeur à passer lors de la première invocation de la méthode func est spécifiée.
Linq.book Page 177 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
177
Second prototype public static U Aggregate( this IEnumerable source, U seed, Func func);
Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. L’exception InvalidOperationException est levée dans le premier prototype si la séquence d’entrée est vide. Exemples Le Listing 5.63 illustre le premier prototype. Ici, nous calculons la valeur 5! (factorielle 5). Ce résultat est obtenu en multipliant entre eux tous les entiers positifs inférieurs ou égaux à 5. La valeur 5! est donc égale à 1 × 2 × 3 × 4 × 5. Listing 5.63 : Exemple d’appel du premier prototype. int N = 5; IEnumerable intSequence = Enumerable.Range(1, N); // Liste des éléments de la séquence foreach (int item in intSequence) Console.WriteLine(item); // Calcul et affichage de la factorielle // av == valeur de l’agrégat, e == élément int agg = intSequence.Aggregate((av, e) => av * e); Console.WriteLine("{0}! = {1}", N, agg);
Ce code génère une séquence d’entiers compris entre 1 et 5 en utilisant l’opérateur Range. Après avoir affiché ces éléments, l’opérateur Aggregate est appelé, en lui fournissant une expression lambda qui multiplie l’agrégat par l’élément. Voici les résultats : 1 2 3 4 5 5! = 120
ATTENTION Lorsque vous utilisez le premier prototype de l’opérateur Aggregate, vous devez faire attention à ce que le premier élément ne soit pas traité à deux reprises par la méthode func. Dans l’exemple précédent, les paramètres 1 et 1 sont transmis en entrée de la méthode func. Cela n’affecte en rien le résultat final, puisque les valeurs sont multipliées entre elles. Le résultat aurait en revanche été faussé si les valeurs avaient été additionnées.
Linq.book Page 178 Mercredi, 18. février 2009 7:58 07
178
LINQ to Objects
Partie II
Pour illustrer le second prototype, nous allons utiliser un opérateur Sum "fait maison" (voir Listing 5.64). Listing 5.64 : Exemple d’appel du second prototype. IEnumerable intSequence = Enumerable.Range(1, 10); // Affichage des éléments de la séquence foreach (int item in intSequence) Console.WriteLine(item); Console.WriteLine("--"); // Calcul et affichage de la somme int sum = intSequence.Aggregate(0, (s, i) => s + i); Console.WriteLine(sum);
La valeur "0" a été définie comme premier argument de l’opérateur Aggregate afin que le premier appel de la méthode func n’altère pas le résultat final. Voici les résultats affichés dans la console : 1 2 3 4 5 6 7 8 9 10 -55
Comme vous pouvez le voir, le résultat est le même que celui obtenu pour illustrer l’opérateur Sum, dans le Listing 5.51.
Résumé Ce chapitre et le précédent vous semblent peut-être quelque peu indigestes. Ils contiennent cependant les bases de LINQ. J’espère avoir couvert tous les opérateurs qui vous seront utiles. Pour que LINQ révèle toute sa puissance, vous devez bien comprendre ces opérateurs et savoir comment les utiliser. Il n’est pas nécessaire de retenir le détail de chaque opérateur. Sachez juste qu’ils existent et quels services ils peuvent vous rendre. En se fondant sur ce qui a été vu jusqu’ici à propos de LINQ to Objects et des opérateurs de requête standard, vous avez pu voir à quel point LINQ s’est révélé puissant et pratique pour interroger des données de tout type stockées dans des collections en mémoire. En utilisant les quelque 50 opérateurs de LINQ to Objects, vos requêtes seront plus cohérentes, plus fiables et plus rapides à écrire.
Linq.book Page 179 Mercredi, 18. février 2009 7:58 07
Chapitre 5
Les opérateurs non différés
179
Je n’insisterai jamais assez sur le fait que la plupart des opérateurs de requête standard travaillent sur des collections qui implémentent l’interface IEnumerable. Aucune des collections C# héritées (celles de l’espace de noms System.Collection) n’implémentent cette interface ; elles sont donc exclues. Je sais pourtant que certains lecteurs essayeront (sans succès !) d’appliquer des requêtes LINQ à des ArrayList provenant de code hérité. Si vous vous trouvez dans une telle situation, jetez un œil aux opérateurs Cast et OfType. Au chapitre suivant, nous allons nous intéresser à la génération et à l’interrogation de séquences XML. Cette partie de LINQ a pour nom "LINQ to XML".
Linq.book Page 180 Mercredi, 18. février 2009 7:58 07
Linq.book Page 181 Mercredi, 18. février 2009 7:58 07
III LINQ to XML
Linq.book Page 182 Mercredi, 18. février 2009 7:58 07
Linq.book Page 183 Mercredi, 18. février 2009 7:58 07
6 Introduction à LINQ to XML Ce chapitre aborde la facette LINQ to XML du langage LINQ. En préambule, le Listing 6.1 montre comment créer une hiérarchie XML en utilisant l’API Microsoft DOM (Document Object Model) W3C DOM XML. Il n’est pas nécessaire d’aller bien loin dans le code pour se rendre compte à quel point le processus est douloureux ! Listing 6.1 : Un exemple XML basique. using System.Xml; // Déclaration de variables XmlElement xmlBookParticipant; XmlAttribute xmlParticipantType; XmlElement xmlFirstName; XmlElement xmlLastName; // Instanciation d’un objet XmlDocument XmlDocument xmlDoc = new XmlDocument(); // Création de l’élément parent et ajout au document XmlElement xmlBookParticipants = xmlDoc.CreateElement("BookParticipants"); xmlDoc.AppendChild(xmlBookParticipants); // Création d’un participant et ajout à la liste des participants xmlBookParticipant = xmlDoc.CreateElement("BookParticipant"); xmlParticipantType = xmlDoc.CreateAttribute("type"); xmlParticipantType.InnerText = "Author"; xmlBookParticipant.Attributes.Append(xmlParticipantType); xmlFirstName = xmlDoc.CreateElement("FirstName"); xmlFirstName.InnerText = "Joe"; xmlBookParticipant.AppendChild(xmlFirstName); xmlLastName = xmlDoc.CreateElement("LastName"); xmlLastName.InnerText = "Rattz"; xmlBookParticipant.AppendChild(xmlLastName); xmlBookParticipants.AppendChild(xmlBookParticipant); // Création d’un participant autre et ajout à la liste des participants xmlBookParticipant = xmlDoc.CreateElement("BookParticipant"); xmlParticipantType = xmlDoc.CreateAttribute("type"); xmlParticipantType.InnerText = "Editor";
Linq.book Page 184 Mercredi, 18. février 2009 7:58 07
184
LINQ to XML
Partie III
xmlBookParticipant.Attributes.Append(xmlParticipantType); xmlFirstName = xmlDoc.CreateElement("FirstName"); xmlFirstName.InnerText = "Ewan"; xmlBookParticipant.AppendChild(xmlFirstName); xmlLastName = xmlDoc.CreateElement("LastName"); xmlLastName.InnerText = "Buckingham"; xmlBookParticipant.AppendChild(xmlLastName); xmlBookParticipants.AppendChild(xmlBookParticipant); // Recherche des auteurs et affichage de leur nom XmlNodeList authorsList = xmlDoc.SelectNodes("BookParticipants/BookParticipant[@type=\"Author\"]"); foreach (XmlNode node in authorsList) { XmlNode firstName = node.SelectSingleNode("FirstName"); XmlNode lastName = node.SelectSingleNode("LastName"); Console.WriteLine("{0} {1}", firstName, lastName); }
Ce code construit la hiérarchie XML et affiche le nom de chaque participant. La structure XML désirée Joe Rattz Ewan Buckingham
L’écriture, la compréhension et la maintenance de ce code sont un vrai cauchemar ! Par ailleurs, il ne suffit pas d’observer son contenu pour en déduire la structure XML générée. Si la méthode DOM est si lourde, c’est en partie parce qu’il n’est pas possible de créer un élément, de l’initialiser et de l’attacher à la hiérarchie en utilisant une seule et même instruction. Au lieu de cela, trois étapes sont nécessaires : chaque élément doit être créé, son membre InnerText, initialisé à la valeur souhaitée puis l’élément ajouté à un nœud de l’arborescence. Cette technique génère beaucoup de code. Sans compter qu’il faut également créer un document XML : sans lui, impossible de créer un simple élément ! Observez le listing et son résultat. Ne trouvez-vous pas la quantité de code disproportionnée ? Appuyez sur Ctrl+F5 pour exécuter ce programme. Voici le résultat affiché dans la console : System.Xml.XmlElement System.Xml.XmlElement
Les noms et prénoms des participants n’apparaissent pas. Nous allons tenter de modifier la ligne Console.WriteLine pour obtenir les données souhaitées : Console.WriteLine("{0} {1}", firstName.ToString(), lastName.ToString());
Linq.book Page 185 Mercredi, 18. février 2009 7:58 07
Chapitre 6
Introduction à LINQ to XML
185
Un nouveau Ctrl+F5 produit… le même résultat : System.Xml.XmlElement System.Xml.XmlElement
La tentative a échoué !
Introduction Microsoft aurait pu se contenter de fournir une API LINQ de requêtage XML. Heureusement, les développeurs sont allés beaucoup plus loin. Après plusieurs années d’utilisation de l’API W3C DOM XML, il est apparu clairement qu’une amélioration s’imposait. Plutôt qu’utiliser l’artillerie DOM, n’avez-vous jamais créé directement des éléments XML en passant par des chaînes ? Ou été tenté de le faire ? Plusieurs déficiences de l’API W3C DOM XML ont été examinées et un nouveau modèle d’objet a été créé. Il en a résulté une méthode bien plus simple et élégante pour créer des arbres XML : la "construction fonctionnelle". Et, croyez-moi, cette technique vaut son pesant d’or ! Bien entendu, la nouvelle API se devait de supporter les requêtes LINQ, sans quoi elle n’aurait pas pu faire partie du langage LINQ. Par l’intermédiaire de méthodes d’extension, des opérateurs de requêtes spécifiques XML ont ainsi été implémentés. En combinant ces opérateurs et les opérateurs de requête standard de LINQ to Objects (voir Chapitre 2), vous aurez à votre disposition tout ce qu’il faut pour manipuler des données XML de façon élégante et efficace.
Se passer de l’API W3C DOM XML Nous allons raisonner sur un cas pratique : le projet sur lequel j’ai personnellement travaillé dans la division IT d’une grande entreprise. J’ai dû mettre au point une classe permettant de pister toutes les actions des utilisateurs dans une application ASP.NET. C’est ainsi qu’est née la classe logging. Cette classe avait deux buts : identifier tout utilisateur qui abuserait du système et être prévenu par e-mail si une exception avait été levée. Ce second point se justifiait par le fait que les utilisateurs qui avaient provoqué une exception n’étaient jamais en mesure de m’indiquer clairement dans quelles conditions s’était produit l’incident. Je voulais donc un procédé capable de traquer les moindres mouvements des utilisateurs côté serveur. Toutes les actions entreprises par l’utilisateur (une demande de facture ou la soumission d’une commande, par exemple) devaient être considérées comme un événement. Chaque événement était mémorisé dans les champs d’une base de données : références de l’utilisateur, date, heure, type de l’événement, etc. Malheureusement, ces informations ne me permettaient pas de connaître le détail de chaque
Linq.book Page 186 Mercredi, 18. février 2009 7:58 07
186
LINQ to XML
Partie III
action. Par exemple, pour une commande, j’aurais voulu connaître son numéro et les différents articles commandés. En fait, j’avais besoin de toutes les informations qui me permettraient de réitérer la situation qui avait déclenché une exception. Chaque événement manipulait des données différentes, mais je ne voulais pas que ces données soient stockées dans des tableaux différents. La solution XML s’imposait d’elle-même. Par exemple, pour une demande de facture, les données XML pouvaient avoir l’allure suivante : 10/2/2006 10/9/2006 False
Et, pour une commande : 4754611903 12 Atlanta USPS First Class
Étant donné que les données étaient liées au type des événements, il était impossible de les valider. L’utilisation de l’API XML DOM était donc avantageuse. Ce gestionnaire d’événements est devenu un outil très utile. Il a permis d’identifier et de résoudre plus facilement les bugs. Il est assez amusant d’appeler un client et de l’informer que l’erreur survenue sur la commande 32728 qu’il a passée la veille est désormais réparée. Le trouble qui résulte lorsque le client prend conscience qu’il est possible de connaître le détail de ses actions est une vraie récompense en soi. Si vous connaissez déjà le XML, vous avez certainement remarqué que ces données n’ont aucun nœud parent. Cela constitue un problème si vous utilisez l’API W3C DOM. Mais, dans mon cas, j’ai utilisé l’API String.Format XML, qui vous est peut-être également familière. Voici le code utilisé : string xmlData = string.Format( "{0}{1}{2}", Date.ToShortDateString(), endDate.ToShortDateString(), includePaid.ToString());
Je sais que ce n’est pas la meilleure des façons de définir des données XML et qu’il est facile de se tromper dans son écriture. Pour faciliter la saisie, j’ai donc créé une méthode à laquelle je passe en paramètres une liste d’éléments et les données correspondantes : string xmlData = XMLHelper( "StartDate", startDate.ToShortDateString(), "EndDate", endDate.ToShortDateString(), "IncPaid", includePaid.ToString());
La méthode XMLHelper crée également un nœud parent. Les améliorations ne sont pas flagrantes. Comme vous pouvez le voir, je n’ai rien fait pour encoder mes données dans cet appel.
Linq.book Page 187 Mercredi, 18. février 2009 7:58 07
Chapitre 6
Introduction à LINQ to XML
187
Bien que l’utilisation de la méthode String.Format (ou une autre technique externe à l’API XML DOM) ne soit pas une très bonne alternative, DOM se révèle trop complexe lorsqu’il s’agit de manipuler quelques lignes de XML. Si vous pensez que mon approche est un peu trop personnelle, sachez que, récemment, lors d’un séminaire Microsoft, l’intervenant a présenté un code qui construisait une structure XML… en concaténant plusieurs chaînes !
Résumé La plupart des développeurs associent LINQ au requêtage de données, et en particulier de données provenant de bases de données. En tournant les pages de cet ouvrage, vous verrez que LINQ to XML apporte également une vraie réponse quant à la manipulation et à l’interrogation de données XML. Dans ce chapitre, je vous ai montré à quel point il était douloureux de manipuler du XML via l’API W3C DOM XML. Vous avez également vu qu’il était possible de se passer de cette API. Au chapitre suivant, nous nous intéresserons à l’API LINQ to XML. Par son intermédiaire, vous apprendrez à créer des hiérarchies XML en quelques lignes. À titre indicatif, si la hiérarchie créée dans le Listing 6.1 demandait 29 lignes de code, elle sera réduite à 10 lignes seulement en passant par LINQ to XML. Après avoir lu les deux prochains chapitres, vous serez certainement convaincu de l’avancée révolutionnaire de LINQ, tant au niveau de la manipulation du XML qu’à celui de l’interrogation des bases de données.
Linq.book Page 188 Mercredi, 18. février 2009 7:58 07
Linq.book Page 189 Mercredi, 18. février 2009 7:58 07
7 L’API LINQ to XML Au chapitre précédent, vous avez vu à quel point il était difficile de créer un document XML en utilisant l’API W3C DOM XML. Vous avez également appris à vous passer de cette API pour alléger le code. En outre, vous avez pu constater que LINQ sait faire autre chose qu’interroger des collections : il peut également manipuler des hiérarchies XML, à travers l’API LINQ to XML. Dans ce chapitre, vous allez découvrir comment utiliser LINQ to XML pour créer, parcourir, manipuler et interroger des documents XML, et effectuer des recherches dans des objets XML. Pour illustrer ce chapitre, nous utiliserons une application console. Afin de pouvoir tirer parti de LINQ to XML, vous devez y ajouter une référence vers l’assembly System.Xml.Linq, si celle-ci n’est pas déjà présente.
Espaces de noms référencés Les exemples de ce chapitre vont utiliser les espaces de noms System.Linq, System.Xml.Linq et System.Collections.Generic. Si elles ne sont pas déjà présentes, vous devez donc ajouter les directives using suivantes dans votre code : using System.Linq; using System.Xml.Linq; using System.Collections.Generic;
Si vous parcourez le code source (www.pearson.fr), vous verrez qu’une directive using a également été ajoutée sur l’espace de noms System.Diagnostic. Cette directive n’est pas nécessaire si vous saisissez directement les exemples de ce chapitre. Elle n’est là que pour les besoins propres au code source.
Linq.book Page 190 Mercredi, 18. février 2009 7:58 07
190
LINQ to XML
Partie III
Améliorations de l’API Après avoir expérimenté l’API Microsoft W3C XML DOM pendant plusieurs années, des points négatifs et des faiblesses se sont peu à peu dessinés. Pour y remédier, les points suivants ont été examinés par les équipes de développement de Microsoft : m
construction d’arbres XML ;
m
solutions "centrées-document" ;
m
espaces de noms et préfixes ;
m
extraction de valeurs de nœuds.
Non contents de grossir et parfois d’obscurcir le code, ces points sont une véritable gêne lorsque l’on travaille avec des données XML. Il était donc important de les examiner de près afin que LINQ to XML fonctionne d’une manière irréprochable. Un exemple : supposons que vous vouliez utiliser une projection, afin qu’une requête LINQ retourne du code XML. L’API XML existante ne permettant pas d’instancier un nouvel élément avec une déclaration new, il fallait corriger cette limitation pour que LINQ to XML manipule des données XML aussi simplement que possible. Dans les pages suivantes, nous allons passer en revue chacune de ces problématiques et voir comment LINQ to XML les solutionne. La construction fonctionnelle simplifie la création d’arbres XML Si vous vous reportez au Listing 6.1, au chapitre précédent, vous verrez qu’il est très difficile d’en tirer un schéma XML. Vous constaterez également que le code est très "verbeux". Après avoir instancié un nouveau document XML, plusieurs nœuds doivent être définis. À titre d’exemple, pour ajouter un élément il est nécessaire de le définir, de l’initialiser et de le lier avec un élément parent. Ces étapes doivent être répétées autant de fois que nécessaire pour définir toute la structure XML. Un tel procédé rend difficilement perceptible le schéma XML et fait exagérément grossir le code. Cette API n’est malheureusement pas capable de créer un élément (ou un autre type de nœud) en le positionnant dans l’arbre XML et de l’initialiser par la même occasion. Cette technique est toujours utilisable dans l’API LINQ to XML, mais une autre, bien plus efficace, connue sous le nom de "construction fonctionnelle", a fait son apparition. Cette technique permet de définir le schéma XML pendant les phases de construction et d’initialisation des objets XML. Pour ce faire, la nouvelle API fournit des constructeurs d’objets XML qui acceptent un ou plusieurs objets, accompagnés de leurs valeurs. Le type de l’objet ou des objets étant spécifié, il détermine leur point d’appartenance. Voici le modèle général : XMLOBJECT o = new XMLOBJECT(OBJECTNAME, XMLOBJECT1, XMLOBJECT2, ... XMLOBJECTN);
Linq.book Page 191 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
191
INFO Le code précédent n’a qu’un objet purement démonstratif. Aucune des classes référencées dans les arguments n’existe réellement. Elles ne sont là que pour matérialiser des classes XML purement abstraites.
Si vous utilisez la classe LINQ to XML XAttribute pour ajouter un attribut XML à un élément de type XElement, l’attribut devient un attribut de l’élément. Par exemple, dans le code précédent, si l’attribut XMLOBJECT1 est ajouté à l’élément XMLOBJECT o, si o est un XElement et XMLOBJECT1, un XAttribute, XMLOBJECT1 devient un attribut du XElement o. Si vous ajoutez un XElement à un XElement, l’élément ajouté devient un enfant de l’élément auquel il est ajouté. Par exemple, si XMLOBJECT1 et o sont deux éléments, XMLOBJECT1 devient l’enfant de l’élément o. Lorsqu’un XMLOBJECT est instancié, son contenu peut être défini par un ou plusieurs XMLOBJECT. Comme vous le verrez un peu plus loin, dans la section "Création de textes avec XText", il est également possible de spécifier son contenu en ajoutant une chaîne. Cette dernière sera automatiquement convertie en un XMLOBJECT. Le Listing 7.1 donne un exemple de création d’un schéma XML. Listing 7.1 : Utilisation de la construction fonctionnelle pour définir un schéma XML. XElement xBookParticipant = new XElement("BookParticipant", new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")); Console.WriteLine(xBookParticipant.ToString());
Deux objets XElement ont été passés lors de la construction de l’élément BookParticipant. Chacun d’eux est donc un enfant de BookParticipant. Notez également que, lors de la construction des XElement FirstName et LastName, une valeur texte (et non deux objets enfants) a été passée. Voici les résultats de ce code : Joe Rattz
Le schéma XML apparaît clairement dans le code. Remarquez également à quel point le code est concis. Le Listing 7.2 représente le code LINQ to XML équivalent au Listing 6.1. Listing 7.2 : Définition de l’arbre du Listing 6.1, avec un code bien moins important. XElement xBookParticipants = new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"),
Linq.book Page 192 Mercredi, 18. février 2009 7:58 07
192
LINQ to XML
Partie III
new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))); Console.WriteLine(xBookParticipants.ToString());
Ce code est bien plus concis et facile à maintenir que celui du Listing 6.1. Par ailleurs, la structure des données peut être facilement devinée par simple lecture du code. Voici le résultat : Joe Rattz Ewan Buckingham
La nouvelle API a également un autre avantage : les données créées sont formatées comme un arbre XML traditionnel. Il en va tout autrement de l’arbre créé par le code du Listing 6.1 : Joe…
Au chapitre suivant, quand nous nous intéresserons aux requêtes LINQ qui produisent des sorties XML, vous verrez à quel point la construction fonctionnelle est importante. L’élément, point central d’un objet XML Avec l’API W3C DOM, il était impossible de définir un élément XML XmlElement sans le rattacher à un document XML XmlDocument. Si vous essayez d’instancier un XmlElement avec cette instruction : XmlElement xmlBookParticipant = new XmlElement("BookParticipant");
vous obtenez l’erreur de compilation ci-après : ’System.Xml.XmlElement’ ne contient pas un constructeur qui accepte des arguments ’1’
Avec l’API W3C DOM, la seule façon de créer un XmlElement consiste à appeler la méthode CreateElement d’un objet XmlDocument : XmlDocument xmlDoc = new XmlDocument(); XmlElement xmlBookParticipant = xmlDoc.CreateElement("BookParticipant");
Ce code fonctionne à la perfection, mais il n’est pas toujours pratique de devoir créer un document XML avant de pouvoir définir un élément XML. La nouvelle API LINQ to XML permet d’instancier un élément sans le rattacher nécessairement à un document XML.
Linq.book Page 193 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
193
XElement xeBookParticipant = new XElement("BookParticipant");
Les éléments XML ne sont pas les seuls nœuds affectés par cette restriction de l’API X3C DOM. Les attributs, les commentaires, les sections CData, les instructions de calcul et les références d’entités doivent tous être rattachés à un document XML. Avec LINQ to XML, tous ces objets pourront être instanciés à la volée, sans qu’un document XML n’ait été défini au préalable. Bien entendu, rien ne vous empêche de créer un document XML avec la nouvelle API. À titre d’exemple, le Listing 7.3 crée un document XML, y ajoute l’élément BookParticipants et insère un élément BookParticipant dans ce dernier. Listing 7.3 : Création d’un document XML et de sa structure avec l’API LINQ to XML. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(xDocument.ToString());
Voici le résultat affiché dans la console suite à l’appui sur Ctrl+F5 : Joe Rattz
Le code XML issu du Listing 7.3 est très proche de celui en sortie du Listing 6.1, à ceci près qu’un seul participant a été ajouté au document. La construction fonctionnelle le rend cependant bien plus lisible, et il suffit d’observer le code pour en déduire le schéma correspondant. Étant donné qu’il n’est plus nécessaire de définir un document XML, il est encore possible de simplifier le code (voir Listing 7.4). Listing 7.4 : Le même exemple que le précédent, mais sans la définition du document XML. XElement xElement = new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))); Console.WriteLine(xElement.ToString());
L’exécution de ce code produit le même résultat que précédemment :
Linq.book Page 194 Mercredi, 18. février 2009 7:58 07
194
LINQ to XML
Partie III
Joe Rattz
Là ne s’arrêtent pas les prouesses de LINQ to XML. Par son intermédiaire, vous pouvez également lire et écrire des données XML dans un fichier. Noms, espaces de noms et préfixes Les termes "noms", "espaces de noms" et "préfixes d’espaces de noms" sont souvent abscons, sinon difficiles à appréhender pour le programmeur XML. Pour éviter toute confusion, sachez que les préfixes des espaces de noms sont gérés à l’extérieur de l’API. Ils ne font que s’ajouter aux espaces de noms et n’ont aucune existence à l’intérieur de l’API. Les espaces de noms sont utilisés pour identifier de manière unique le schéma XML d’une portion d’arbre XML. Une URI peut donc être utilisée pour chaque espace de noms, puisqu’il est unique au sein d’une société. Dans plusieurs exemples, nous utiliserons l’arbre XML suivant : Joe Rattz
Les codes écrits pour traiter ces données XML s’attendront à ce que le nœud BookParticipants contienne plusieurs nœuds BookParticipant, chacun d’entre eux ayant un attribut type et des nœuds FirstName et LastName. Que se passerait-il si ce code devait également traiter des données XML issues d’une autre source, contenant un nœud BookParticipants, mais dont le schéma diffère du précédent ? Eh bien, un espace de noms informerait le code sur la structure du schéma, et le traitement serait alors approprié. Dans XML, chaque élément a besoin d’un nom. Lorsqu’un élément est créé, si son nom est spécifié dans le constructeur, son type string est implicitement converti en un objet XName. Ce dernier consiste en un espace de noms XNameSpace, l’objet et son nom local (c’est-à-dire le nom que vous avez choisi). À titre d’exemple, l’élément BookParticipants pourrait être créé comme suit : XElement xBookParticipants = new XElement("BookParticipants");
Lorsque l’élément est créé, un objet XName contenant un espace de noms non référencé et le nom local BookParticipants est défini. Si vous utilisez le débogueur sur cette ligne de code et que vous examiniez la variable xBookParticipants dans la fenêtre Espion Express, vous verrez que son membre Name est initialisé à {BookParticipants}. Développez le membre Name. Vous verrez qu’il contient le membre LocalName initialisé à BookParticipants, et un membre NameSpace vide. Ici, l’espace de noms n’a pas été défini.
Linq.book Page 195 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
195
Pour spécifier un espace de noms, il vous suffit de créer un objet XNameSpace et de l’utiliser comme préfixe du nom local choisi : XNamespace nameSpace = "http://www.linqdev.com"; XElement xBookParticipants = new XElement(nameSpace + "BookParticipants");
Maintenant, lorsque vous visualisez l’élément xBookParticipants dans la fenêtre Espion Express du débogueur, le nom a pour valeur {{http://www.linqdev.com}BookParticipants}. Développez le membre Name. Le membre LocalName a toujours pour valeur BookParticipants mais, maintenant, le membre Namespace est initialisé à {http://www.linqdev.com}. Il n’est pas obligatoire d’utiliser un objet NameSpace pour spécifier l’espace de noms. Vous auriez tout aussi bien pu le spécifier dans l’implémentation du XElement : XElement xBookParticipants = new XElement("{http://www.linqdev.com}" +"BookParticipants");
Des accolades entourent l’espace de noms, afin d’indiquer au constructeur XElement qu’il s’agit d’un espace de noms et pas du nom de l’élément. Si vous examinez à nouveau le membre Name dans la fenêtre Espion Express du débogueur, vous verrez que le membre Name et ses enfants LocalName et NameSpace sont tous initialisés comme auparavant, lorsque l’élément avait été créé avec un objet XNamespace. Ayez bien en tête qu’il ne suffit pas de définir l’URI de votre société ou de votre domaine pour garantir l’unicité d’un espace de noms. Cela garantit simplement que vous n’entrerez pas en conflit avec d’autres sociétés qui utilisent également les règles inhérentes aux espaces de noms. Notez cependant qu’à l’intérieur de votre société des conflits entre départements pourraient se produire si la seule URI constitue l’espace de noms. C’est à ce point précis que vous devrez faire intervenir votre connaissance des divisions, départements et autres sous-structures de votre société. L’idéal serait que l’espace de noms s’étende sur tous les niveaux dont vous avez le contrôle. Supposons par exemple que vous travailliez chez LINQDev.com et que vous deviez créer un schéma relatif aux retraites pour le département des ressources humaines. L’espace de noms pourrait être le suivant : XNamespace nameSpace = "http://www.linqdev.com/ressourceshumaimes/retraites";
Pour terminer cette discussion sur le fonctionnement des espaces de noms, nous allons modifier le Listing 7.2 en y incluant un espace de noms (voir Listing 7.5). Listing 7.5 : Une version modifiée du Listing 7.2 incluant un espace de noms. XNamespace nameSpace = "http://www.linqdev.com"; XElement xBookParticipants = new XElement(nameSpace + "BookParticipants", new XElement(nameSpace + "BookParticipant", new XAttribute("type", "Author"), new XElement(nameSpace + "FirstName", "Joe"), new XElement(nameSpace + "LastName", "Rattz")), new XElement(nameSpace + "BookParticipant", new XAttribute("type", "Editor"),
Linq.book Page 196 Mercredi, 18. février 2009 7:58 07
196
LINQ to XML
Partie III
new XElement(nameSpace + "FirstName", "Ewan"), new XElement(nameSpace + "LastName", "Buckingham"))); Console.WriteLine(xBookParticipants.ToString());
Appuyez sur Ctrl+F5 pour exécuter ce code. Voici les résultats affichés dans la console : Joe Rattz Ewan Buckingham
Si un programme lit ce schéma, il saura qu’il a été émis par LINQDev.com. Pour isoler le préfixe de l’espace de noms, vous utiliserez l’objet XAttribute, comme dans le Listing 7.6. Listing 7.6 : Définition d’un préfixe dans un espace de noms. XNamespace nameSpace = "http://www.linqdev.com"; XElement xBookParticipants = new XElement(nameSpace + "BookParticipants", new XAttribute(XNamespace.Xmlns + "linqdev", nameSpace), new XElement(nameSpace + "BookParticipant")); Console.WriteLine(xBookParticipants.ToString());
Le préfixe utilisé dans ce code est "linqdev". Un objet XAttribute est utilisé pour inclure ce préfixe dans le schéma. Voici les résultats affichés dans la console :
Extraction de valeurs de nœuds Si vous avez parcouru le chapitre précédent, vous avez certainement été étonné par les résultats du Listing 6.1. L’obtention des valeurs issues d’un nœud est un vrai cassetête ! N’ayant pas travaillé sur du code XML DOM depuis un moment, j’ai inévitablement été confronté à une erreur, en oubliant qu’une étape supplémentaire était nécessaire pour extraire les données. L’API LINQ to XML solutionne ce problème d’une manière élégante. Tout d’abord, l’appel de la méthode ToString sur un élément produit la chaîne XML elle-même, et non le type de l’objet, comme le fait l’API W3C DOM. Ceci est très pratique lorsque vous voulez obtenir une portion de XML à partir d’un certain point dans l’arbre, et elle a bien plus de sens que de fournir le type de l’objet.
Linq.book Page 197 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
197
Listing 7.7 : La méthode ToString appliquée à un élément produit l’arbre XML correspondant. XElement name = new XElement("Name", "Joe"); Console.WriteLine(name.ToString());
Voici le résultat obtenu par un appui sur Ctrl+F5 : Joe
Quel changement ! Attendez un peu, la suite est encore plus étonnante. Bien entendu, les nœuds enfants sont inclus dans la sortie mais, étant donné qu’aucune surcharge de la méthode WriteLine n’a été définie pour traiter les XElement, la méthode ToString est automatiquement appelée, comme dans le Listing 7.8. Listing 7.8 : Appel implicite de la méthode ToString dans un Console.WriteLine pour obtenir l’arbre XML. XElement name = new XElement("Person", new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")); Console.WriteLine(name);
Voici les résultats affichés dans la console : Joe Rattz
Encore plus important : si vous utilisez un opérateur de casting sur un nœud pour le convertir dans un type compatible avec son contenu, vous obtenez la valeur du nœud. Le Listing 7.9 donne un exemple dans lequel la valeur du nœud Name est convertie en une chaîne de caractères. Listing 7.9 : Le casting d’un élément produit la donnée qui y est stockée. XElement name = new XElement("Name", "Joe"); Console.WriteLine(name); Console.WriteLine((string)name);
Voici les résultats de ce code : Joe Joe
String n’est pas le seul opérateur de casting. Les opérateurs suivants sont également à votre disposition : int, int?, uint, uint?, long, long?, ulong, ulong?, bool, bool?, float, float?, double, double?, decimal, decimal?, TimeSpan, TimeSpan?, DateTime, DateTime?, GUID et GUID?.
Linq.book Page 198 Mercredi, 18. février 2009 7:58 07
198
LINQ to XML
Partie III
Le Listing 7.10 donne un exemple des valeurs stockées dans plusieurs nœuds. Listing 7.10 : Valeurs stockées dans différents nœuds et récupérées par casting. XElement count = new XElement("Count", 12); Console.WriteLine(count); Console.WriteLine((int)count); XElement smoker = new XElement("Smoker", false); Console.WriteLine(smoker); Console.WriteLine((bool)smoker); XElement pi = new XElement("Pi", 3.1415926535); Console.WriteLine(pi); Console.WriteLine((double)pi);
Voici les résultats : 12 12 false False 3.1415926535 3.1415926535
Cette approche est simple et intuitive. En utilisant l’API LINQ to XML, les difficultés rencontrées dans le Listing 6.1 feront à tout jamais partie du passé ! Dans les exemples étudiés jusqu’ici, les éléments ont été convertis dans leurs types d’origine. Ceci n’est pas une obligation : il suffit que la conversion soit possible. Le Listing 7.11 donne un exemple de casting d’une chaîne de caractères en booléen. Listing 7.11 : Casting d’un nœud en utilisant un type différent du type d’origine. XElement smoker = new XElement("Smoker", "true"); Console.WriteLine(smoker); Console.WriteLine((bool)smoker);
Étant donné que l’élément a pour valeur la chaîne "true" et que cette chaîne peut être convertie en une valeur booléenne, le code s’exécute sans encombre. Voici les résultats : true True
Ce code ne laisse pas apparaître le nom de la méthode utilisée pour effectuer le casting. Le Listing 7.12 va vous montrer qu’il s’agit de la méthode System.Xml.XmlConvert. Listing 7.12 : Le casting booléen utilise la classe System.Xml.XmlConvert. try { XElement smoker = new XElement("Smoker", "Tue"); Console.WriteLine(smoker); Console.WriteLine((bool)smoker); }
Linq.book Page 199 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
199
catch (Exception ex) { Console.WriteLine(ex); }
La valeur affectée à l’élément Smoker a été intentionnellement mal orthographiée afin d’obtenir le nom de la méthode utilisée pour effectuer le casting. Un appui sur Ctrl+F5 affiche les informations suivantes dans la console : Tue System.FormatException: The string ’tue’ is not a valid Boolean value. at System.Xml.XmlConvert.ToBoolean(String s) ...
Comme vous pouvez le voir, le casting a provoqué une exception lors de l’appel à la méthode System.Xml.XmlConvert.ToBoolean.
Le modèle d’objet LINQ to XML L’API LINQ to XML vient avec un nouveau modèle d’objet contenant plusieurs classes issues de l’espace de noms System.Xml.Linq. L’une d’entre elles est la classe statique qui héberge les méthodes d’extension (Extensions). Deux autres sont dédiées aux comparateurs (XNodeDocumentOrderComparer et XNodeEqualityComparer). Les autres classes sont utilisées pour construire les arbres XML (voir Figure 7.1). Le modèle d’objet LINQ to XML.
XDocument
XComment
XElement
XContainer
XDocumentType
XAttribute
XDeclaration
XName
XNamespace
XCData
XProcessingInstruction
XText
XNode
XObject
XStreamingElement
Derived
Figure 7.1 :
Quelques remarques intéressantes : 1. Les classes XObject, XContainer et XNode sont abstraites. Elles ne peuvent donc pas être construites. 2. Les attributs XAttribute ne sont pas dérivés de nœuds XNode. En fait, il s’agit d’un tout autre type de classe, constitué de paires nom/valeur. 3. Les éléments XStreamingElement n’héritent pas de XElement. 4. Les classes XDocument et XElement sont les seules à avoir des nœuds enfants dérivés de XNode. Vous utiliserez ces classes pour construire vos arbres XML. L’API LINQ to XML étant centrée sur les éléments, la classe XElement vous sera particulièrement utile.
Linq.book Page 200 Mercredi, 18. février 2009 7:58 07
200
LINQ to XML
Partie III
Exécution différée des requêtes, suppression de nœuds et bogue d’Halloween L’exécution de toutes requêtes LINQ est différée. Il peut parfois en découler des effets secondaires indésirables. Le "bogue d’Halloween" doit son nom à la première équipe qui en a débattu ouvertement. Ces spécialistes d’Halloween ont discuté des problèmes qui découlent du changement manuel d’un index dans une boucle. Cette situation a été détectée pour la première fois par des ingénieurs bases de données alors qu’ils mettaient au point un processus d’optimisation. Une de leurs requêtes de test a modifié la valeur d’une cellule utilisée comme index par le processus d’optimisation. Cela a engendré une boucle sans fin dont le processus d’optimisation ne pouvait se dégager. Vous avez peut-être déjà expérimenté ce problème sans connaître son nom. N’avezvous jamais effectué une boucle sur une collection dans laquelle la suppression d’un élément entraînait la fin ou le mauvais comportement de la boucle ? J’ai personnellement rencontré ce problème récemment, alors que je travaillais avec des contrôles serveur ASP.NET. J’ai été amené à supprimer les enregistrements sélectionnés par l’utilisateur dans un contrôle DataGrid. Pour ce faire, j’ai bouclé sur les enregistrements, du premier au dernier, en supprimant ceux qui étaient sélectionnés. Ce faisant, les pointeurs utilisés dans la boucle ont été désorganisés. Résultat : certains enregistrements ont été supprimés par erreur et d’autres qui auraient dû être supprimés ont été ignorés. Le concepteur des contrôles a trouvé une solution qui consistait à parcourir les enregistrements du dernier au premier. Avec LINQ to XML, vous tomberez forcément sur ce type de problème lorsque vous supprimerez des nœuds dans un arbre XML, mais peut-être également dans d’autres situations totalement différentes. Il est donc important d’avoir ce problème à l’esprit lorsque vous vous lancerez dans le codage. Listing 7.13 : Mise en évidence du bogue d’Halloween. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); foreach (XElement element in elements) { Console.WriteLine("Elément source: {0} : valeur = {1}", element.Name, element.Value);
Linq.book Page 201 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
201
} foreach (XElement element in elements) { Console.WriteLine("Suppressionde l’élément {0} = {1} ...", element.Name, ➥element.Value); element.Remove(); } Console.WriteLine(xDocument);
La première ligne définit le document XML. Le bloc d’instructions suivant initialise ce document avec une séquence d’éléments BookParticipant. La première boucle foreach affiche les deux éléments de la séquence. La boucle suivante énumère à nouveau la séquence et supprime l’élément BookParticipant. Enfin, la dernière instruction affiche le document XML résultant. Si le bogue d’Halloween ne vous saute pas aux yeux, regardez de plus près le message de suppression. Normalement, les deux éléments BookParticipant devraient être supprimés et il devrait en résulter un document XML vide. Et, pourtant, voici le résultat : Elément source : BookParticipant : valeur = JoeRattz Elément source : BookParticipant : valeur = EwanBuckingham Suppression de l’élément BookParticipant = JoeRattz ... Ewan Buckingham
Sur les deux éléments SourceParticipant, seul le premier, JoeRattz, est effectivement supprimé. Le bogue d’Halloween a eu raison de la seconde énumération ! Dans certains cas, ce problème peut se manifester différemment : l’énumération peut se terminer prématurément ou une exception peut être levée. Vous vous demandez certainement quelle solution peut être apportée à ce problème. Eh bien, dans ce cas précis, la solution consiste à mettre les éléments dans une mémoire tampon et à énumérer cette mémoire plutôt que le document XML, pour lequel les pointeurs internes sont altérés par la suppression. Pour ce faire, nous allons utiliser un opérateur de requête standard spécialement conçu pour mettre des éléments dans une mémoire tampon, afin d’éviter les problèmes liés au côté différé de certaines requêtes. Listing 7.14 : Une solution au bogue d’Halloween. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"),
Linq.book Page 202 Mercredi, 18. février 2009 7:58 07
202
LINQ to XML
Partie III
new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } foreach (XElement element in elements.ToArray()) { Console.WriteLine("Suppression de l’élément {0} = {1} ...", element.Name, ➥element.Value); element.Remove(); } Console.WriteLine(xDocument);
Ce code est proche du précédent mais, ici, la suppression se fait en énumérant les éléments via l’opérateur ToArray. Voici le résultat : Elément source : BookParticipant : valeur = JoeRattz Elément source : BookParticipant : valeur = EwanBuckingham Suppression de l’élément BookParticipant = JoeRattz ... Suppression de l’élément BookParticipant = EwanBuckingham ...
Cette fois-ci, deux messages de suppression sont affichés dans la console. Les deux éléments sont bien supprimés, et le bogue d’Halloween a été éradiqué.
Création XML Comme il a été dit précédemment, la construction fonctionnelle de l’API LINQ to XML facilite grandement la création d’un arbre XML. Cela vous sera confirmé tout au long de cette section, qui passe en revue la création des principales classes XML par l’intermédiaire de cette nouvelle API. Étant donné que les éléments sont le point central de l’API LINQ to XML et que vous travaillerez avec ce type d’objet dans la plupart des cas, nous allons nous intéresser dans un premier temps à la création d’éléments avec la classe XElement. Par la suite, les autres classes XML seront passées en revue par ordre alphabétique. Création d’éléments avec XElement La classe XElement est la plus utilisée dans cette nouvelle API. Nous allons examiner deux des constructeurs de cette classe : XElement.XElement(XName name, object content); XElement.XElement(XName name, params object[] content);
Le premier constructeur est le plus simple. Il correspond au cas où un élément a une valeur texte et aucun nœud enfant (voir Listing 7.15).
Linq.book Page 203 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
203
Listing 7.15 : Création d’un élément avec le premier prototype. XElement firstName = new XElement("FirstName", "Joe"); Console.WriteLine((string)firstName);
Le premier argument du constructeur est un objet XName. Comme il a été dit précédemment, cet objet sera créé en convertissant de façon implicite la chaîne passée en entrée en un XName. Le deuxième argument représente la valeur de l’élément ; dans cet exemple une chaîne initialisée à "Joe". L’API convertit automatiquement cette chaîne en un objet XText. La deuxième instruction utilise un opérateur de casting pour obtenir la valeur de l’élément FirstName. Voici le résultat : Joe
Le choix du type des objets est très flexible. C’est le type d’un objet qui contrôle les relations avec l’élément auquel il est ajouté. Le Tableau 7.1 dresse la liste de tous les types de contenus autorisés et indique comment ils sont gérés. Même si bon nombre d’éléments sont stockés sous la forme de chaînes (c’est par exemple le cas des entiers, qui font partie de la catégorie "autres types" du Tableau 7.1), vous pouvez les lire dans leur format d’origine en utilisant les opérateurs de casting appropriés. Par exemple, en appliquant l’opérateur de casting (int) à un élément, vous obtenez la valeur entière de cet élément. Tant que vous utilisez un opérateur de casting licite, le casting est la façon la plus simple d’obtenir la valeur d’un élément, exprimée dans son type d’origine. Le deuxième constructeur XElement est semblable au premier, mais il permet de spécifier un contenu composé de plusieurs objets. Reportez-vous aux Listings 7.1 ou 7.2 pour avoir un exemple du deuxième constructeur. Tableau 7.1 : Comportement de l’insertion d’objets LINQ to XML dans un objet parent
Type de l’objet
Gestion
String
Un objet string ou une chaîne littérale est automatiquement converti en un objet XText et considéré comme tel.
XText
Un tel objet peut avoir une valeur string ou XText. Il est ajouté comme nœud enfant de l’élément, mais considéré comme le contenu texte de l’élément.
XCData
Un tel objet peut avoir une valeur string ou XCData. Il est ajouté comme nœud enfant de l’élément, mais considéré comme le contenu CData de l’élément.
XElement
Cet objet est ajouté en tant qu’élément enfant.
XAttribute
Cet objet est ajouté en tant qu’attribut.
XProcessingInstruction Cet objet est ajouté en tant que contenu enfant.
Linq.book Page 204 Mercredi, 18. février 2009 7:58 07
204
LINQ to XML
Partie III
Tableau 7.1 : Comportement de l’insertion d’objets LINQ to XML dans un objet parent (suite)
Type de l’objet
Gestion
XComment
Cet objet est ajouté en tant que contenu enfant.
IEnumerable
Cet objet est énuméré et la manipulation des types est appliquée de façon récursive.
null
Cet objet est ignoré. Comme vous le verrez par la suite, ce type d’objet peut se révéler utile lors de transformations XML.
Autres types
La méthode ToString est appelée et la valeur résultante est traitée en tant qu’une chaîne de caractères.
Un peu plus tôt dans cette section, nous avons rappelé que la construction fonctionnelle allait être très utile pour définir des requêtes LINQ qui produisent des données XML. Pour illustrer ces propos, nous allons créer l’arbre XML BookParticipants. Plutôt qu’écrire "à la main" les valeurs des éléments, nous allons les récupérer en interrogeant une source de données compatible LINQ. Dans cet exemple, la source de données sera un tableau. Avant de commencer, nous avons besoin d’une classe pour stocker les données. Étant donné qu’il existe plusieurs types de BookParticipants, nous allons utiliser un enum pour les recenser. enum ParticipantTypes { Author = 0, Editor } class BookParticipant { public string FirstName; public string LastName; public ParticipantTypes ParticipantType; }
Nous allons maintenant définir et initialiser un tableau de BookParticipant. L’arbre XML sera alors généré en utilisant une requête LINQ qui extraira les données du tableau (voir Listing 7.16). Listing 7.16 : Création d’un arbre XML avec une requête LINQ. BookParticipant[] bookParticipants = new[] { new BookParticipant {FirstName = "Joe", LastName = "Rattz", ParticipantType = ParticipantTypes.Author}, new BookParticipant {FirstName = "Ewan", LastName = "Buckingham", ParticipantType = ParticipantTypes.Editor} }; XElement xBookParticipants = new XElement("BookParticipants", bookParticipants.Select(p =>
Linq.book Page 205 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
205
new XElement("BookParticipant", new XAttribute("type", p.ParticipantType), new XElement("FirstName", p.FirstName), new XElement("LastName", p.LastName)))); Console.WriteLine(xBookParticipants);
Le premier bloc de code crée le tableau bookParticipants d’éléments BookParticipant. Le deuxième bloc interroge ce tableau en utilisant un opérateur select et génère des éléments BookParticipant à partir des éléments membres du tableau. Voici l’arbre XML généré : Joe Rattz Ewan Buckingham
Pour n’avoir aucun regret, reportez-vous au Listing 6.1 : ce code génère le même arbre en utilisant l’API W3C XML DOM ! Création d’attributs avec XAttribute Contrairement à ce qui se faisait dans l’API W3C XML DOM, les attributs n’héritent pas des nœuds. Implémentés avec la classe XAttribute, ils consistent en des paires nom/valeur stockées dans une collection d’objets XAttribute appartenant à un objet XElement. Grâce à la construction fonctionnelle, un attribut peut être créé et ajouté à un élément à la volée, comme dans le Listing 7.17. Listing 7.17 : Définition d’un attribut avec la construction fonctionnelle. XElement xBookParticipant = new XElement("BookParticipant", new XAttribute("type", "Author")); Console.WriteLine(xBookParticipant);
L’exécution de ce code donne le résultat suivant :
Parfois, il n’est pas possible de créer un attribut pendant la construction de l’élément. Comme le montre le Listing 7.18, ces deux actions peuvent tout aussi bien être séparées.
Linq.book Page 206 Mercredi, 18. février 2009 7:58 07
206
LINQ to XML
Partie III
Listing 7.18 : La définition de l’élément et l’ajout de son attribut sont dissociés. XElement xBookParticipant = new XElement("BookParticipant"); XAttribute xAttribute = new XAttribute("type", "Author"); xBookParticipant.Add(xAttribute); Console.WriteLine(xBookParticipant);
Le résultat est identique au précédent :
À nouveau, remarquez à quel point la méthode XElement.Add est flexible. Elle accepte tout type d’objet et applique les mêmes règles au contenu de l’élément que lors de l’instanciation du XElement. Création de commentaires avec XComment La création de commentaires avec LINQ to XML est vraiment simple. La classe utilisée est XComment. Vous pouvez créer un commentaire et le lier à un élément à la volée, en utilisant la construction fonctionnelle (voir Listing 7.19). Listing 7.19 : Définition d’un commentaire avec la création fonctionnelle. XElement xBookParticipant = new XElement("BookParticipant", new XComment("Cette personne est ➥retraitée")); Console.WriteLine(xBookParticipant);
Voici le résultat affiché dans la console :
Parfois, il n’est pas possible de définir un commentaire lors de la construction de l’élément. Si nécessaire, vous pouvez utiliser la méthode Add pour ajouter le commentaire après que l’élément eut été construit (voir Listing 7.20). Listing 7.20 : La définition de l’élément et l’ajout du commentaire sont dissociés. XElement xBookParticipant = new XElement("BookParticipant"); XComment xComment = new XComment("Cette personne est retraitée"); xBookParticipant.Add(xComment); Console.WriteLine(xBookParticipant);
Le résultat est identique au précédent :
Linq.book Page 207 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
207
Création de conteneurs avec XContainer XContainer est une classe abstraite. Il n’est donc pas possible de l’instancier. En revanche, vous pouvez instancier une de ses sous-classes : XDocument ou XElement. La classe XContainer hérite de la classe XNode et peut contenir d’autres classes qui héritent de XNode.
Création de déclarations avec XDeclaration Grâce à la classe XDeclaration de l’API LINQ to XML, la définition de déclarations est un jeu d’enfant. Contrairement à la plupart des autres classes de l’API LINQ to XML, les déclarations s’ajoutent au document XML et non à un élément. Vous rappelez-vous à quel point le constructeur de la classe XElement était flexible ? Toutes les classes pour lesquelles il n’était pas spécialement destiné déclenchaient l’appel de la méthode ToString et le texte retourné était ajouté à l’élément sous une forme textuelle. Par inadvertance, il est donc possible d’ajouter une déclaration à un élément en utilisant la classe XDeclaration. Cependant, si cela est permis, le résultat ne sera pas celui escompté. ATTENTION Les déclarations XML s’appliquent au document XML. Cependant, il est possible de les appliquer à un XElement, sans toutefois obtenir l’effet recherché.
Il est possible de définir une déclaration à la volée et de l’ajouter à un document XML en utilisant la construction fonctionnelle (voir Listing 7.21). Listing 7.21 : Définition d’une déclaration avec la construction fonctionnelle. XDocument xDocument = new XDocument(new XDeclaration("1.0", "UTF-8", "yes"), new XElement("BookParticipant")); Console.WriteLine(xDocument);
Voici le résultat :
Comme vous pouvez le voir, la déclaration n’apparaît pas dans la sortie console. Cependant, si vous déboguez le code et affichez la fenêtre Espion Express, vous verrez que la déclaration est bien là. Parfois, il n’est pas possible de définir la déclaration lors de la construction du document. Vous devez alors instancier la déclaration, puis l’affecter à la propriété Declaration du document (voir Listing 7.22).
Linq.book Page 208 Mercredi, 18. février 2009 7:58 07
208
LINQ to XML
Partie III
Listing 7.22 : Création d’une déclaration et affectation à la propriété Declaration du document. XDocument xDocument = new XDocument(new XElement("BookParticipant")); XDeclaration xDeclaration = new XDeclaration("1.0", "UTF-8", "yes"); xDocument.Declaration = xDeclaration; Console.WriteLine(xDocument);
Voici le résultat :
Tout comme dans l’exemple précédent, la déclaration n’apparaît pas dans la sortie console. Cependant, si vous déboguez le code et affichez la fenêtre Espion Express, vous verrez que la déclaration est bien là. Création de types de documents avec XDocumentType La classe XDocumentType de l’API LINQ to XML facilite grandement la création de types de documents (DTD). Contrairement à la plupart des autres classes de l’API LINQ to XML, les types de documents s’ajoutent au document XML et non à un élément. Vous rappelez-vous à quel point le constructeur de la classe XElement était flexible ? Toutes les classes pour lesquelles il n’était pas spécialement destiné déclenchaient l’appel de la méthode ToString et le texte retourné était ajouté à l’élément sous une forme textuelle. Par inadvertance, il est donc possible d’ajouter une déclaration à un élément en utilisant la classe XDeclaration. Cela est permis, mais ne donnera pas le résultat escompté. ATTENTION Les types de documents XML s’appliquent au document XML. Cependant, il est possible de les appliquer à un XElement, sans toutefois obtenir l’effet recherché.
Il est possible de définir un type de document à la volée et de l’ajouter à un document XML en utilisant la construction fonctionnelle (voir Listing 7.23). Listing 7.23 : Définition d’un type de document avec la construction fonctionnelle. XDocument xDocument = new XDocument(new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XElement("BookParticipant")); Console.WriteLine(xDocument);
Linq.book Page 209 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
209
Voici le résultat :
Parfois, il n’est pas possible de définir le type de document lors de la construction du document. Vous devez alors instancier la définition, puis l’ajouter au document XML avec la méthode add (voir Listing 7.24). Listing 7.24 : Création d’un type de document et ajout au document. XDocument xDocument = new XDocument(); XDocumentType documentType = new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null); xDocument.Add(documentType, new XElement("BookParticipants")); Console.WriteLine(xDocument);
Voici le résultat :
Dans ce code, aucun élément n’a été ajouté avant de définir le type du document. Si vous tentez de définir le type du document après avoir ajouté un ou plusieurs éléments, l’exception suivante est levée : L’exception InvalidOperationException n’a pas été gérée. Cette opération créerait un document incorrectement structuré.
Si vous êtes amené à définir un type de document après l’instanciation du document, assurez-vous qu’aucun élément n’a été spécifié durant l’instanciation du document ou avant la déclaration DTD. Création de documents avec XDocument Comme il a été dit précédemment, il n’est pas nécessaire de définir un document XML pour être en mesure de créer un arbre ou un élément XML. Cependant, si vous êtes amené à le faire, LINQ to XML va vous simplifier grandement la tâche (voir Listing 7.25). Listing 7.25 : Création d’un document XML avec XDocument. XDocument xDocument = new XDocument(); Console.WriteLine(xDocument);
Ce code ne produit aucune sortie, puisque le document est vide.
Linq.book Page 210 Mercredi, 18. février 2009 7:58 07
210
LINQ to XML
Partie III
Cet exemple étant un peu trop simple, nous allons créer un nouveau document et y ajouter toutes les classes LINQ to XML spécifiquement conçues pour être ajoutées à un objet XDocument (voir Listing 7.26). Listing 7.26 : Un autre exemple légèrement plus complexe de création d’un document XML avec XDocument. XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), new XElement("BookParticipants")); Console.WriteLine(xDocument);
L’instruction de traitement et l’élément auraient pu être ajoutés au niveau élément. Ils ont été placés au niveau du document pour lui donner un peu de consistance. Voici le résultat :
Vous avez peut-être remarqué que la déclaration n’apparaît pas dans la sortie console. Comme indiqué dans les exemples de la section "Définition de déclarations avec XDeclarations", vous pouvez déboguer le code et afficher une fenêtre Espion Express pour constater que la déclaration est bien là. Création de noms avec XName Comme indiqué un peu plus tôt dans ce chapitre, il n’est pas possible de créer des noms en utilisant un objet XName. Cette classe n’a en effet aucun constructeur public. Vous ne pouvez donc pas l’instancier. Un objet XName sera défini à partir d’une chaîne, éventuellement complétée d’un espace de noms, lorsque le code le nécessite. Un objet XName est constitué d’un nom local (une chaîne) et d’un espace de noms (un XNamespace). Dans le Listing 7.27, le code appelle le constructeur XElement dont l’argument est un XName. Listing 7.27 : Dans cet exemple, un objet XName est automatiquement créé. XElement xBookParticipant = new XElement("BookParticipant"); Console.WriteLine(xBookParticipant);
Dans cet exemple, un objet XElement est instancié à partir de son nom au format chaîne. L’objet XName BookParticipant est automatiquement créé et affecté à la propriété Name de l’objet XElement. Ici, aucun espace de noms n’étant spécifié, le XName n’a donc aucun espace de noms.
Linq.book Page 211 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
211
Voici le résultat :
Le Listing 7.28 montre comment instancier un XElement en fournissant son nom et un espace de noms. Listing 7.28 : Dans cet exemple, un objet XName est automatiquement créé, accompagné d’un espace de noms. XNamespace ns = "http://www.linqdev.com/Books"; XElement xBookParticipant = new XElement(ns + "BookParticipant"); Console.WriteLine(xBookParticipant);
Ce code produit la sortie XML suivante :
Pour avoir de plus amples informations sur la création de noms en utilisant l’API LINQ to XML, reportez-vous à la section intitulée "Noms, espaces de noms et préfixes", un peu plus tôt dans ce chapitre. Création d’espaces de noms avec XNamespace Dans l’API LINQ to XML, les espaces de noms sont implémentés avec la classe XNamespace. Vous trouverez un exemple de création et d’utilisation d’un espace de noms dans le Listing 7.28. Pour avoir de plus amples informations sur la création de noms en utilisant l’API LINQ to XML, reportez-vous à la section intitulée "Noms, espaces de noms et préfixes", un peu plus tôt dans ce chapitre. Création de nœuds avec XNode XNode étant une classe abstraite, il n’est pas possible de l’instancier. Vous pouvez en revanche instancier une de ses sous-classes : XComment, XContainer, XDocumentType, XProcessingInstruction ou XText. Théoriquement, un XNode est une classe quelconque qui fonctionne comme un nœud dans un arbre XML.
Création d’instructions de traitement avec XProcessingInstruction La définition d’instructions de traitement n’a jamais été aussi simple qu’avec la classe XProcessingInstruction de l’API LINQ to XML. Vous pouvez définir des instructions de traitement au niveau document ou élément. Le Listing 7.29 illustre ces deux possibilités en utilisant la construction fonctionnelle.
Linq.book Page 212 Mercredi, 18. février 2009 7:58 07
212
LINQ to XML
Partie III
Listing 7.29 : Définition d’une instruction de traitement aux niveaux document et élément. XDocument xDocument = new XDocument( new XProcessingInstruction("BookCataloger", "out-of-print"), new XElement("BookParticipants", new XElement("BookParticipant", new XProcessingInstruction("ParticipantDeleter", "delete"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(xDocument);
Avant de donner le résultat de ce code, je veux insister sur la simplicité d’utilisation de la construction fonctionnelle. La comparaison de ce code avec celui du Listing 6.1 met clairement en évidence la supériorité de l’API LINQ to XML par rapport à l’ancienne API W3C XML. Voici les résultats : Joe Rattz
Je suppose qu’il ne vous sera pas trop difficile d’imaginer le code permettant d’ajouter une instruction de traitement après la construction du document, puisqu’il s’apparente à celui permettant d’ajouter un autre type de nœud. Le Listing 7.30 donne un exemple plus complexe de création et d’ajout d’une instruction de traitement a fortiori. Listing 7.30 : Ajout d’instructions de traitement après la construction du document et de l’élément. XDocument xDocument = new XDocument(new XElement("BookParticipants", new XElement("BookParticipant", new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); XProcessingInstruction xPI1 = new XProcessingInstruction("BookCataloger", "out-of-print"); xDocument.AddFirst(xPI1); XProcessingInstruction xPI2 = new XProcessingInstruction("ParticipantDeleter", "delete"); XElement outOfPrintParticipant = xDocument .Element("BookParticipants") .Elements("BookParticipant") .Where(e => ((string)((XElement)e).Element("FirstName")) == "Joe" && ((string)((XElement)e).Element("LastName")) == "Rattz") .Single(); outOfPrintParticipant.AddFirst(xPI2); Console.WriteLine(xDocument);
Linq.book Page 213 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
213
Plusieurs passages de ce listing sont dignes d’intérêt. Comme vous pouvez le voir, le document et l’arbre XML ont été créés en utilisant la construction fonctionnelle. Une instruction de traitement a été ajoutée au document après sa construction. Ici, c’est la méthode XElement.AddFirst qui a été choisie pour créer le premier nœud enfant du document (cette méthode a été préférée à XElement.Add, qui ajoute un nœud à la fin des nœuds enfants du document. À cet emplacement, il pourrait être trop tard pour honorer une instruction de traitement). Pour ajouter une instruction de traitement à un des éléments, nous devons y faire référence. Nous aurions pu construire un objet XElement et mémoriser sa référence, mais j’ai pensé qu’il était temps d’introduire les possibilités des requêtes LINQ à venir. Comme vous pouvez le voir, la requête utilisée est plutôt complexe. Elle extrait du document l’élément BookParticipants en utilisant la méthode Element (voir "Déplacements XML", un peu plus loin dans cette section). La séquence d’objets XElement BookParticipant, pour laquelle les éléments FirstName et LastName ont respectivement pour valeur "Joe" et "Ratz", est alors récupérée. Les valeurs de FirstName et LastName ont été obtenues en utilisant l’opérateur de casting (string). L’opérateur Where retourne un IEnumerable, alors que nous avons besoin d’un XElement. La réponse retournée par la requête étant unique, nous pouvons utiliser l’opérateur de requête standard différé First de LINQ to Object. Une fois la référence à l’objet XElement obtenue, il est très simple d’ajouter l’instruction de traitement et d’afficher les résultats. Voici les résultats affichés dans la console : Joe Rattz
Création d’éléments streaming avec XStreamingElement Dans la deuxième partie de cet ouvrage, nous avons vu que plusieurs des opérateurs de requête standard différaient leur exécution jusqu’à l’énumération des données retournées. Si vous utilisez de tels opérateurs tout en voulant obtenir une projection au format XML, il faudra choisir entre le côté différé des opérateurs de requête standard et l’exécution immédiate d’une requête de projection LINQ to XML. À titre d’exemple, dans le Listing 7.31, le quatrième élément du tableau names est modifié et, pourtant, lorsque nous affichons les valeurs de l’objet XElement, l’arbre XML contient les données originales. Ceci vient du fait que l’élément XNames a été entièrement créé avant que l’élément du tableau names n’ait été modifié.
Linq.book Page 214 Mercredi, 18. février 2009 7:58 07
214
LINQ to XML
Partie III
Listing 7.31 : Exécution immédiate de l’arbre XML. string[] names = { "John", "Paul", "George", "Pete" }; XElement xNames = new XElement("Beatles", from n in names select new XElement("Name", n)); names[3] = "Ringo"; Console.WriteLine(xNames);
Ce code produit l’arbre XML suivant : John Paul George Pete
Comme vous le voyez, chaque objet XElement de la séquence devient un élément enfant de Beatles. L’élément name[3] a été initialisé à "Ringo" avant d’afficher l’arbre XML et, pourtant, le dernier élément de cet arbre contient toujours la valeur originale " Pete". Ceci vient du fait que la séquence names doit être énumérée pour pouvoir construire l’objet XElement. La requête est donc exécutée immédiatement. Si vous voulez que la construction de l’arbre XML soit différée, il faut utiliser des éléments streaming implémentés avec la classe XStreamingElement. Le Listing 7.32 représente le même exemple, mais cette fois-ci nous utilisons des objets XStreamingElement à la place des objets XElement. Listing 7.32 : Exécution différée de la construction de l’arbre XML avec la classe XStreamingElement. string[] names = { "John", "Paul", "George", "Pete" }; XStreamingElement xNames = new XStreamingElement("Beatles", from n in names select new XStreamingElement("Name", n)); names[3] = "Ringo"; Console.WriteLine(xNames);
Si ce code fonctionne, le dernier nœud Name devrait avoir la valeur "Ringo". Voici le résultat : John Paul George Ringo
Linq.book Page 215 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
215
Création de textes avec XText Comme le prouve le Listing 7.33, la définition de texte est très simple. Listing 7.33 : Création d’un élément et affectation d’une valeur chaîne. XElement xFirstName = new XElement("FirstName", "Joe"); Console.WriteLine(xFirstName);
Voici le résultat : Joe
Une chose n’apparaît pas dans ce listing : la chaîne "Joe" est transformée en un objet XText avant d’être ajoutée à l’objet XElement. En examinant l’objet xFirstName dans le débogueur, on se rend compte qu’il contient un seul nœud : un objet XText de valeur "Joe". Étant donné que cette conversion est automatique, dans la plupart des cas vous ne serez pas obligé de construire un objet texte. Cependant, si cela est nécessaire, il vous suffira d’instancier un objet XText, comme dans le Listing 7.34. Listing 7.34 : Création d’un nœud texte et utilisation dans l’initialisation d’un XElement. XText xName = new XText("Joe"); XElement xFirstName = new XElement("FirstName", xName); Console.WriteLine(xFirstName);
Ce code donne le même résultat que le précédent. Si vous utilisez le débogueur pour examiner l’état interne de l’objet xFirstName, vous verrez qu’il est identique à celui de l’objet créé dans l’exemple précédent : Joe
Définition d’un objet CData avec XCData Le Listing 7.35 donne un exemple de définition d’un objet CData. Listing 7.35 : Création d’un nœud CData puis initialisation d’un XElement. XElement xErrorMessage = new XElement("HTMLMessage", new XCData("Invalid user id or password.")); Console.WriteLine(xErrorMessage);
Voici le résultat : Invalid user id or password.]]>
Linq.book Page 216 Mercredi, 18. février 2009 7:58 07
216
LINQ to XML
Partie III
Sauvegarde de fichiers XML La création, la modification et la suppression de données XML n’auraient aucun intérêt s’il n’était pas possible de sauvegarder les données. Cette section va vous montrer plusieurs techniques de sauvegarde. Sauvegardes avec XDocument.Save() Vous pouvez sauvegarder vos données XML en utilisant un des prototypes de la méthode XDocument.Save : void void void void void
XDocument.Save(string filename); XDocument.Save(TextWriter textWriter); XDocument.Save(XmlWriter writer); XDocument.Save(string filename, SaveOptions options); XDocument.Save(TextWriter textWriter, SaveOptions options);
Le Listing 7.36 donne un exemple de sauvegarde du document XML dans le dossier du projet. Listing 7.36 : Sauvegarde d’un document avec la méthode XDocument.Save. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); xDocument.Save("bookparticipants.xml");
La méthode Save a été appelée sur un objet de type XDocument. Ceci est possible car les méthodes Save sont des méthodes d’instances. Comme vous le verrez un peu plus loin, les méthodes Load sont en revanche des méthodes statiques. Elles doivent être appelées sur des classes XDocument ou XElement. Voici le contenu du fichier bookparticipants.xml, ouvert dans un éditeur de texte tel que le Bloc-notes de Windows. Joe Rattz
Ce document XML est facile à lire parce que la version de la méthode Save met en forme les données. Si, en revanche, nous appelions la méthode Save suivante : xDocument.Save("bookparticipants.xml", SaveOptions.DisableFormatting);
Linq.book Page 217 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
217
les résultats seraient bien moins lisibles : Joe Rattz
Les données sont placées sur une seule et même ligne. Pour vous en assurer, ouvrez le fichier dans un éditeur de texte. Si vous l’ouvriez dans un navigateur Internet, elles seraient automatiquement mises en forme pour apparaître comme dans le résultat du Listing 7.36. INFO Passée en deuxième argument de la méthode Save, la valeur SaveOptions.None produit le même résultat que le Listing 7.36.
Sauvegarde avec XElement.Save Je l’ai répété plusieurs fois, avec l’API LINQ to XML, il n’est pas nécessaire de créer un document XML. Ceci reste d’actualité quant à la sauvegarde de données XML. La classe XElement propose plusieurs méthodes qui abondent dans ce sens : void void void void void
XElement.Save(string filename); XElement.Save(TextWriter textWriter); XElement.Save(XmlWriter writer); XElement.Save(string filename, SaveOptions options); XElement.Save(TextWriter textWriter, SaveOptions options);
Le Listing 7.37 est un exemple très proche du précédent mais, ici, aucun document XML n’est créé. Listing 7.37 : Sauvegarde d’un élément avec la méthode XElement. XElement bookParticipants = new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))); bookParticipants.Save("bookparticipants.xml");
Le résultat est identique au précédent : Joe Rattz
Linq.book Page 218 Mercredi, 18. février 2009 7:58 07
218
LINQ to XML
Partie III
Lecture de fichiers XML Cette section passe en revue quelques-unes des techniques qui permettent de lire des données stockées dans un fichier XML. Lecture avec XDocument.Load() Voici la liste des méthodes qui vous permettront de lire des données stockées dans un fichier XML : static static static static static static
XDocument XDocument XDocument XDocument XDocument XDocument
XDocument.Load(string uri); XDocument.Load(TextReader textReader); XDocument.Load(XmlReader reader); XDocument.Load(string uri, LoadOptions options); XDocument.Load(TextReader textReader, LoadOptions options); XDocument.Load(XmlReader reader, LoadOptions options);
Ces méthodes sont les parfaites répliques des méthodes XDocument.Save. Il existe cependant quelques différences qui valent la peine d’être signalées. Tout d’abord, les méthodes Save étant des méthodes d’instance, elles s’appliquent à un objet XDocument ou XElement. Les méthodes Load étant des méthodes statiques, vous devez appeler la classe XDocument elle-même. Par ailleurs, les méthodes Save dont le premier paramètre est de type string doivent spécifier le nom du fichier, alors que les méthodes Load dont le premier paramètre est de type string acceptent les URI. Le Tableau 7.2 dresse la liste des valeurs possibles du paramètre LoadOptions. Tableau 7.2 : Le paramètre LoadOptions.
Valeur
Description
LoadOptions.None
Aucune option de chargement.
LoadOptions.PreserveWhitespace
Conservation des sauts de ligne et autres espaces dans la source XML.
LoadOptions.SetLineInfo
Cette option permet d’obtenir la ligne et la position des objets hérités de XObject en utilisant l’interface IXmlLineInfo.
LoadOptions.SetBaseUri
Cette option permet d’obtenir l’URI des objets qui héritent de XObject.
Ces options peuvent être combinées en utilisant l’opérateur OR (|). Mais, attention, en fonction du contexte certaines options ne donneront pas les résultats escomptés. Par exemple, lorsqu’un élément ou un document est créé à partir d’une chaîne, aucune ligne d’information ni aucun URI ne sont disponibles. De même, lorsqu’un document est créé à partir d’un XmlReader, aucun URI n’est disponible. Le Listing 7.38 montre comment lire le document XML créé dans l’exemple précédent.
Linq.book Page 219 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
219
Listing 7.38 : Lecture d’un document avec la méthode XDocument.Load. XDocument xDocument = XDocument.Load("bookparticipants.xml", LoadOptions.SetBaseUri | LoadOptions.SetLineInfo); Console.WriteLine(xDocument); XElement firstName = xDocument.Descendants("FirstName").First(); Console.WriteLine("FirstName : ligne {0}, position{1}", ((IXmlLineInfo)firstName).LineNumber, ((IXmlLineInfo)firstName).LinePosition); Console.WriteLine("Adresse URI de l’élément FirstName
:{0}", firstName.BaseUri);
INFO Pour que le type IXmlLineInfo puisse être utilisé, vous devez ajouter une directive using System.xml; ou faire référence à l’espace de noms correspondant.
Ce code charge le fichier XML créé dans l’exemple précédent. Après le chargement et l’affichage du document, nous définissons une référence pour l’élément FirstName et affichons sa ligne et sa position dans le document XML source. Le code se termine par l’affichage de l’adresse URI de l’élément FirstName. Voici les résultats : Joe Rattz FirstName : ligne 4, position 6 Adresse URI de l’élément FirstName : file:///C:/Documents and Settings/…/Projects/ LINQChapter7/LINQChapter7/bin/Debug/bookparticipants.xml
Le document a bien l’allure souhaitée. Cependant, la ligne de l’élément FirstName n’a pas l’air de correspondre. Un rapide coup d’œil au résultat du Listing 7.37 aura tôt fait de vous convaincre que cette information est correcte. En effet, la première ligne est réservée à la déclaration du document, et cette ligne n’apparaît pas dans l’affichage du document :
Lecture avec XElement.Load() Virtuellement, la lecture d’un élément ou d’un document ne présente aucune différence. Voici les méthodes permettant de lire des données stockées dans un XDocument ou un XElement : static XElement XElement.Load(string uri); static XElement XElement.LoadTextReader textReader); static XElement XElement.Load(XmlReader reader);
Linq.book Page 220 Mercredi, 18. février 2009 7:58 07
220
LINQ to XML
Partie III
static XElement XElement.Load(string uri, LoadOptions options); static XElement XElement.Load(TextReader textReader, LoadOptions options); static XElement XElement.Load(XmlReader reader, LoadOptions options);
Tout comme les méthodes XDocument.Save, ces méthodes sont statiques. Elles doivent donc être appelées à partir de la classe XElement. Le Listing 7.39 montre comment lire les données XML sauvegardées avec la méthode XElement.Save dans le Listing 7.37. Listing 7.39 : Lecture d’un document avec la méthode XElement.Load. XElement xElement = XElement.Load("bookparticipants.xml"); Console.WriteLine(xElement);
Les résultats sont bien conformes à nos attentes : Joe Rattz
Tout comme pour XDocument.Load, il existe des surcharges de la méthode XElement.Load qui permettent d’utiliser le paramètre LoadOptions. Reportez-vous à la section intitulée "Lecture avec XDocument.Load()" pour avoir de plus amples informations à ce sujet. Extraction avec XDocument.Parse() ou XElement.Parse() Combien de fois avez-vous extrait des données XML en passant par des chaînes de caractères ? Il faut bien avouer que cette tâche n’est pas des plus agréables ! Mais, rassurezvous, l’API LINQ to XML va apporter une réponse élégante à cette problématique. La méthode statique Parse est accessible aux classes XDocument et XElement. Par son intermédiaire, il est possible d’extraire des données XML. Fort de ce qui a été vu dans ce chapitre, vous ne devez avoir aucune difficulté à imaginer que, si l’extraction de données est possible depuis la classe XDocument, elle l’est aussi depuis la classe XElement. Nous allons donc raisonner sur un seul exemple relatif à la classe XElement. Dans la section intitulée "Sauvegardes avec XDocument.Save", vous avez pu voir l’influence du paramètre LoadOptions lorsqu’il est initialisé à DisableFormatting : les données sont sauvegardées sur une seule et même ligne XML. Le Listing 7.40 utilise cette chaîne XML (en ayant pris le soin d’échapper les guillemets), l’extrait dans un élément et affiche le résultat. Listing 7.40 : Extraction d’une chaîne XML dans un élément. string xml = "" + "JoeRattz" + "";
Linq.book Page 221 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
221
XElement xElement = XElement.Parse(xml); Console.WriteLine(xElement);
Voici le résultat : Joe Rattz
Impressionnant, n’est-ce pas ? Rappelez-vous les vieux jours où vous deviez créer un document en utilisant la classe XmlDocument de l’API W3C XML DOM. Le document n’étant plus l’élément de référence, un simple appel à la méthode Parse suffit désormais pour transformer une chaîne XML en un arbre XML !
Déplacements XML Les déplacements XML sont effectués par l’intermédiaire de 4 propriétés et de 11 méthodes. Dans cette section, nous allons nous efforcer d’utiliser le même code pour chacune des propriétés et des méthodes, en modifiant un simple argument chaque fois que cela sera possible. Le Listing 7.41 est un exemple de construction d’un document XML complet. Listing 7.41 : Le code dont seront dérivés les prochains exemples. // Définition d’une référence vers un des éléments de l’arbre XML XElement firstParticipant; XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine(xDocument);
La première ligne crée une référence au premier élément BookParticipant. Ceci afin d’avoir un élément par rapport auquel effectuer le déplacement (la variable firstParticipant n’est pas utilisée dans ce premier exemple, mais elle le sera dans les suivants).
Linq.book Page 222 Mercredi, 18. février 2009 7:58 07
222
LINQ to XML
Partie III
Le document est passé en argument de la méthode Console.WriteLine. Tout le contenu du document XML sera donc affiché. Dans les prochains exemples, nous choisirons un autre argument pour illustrer les différents types de déplacements. Voici le résultat : Joe Rattz Ewan Buckingham
Propriétés de déplacement Nous commencerons par les propriétés de déplacement primaires. Lorsqu’une direction (up, down, etc.) est spécifiée, elle est relative à l’élément sur lequel la méthode est appelée. Dans les exemples suivants, la référence au premier élément BookParticipant sera prise comme élément de base pour le déplacement. Nœud suivant avec XNode.NextNode La propriété NextNode obtient le nœud frère du nœud courant (voir Listing 7.42). Listing 7.42 : Obtention du nœud frère suivant d’un objet XElement avec la propriété NextNode. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine(firstParticipant.NextNode);
Linq.book Page 223 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
223
L’élément de base étant le premier élément BookParticipant, la propriété NextNode devrait renvoyer vers le deuxième élément BookParticipant. Voici le résultat : Ewan Buckingham
Nœud précédent avec XNode.PreviousNode La propriété PreviousNode donne accès au nœud frère précédent. Pour illustrer cette propriété, nous allons partir du nœud FirstParticipant. Nous lui appliquerons la propriété NextNode pour obtenir le nœud frère suivant puis la propriété PreviousNode pour obtenir le nœud frère précédent, c’est-à-dire… le nœud de départ (voir Listing 7.43). Listing 7.43 : Obtention du nœud frère précédent d’un objet XElement avec la propriété PreviousNode. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine(firstParticipant.NextNode.PreviousNode);
C’est bien le premier élément qui est affiché dans la console : Joe Rattz
Remonter au niveau du document avec XObject.Document Pour remonter au niveau du document à partir d’un XElement quelconque, il suffit d’utiliser la propriété Document (voir Listing 7.44, et en particulier l’appel à la méthode WriteLine).
Linq.book Page 224 Mercredi, 18. février 2009 7:58 07
224
LINQ to XML
Partie III
Listing 7.44 : Accès au document à partir d’un objet XElement en utilisant la propriété Document. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine(firstParticipant.Document);
Tout comme pour le Listing 7.41, ce code affiche la totalité du document : Joe Rattz Ewan Buckingham
Remonter d’un niveau avec XObject.Parent Pour obtenir l’élément parent d’un objet XElement, il vous suffit d’utiliser la propriété Parent (voir Listing 7.45, et en particulier l’appel à la méthode WriteLine). Listing 7.45 : Accès au parent de l’objet firstParticipant en utilisant la propriété Parent. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")),
Linq.book Page 225 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
225
new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine(firstParticipant.Parent);
Voici le résultat : Joe Rattz Ewan Buckingham
Ne vous laissez pas abuser : il s’agit non pas du document complet, mais du parent de l’objet firstParticipant. Remarquez l’absence du DTD et de l’instruction de transformation. Méthodes de déplacement Étant donné que les méthodes de déplacement retournent des séquences composées de plusieurs nœuds, l’instruction Console.WriteLine va être remplacée par une boucle foreach qui permettra d’afficher les différents nœuds : foreach(XNode node in firstParticipant.Nodes()) { Console.WriteLine(node); }
Dans les différents exemples, seule différera la méthode appliquée à l’objet firstParticipant dans la boucle foreach. Nœuds enfants avec XContainer.Nodes() La méthode Nodes() retourne une collection de nœuds enfants XNode de l’élément spécifié (voir Listing 7.46). À toutes fins utiles, nous rappelons qu’une séquence est un objet IEnumerable. Listing 7.46 : Accès aux enfants de l’objet firstParticipant en utilisant la propriété Nodes. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant",
Linq.book Page 226 Mercredi, 18. février 2009 7:58 07
226
LINQ to XML
Partie III
new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine(firstParticipant.Nodes());
Voici le résultat : Joe Rattz
Cette méthode retourne les éléments enfants (XElement), mais également les autres types de nœuds : commentaires (XComment), texte (XText), instructions de traitement (XProcessingInstruction), type de document (XDocumentType). En revanche, elle ne retourne pas les attributs puisque ces derniers ne sont pas des nœuds. Pour mieux illustrer la méthode Nodes(), plusieurs nœuds enfants ont été ajoutés à l’élément firstParticipant dans le Listing 7.47. Listing 7.47 : Accès aux différents types d’enfants de l’objet firstParticipant en utilisant la propriété Nodes. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("Nouvel auteur"), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XNode node in firstParticipant.Nodes()) { Console.WriteLine(node); }
Cet exemple est différent du précédent. Ici, l’élément firstParticipant a également un enfant de type XComment et un autre de type XProcessingInstruction. Voici le résultat affiché après l’appui sur Ctrl+F5 :
Linq.book Page 227 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
227
Joe Rattz
Le commentaire et l’instruction de traitement sont également affichés. Nous allons maintenant vous montrer comment limiter la sortie à un seul type de nœud en utilisant l’opérateur OfType (voir Chapitre 4). Le Listing 7.48 ne retourne que les nœuds de type élément. Il a suffi pour cela de changer l’argument de la boucle foreach. Listing 7.48 : Utilisation de l’opérateur OfType pour ne retourner que les éléments. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("Nouvel auteur"), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XNode node in firstParticipant.Nodes().OfType()) { Console.WriteLine(node); }
Bien que les nœuds de type XComment et XProcessingInstruction soient implémentés dans ce code, ils n’apparaissent pas dans les résultats : Joe Rattz
Vous commencez certainement à comprendre à quel point les nouvelles caractéristiques du langage C# et le langage LINQ vont faciliter les choses. N’est-il pas intéressant de pouvoir utiliser les opérateurs de requête standard pour restreindre les nœuds XML renvoyés par une méthode LINQ to XML ? Supposons maintenant que vous ne vouliez obtenir que les commentaires enfants de l’élément firstParticipant. Il vous suffit d’utiliser une autre variante de l’opérateur OfType, comme dans le Listing 7.49.
Linq.book Page 228 Mercredi, 18. février 2009 7:58 07
228
LINQ to XML
Partie III
Listing 7.49 : Utilisation de l’opérateur OfType pour ne retourner que les éléments. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("Nouvel auteur"), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XNode node in firstParticipant.Nodes().OfType()) { Console.WriteLine(node); }
Voici le résultat :
Que diriez-vous d’utiliser l’opérateur OfType pour limiter la sortie aux attributs ? Eh bien, ceci est tout bonnement impossible puisque, selon l’API LINQ to XML, les attributs ne sont pas des nœuds de l’arbre XML. Ils consistent en une séquence de paires nom/valeur attachée à un élément. Pour obtenir les attributs de l’objet firstParticipant, le code doit être modifié comme dans le Listing 7.50. Listing 7.50 : Accès aux attributs d’un élément avec la méthode Attributes. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("Nouvel auteur"), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));
Linq.book Page 229 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
229
foreach (XAttribute attr in firstParticipant.Attributes()) { Console.WriteLine(attr); }
Comme vous pouvez le voir, nous avons changé l’argument de la boucle foreach, mais également le type de la variable d’énumération, puisque XAttribute n’hérite pas de XNode. Voici le résultat : type="Author"
Nœuds enfants avec XContainer.Elements() L’API LINQ to XML étant centrée sur les éléments, Microsoft a défini la méthode Elements() pour retourner une collection constituée des éléments enfants d’un élément.
Le Listing 7.51 donne un exemple d’utilisation de cette méthode. Tout en utilisant une autre technique, il est cependant équivalent au Listing 7.48. Listing 7.51 : Accès aux éléments enfants d’un élément avec la méthode Elements. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XNode node in firstParticipant.Elements()) { Console.WriteLine(node); }
Ce code affiche le même résultat que le Listing 7.48 : Joe Rattz
Il existe également une version surchargée de la méthode Elements qui permet de passer le nom de l’élément recherché (voir Listing 7.52).
Linq.book Page 230 Mercredi, 18. février 2009 7:58 07
230
LINQ to XML
Partie III
Listing 7.52 : Accès aux éléments enfants d’un élément nommé avec la méthode XContainer.Elements. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XContainer.Elements(FirstName) { Console.WriteLine(node); }
Voici le résultat : Joe
Premier nœud enfant avec XContainer.Element() La méthode Element retourne le premier élément enfant de l’élément passé en argument. Contrairement à la méthode précédende, c’est non pas une séquence qui est retournée, mais un élément unique (voir Listing 7.53). Listing 7.53 : Accès au premier élément enfant d’un élément nommé avec la méthode Element. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"),
Linq.book Page 231 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
231
new XElement("LastName", "Buckingham")))); Console.WriteLine(firstParticipant.Element("FirstName"));
Voici le résultat : Joe
Ancêtres d’un nœud avec XNode.Ancestors() La propriété Parent permet d’obtenir l’ancêtre direct (le parent) d’un nœud. Si vous désirez obtenir une séquence contenant tous les ancêtres d’un nœud, jusqu’au niveau hiérarchique le plus élevé, vous utiliserez la méthode Ancestors. Seuls les éléments (et non tous les nœuds) ancêtres sont retournés.
Pour mieux illustrer cette méthode, nous allons ajouter plusieurs nœuds enfants à l’élément FirstName du premier participant. Par ailleurs, plutôt qu’énumérer les ancêtres du premier participant, nous utiliserons la méthode Element pour nous déplacer de deux niveaux hiérarchiques vers le bas afin d’atteindre l’élément NickName. Le nombre d’ancêtres sera ainsi plus élevé, ce qui facilitera la compréhension de la méthode Ancestors (voir Listing 7.54). Listing 7.54 : Ancêtres d’un objet XElement avec la méthode Ancestors. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", new XText("Joe"), new XElement("NickName", "Joey")), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XElement element in firstParticipant. Element("FirstName").Element("NickName").Ancestors()) { Console.WriteLine(element.Name); }
Comme vous pouvez le voir, un objet XText initialisé à "Joe" et un XElement nommé NickName ont été ajoutés à l’élément FirstName. Le dernier bloc d’instructions recherche les ancêtres de l’élément NickName. La boucle foreach est exécutée au niveau
Linq.book Page 232 Mercredi, 18. février 2009 7:58 07
232
LINQ to XML
Partie III
XElement (et non XNode). Ainsi, l’instruction WriteLine peut accéder à la propriété Name des éléments retournés. Plutôt qu’afficher le code XML de chaque élément ancêtre, nous nous contenterons d’afficher leur nom. Ceci uniquement dans un souci de clarté.
Voici les résultats : FirstName BookParticipant BookParticipants
Ancêtres d’un nœud avec XElement.AncestorsAndSelf() Cette méthode est comparable à la méthode Ancestors, mais ses résultats incluent l’élément sur lequel s’effectue la recherche. Le Listing 7.55 est le même que le précédent, à ceci près que la méthode AncestorsAndSelf remplace la méthode Ancestors. Listing 7.55 : Ancêtres d’un objet XElement avec la méthode AncestorsAndSelf. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", new XText("Joe"), new XElement("NickName", "Joey")), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XElement element in firstParticipant. Element("FirstName").Element("NickName").AncestorsAndSelf()) { Console.WriteLine(element.Name); }
Les résultats sont identiques à ceux du listing précédent mais, cette fois, ils incluent l’élément NickName : NickName FirstName BookParticipant BookParticipants
Linq.book Page 233 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
233
Descendants d’un nœud avec XContainer.Descendants() Pour obtenir une séquence contenant tous les éléments descendant d’un nœud, vous utiliserez la méthode Descendants. Vous pouvez également utiliser la méthode DescendantNodes pour obtenir tous les nœuds descendant d’un autre nœud. Le Listing 7.56 est le même que le précédent mais, ici, c’est la méthode Descendants qui est appelée. Listing 7.56 : Descendants d’un objet XElement avec la méthode Descendants. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", new XText("Joe"), new XElement("NickName", "Joey")), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XElement element in firstParticipant.Descendants()) { Console.WriteLine(element.Name); }
Voici les résultats : FirstName NickName LastName
Tous les éléments qui descendent de l’élément firstParticipant, mais pas les autres types de nœuds, sont bien listés dans la console. Descendants d’un nœud avec XElement.DescendantsAndSelf() DescendantsAndSelf est le pendant de AncestorsAndSelf. Cette méthode renvoie les descendants de l’élément sur lequel porte la requête, en y incluant cet élément. Le Listing 7.57 est le même que le précédent, à ceci près que la méthode DescendantsAndSelf remplace la méthode Descendants. Listing 7.57 : Descendants d’un objet XElement avec la méthode DescendantsAndSelf. XElement firstParticipant; // Le document complet
Linq.book Page 234 Mercredi, 18. février 2009 7:58 07
234
LINQ to XML
Partie III
XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", new XText("Joe"), new XElement("NickName", "Joey")), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XElement element in firstParticipant.DescendantsAndSelf()) { Console.WriteLine(element.Name); }
Les résultats incluent désormais le nom de l’élément firstParticipant : BookParticipant FirstName NickName LastName
Nœuds frères suivants avec XNode.NodesAfterSelf() Pour illustrer cet exemple, deux commentaires ont été ajoutés à l’élément BookParticipants. Les commentaires XComment étant des nœuds et non des éléments, les résultats mettront en évidence que la méthode NodesAfterSelf retourne tous les types de nœuds frères du nœud ciblé (voir Listing 7.58). Listing 7.58 : Nœuds frères d’un objet XNode avec la méthode NodesAfterSelf. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", new XComment("Début de la liste"), firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")), new XComment("Fin de la liste")));
Linq.book Page 235 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
235
foreach (XNode node in firstParticipant.NodesAfterSelf()) { Console.WriteLine(node); }
Les nœuds ajoutés sont tous deux frères des deux éléments BookParticipant. Cette modification du document XML concerne les exemples des méthodes NodesAfterSelf, ElementsAfterSelf, NodesBeforeSelf et ElementsBeforeSelf. Tous les nœuds frères situés après le premier nœud BookParticipant sont énumérés. Voici le résultat : Ewan Buckingham
Comme vous le voyez, le dernier commentaire est inclus dans le résultat. C’est en effet un nœud frère du nœud situé après le premier BookParticipant. Il se trouve au même niveau hiérarchique que les éléments BookParticipant. Si les éléments FirstName et LastName sont affichés dans les résultats, c’est parce que la méthode ToString est appliquée au nœud BookParticipant. Cette méthode ne se limite pas aux éléments. Elle retourne également les autres types de nœuds. Si vous voulez filtrer les nœuds retournés à un certain type, utilisez l’opérateur TypeOf. Si ce ne sont que les éléments qui vous intéressent, utilisez la méthode ElementsAfterSelf (voir section suivante). Éléments frères suivants avec XNode.ElementsAfterSelf() Nous utiliserons le même document XML que dans l’exemple précédent. Pour ne retenir que les éléments frères qui suivent le nœud référencé, la méthode ElementsAfterSelf est appelée (voir Listing 7.59). Listing 7.59 : Éléments frères qui suivent le nœud référencé avec la méthode ElementsAfterSelf. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", new XComment("Début de la liste"), firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"),
Linq.book Page 236 Mercredi, 18. février 2009 7:58 07
236
LINQ to XML
Partie III
new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")), new XComment("Fin de la liste"))); foreach (XNode node in firstParticipant.ElementsAfterSelf()) { Console.WriteLine(node); }
Voici le résultat : Ewan Buckingham
Cette fois-ci, étant donné que le commentaire n’est pas un élément, il est exclu du résultat. Nous rappelons que les éléments FirstName et LastName sont affichés dans les résultats car la méthode ToString est appliquée au nœud BookParticipant. Nœuds frères précédents avec XNode.NodesBeforeSelf() Cet exemple utilise le même document XML que le Listing 7.58. NodesBeforeSelf se comporte comme NodesAfterSelf, si ce n’est qu’elle retourne les nœuds frères qui précèdent le nœud référencé. Dans cet exemple, nous invoquons la méthode NextNode avant d’appeler NodesBeforeSelf pour que le résultat ne soit pas vide (voir Listing 7.60). Listing 7.60 : Nœuds frères qui précèdent le nœud référencé avec la méthode NodesBeforeSelf. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", new XComment("Début de la liste"), firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")), new XComment("Fin de la liste"))); foreach (XNode node in firstParticipant.NextNode.NodesBeforeSelf()) { Console.WriteLine(node); }
Linq.book Page 237 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
237
La méthode NextNode donne accès au deuxième participant. En lui appliquant la méthode ElementsBeforeSelf, les éléments frères qui précèdent le deuxième participant sont listés. Ici, le premier participant. Voici le résultat : Joe Ratz
Comme vous pouvez le voir, les nœuds frères sont listés dans l’ordre du document. On aurait pu s’attendre à ce que les nœuds soient listés depuis le nœud courant vers le début du document. Nous aurions alors appelé l’opérateur Reverse ou InDocumentOrder (voir chapitre suivant) pour rétablir l’ordre adéquat. Mais il n’en est rien. Une fois encore, ne soyez pas perturbé si les éléments FirstName et LastName font partie des résultats. Ils ne sont pas retournés par la méthode NodesBeforeSelf, mais proviennent de la méthode ToString, appliquée au nœud BookParticipant par la méthode Console.WriteLine. Éléments frères précédents avec XNode.ElementsBeforeSelf() Cet exemple utilise le même document XML que le Listing 7.58. ElementsBeforeSelf se comporte comme ElementsAfterSelf, si ce n’est qu’elle retourne les éléments frères qui précèdent le nœud référencé. Dans cet exemple, nous invoquons la méthode NextNode avant d’appeler NodesBeforeSelf pour que le résultat ne soit pas vide (voir Listing 7.61). Listing 7.61 : Éléments frères qui précèdent le nœud référencé avec la méthode ElementsBeforeSelf. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", new XComment("Début de la liste"), firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")), new XComment("Fin de la liste"))); foreach (XNode node in firstParticipant.NextNode.ElementsBeforeSelf()) { Console.WriteLine(node); }
Linq.book Page 238 Mercredi, 18. février 2009 7:58 07
238
LINQ to XML
Partie III
La méthode NextNode donne accès au deuxième participant. En lui appliquant la méthode ElementsBeforeSelf, les éléments frères qui précèdent le deuxième participant sont listés : ici, le premier participant. Bien entendu, le commentaire n’est pas affiché puisqu’il ne s’agit pas d’un élément : Joe Ratz
Modification de données XML Avec l’API LINQ to XML, la modification de données XML est un vrai jeu d’enfant : il suffit d’utiliser les méthodes dédiées pour ajouter, modifier ou supprimer les nœuds ou les éléments de votre choix. Comme il a été dit auparavant, LINQ to XML travaille essentiellement avec des objets de type XElement. C’est la raison pour laquelle la plupart des exemples qui vont suivre concerneront ce type d’objet. Nous nous intéresserons aux classes qui héritent de XNode, puis aux attributs. Ajout de nœuds Les différentes méthodes étudiées dans cette section utiliseront l’arbre défini dans le Listing 7.62. Listing 7.62 : L’arbre de base contient un seul participant. // Un document incluant un seul participant XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(xDocument);
Ce code définit un arbre XML contenant un seul participant : Joe Rattz
INFO Tous les exemples de cette section sont également utilisables avec les classes LINQ to XML qui héritent de la classe XNode.
Linq.book Page 239 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
239
En complément des méthodes passées en revue dans cette section, vous pouvez également vous reporter à la section intitulée "XElement.SetElementValue() sur des objets enfants de XElement", un peu plus loin dans ce chapitre.
XContainer.Add() Pour ajouter des nœuds à un arbre XML, vous utiliserez essentiellement cette méthode. Elle ajoute un nœud après le dernier nœud enfant du nœud spécifié (voir Listing 7.63). Listing 7.63 : Ajout d’un nœud après le dernier nœud enfant du nœud spécifié avec Add. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))); xDocument.Element("BookParticipants").Add( new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))); Console.WriteLine(xDocument);
Le code en gras a été ajouté au Listing 7.62 pour insérer un élément BookParticipant aux éléments BookParticipant déjà existants. Appliquée au document, la méthode Element renvoie l’élément BookParticipants. Il suffit alors d’utiliser la méthode Add pour lui ajouter un élément BookParticipant. Voici le résultat : Joe Rattz Ewan Buckingham
La méthode Add a ajouté un nouvel élément BookParticipant à la fin des nœuds enfants de l’élément BookParticipants. Elle est aussi flexible que le constructeur XElement et autorise la construction fonctionnelle.
XContainer.AddFirst() Pour ajouter un nœud en première position des nœuds enfants du nœud spécifié, vous utiliserez la méthode AddFirst. Le code utilisé est le même que dans l’exemple précédent mais, ici, la méthode appelée est AddFirst (voir Listing 7.64).
Linq.book Page 240 Mercredi, 18. février 2009 7:58 07
240
LINQ to XML
Partie III
Listing 7.64 : Ajout d’un nœud avant le nœud enfant du nœud spécifié avec AddFirst. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); xDocument.Element("BookParticipants").AddFirst( new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))); Console.WriteLine(xDocument);
Comme on pouvait s’y attendre, le nouvel élément BookParticipant est ajouté devant les nœuds enfants de l’élément BookParticipants : Ewan Buckingham Joe Rattz
XNode.AddBeforeSelf() Pour insérer un nœud à un emplacement bien défini dans une liste de nœuds enfants, vous devez obtenir la référence du nœud devant lequel ou après lequel doit se faire l’insertion, puis appeler la méthode AddBeforeSelf ou AddAfterSelf. Nous utiliserons l’arbre XML du Listing 7.63 comme point de départ, et nous ajouterons un nouveau nœud entre les deux éléments BookParticipant existants. Pour ce faire, il est nécessaire d’obtenir la référence du deuxième élément BookParticipant, comme illustré dans le Listing 7.65. Listing 7.65 : Ajout d’un nœud à l’emplacement spécifié avec AddBeforeSelf. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); xDocument.Element("BookParticipants").Add( new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")));
Linq.book Page 241 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
241
xDocument.Element("BookParticipants"). Elements("BookParticipant"). Where(e => ((string)e.Element("FirstName")) == "Ewan"). Single().AddBeforeSelf( new XElement("BookParticipant", new XAttribute("type", "Technical Reviewer"), new XElement("FirstName", "Fabio"), new XElement("LastName", "Ferracchiati"))); Console.WriteLine(xDocument);
Nous allons définir la référence à l’élément BookParticipant en utilisant un opérateur LINQ. Cela nous permettra de faire un rappel sur les opérateurs de requête standard introduits au Chapitre 2 et de les utiliser. Dans la première ligne en gras, la méthode Element, appliquée à l’élément BookParticipants, permet d’accéder aux éléments qui la constituent. Les trois lignes suivantes sélectionnent l’élément BookParticipant dont l’élément enfant FirstName vaut "Ewan". Un seul élément satisfaisant ce critère et étant donné que le nouvel élément doit être inséré avant l’élément courant, nous utilisons la méthode AddBeforeSelf. L’opérateur Single retourne l’objet XElement BookParticipant. C’est la référence utilisée pour insérer le nouveau XElement. Dans l’opérateur Where, l’élément FirstName est converti en une chaîne. La fonctionnalité d’extraction de valeur de LINQ sera ainsi mise à contribution pour comparer la valeur de l’élément à la chaîne "Ewan". Une fois la référence à l’élément BookParticipant obtenue, il ne reste plus qu’à appeler la méthode AddBeforeSelf pour effectuer l’insertion. Voici les résultats : Joe Rattz Fabio Ferracchiati Ewan Buckingham
Le nouvel élément BookParticipant a bien été inséré avant l’élément BookParticipant dont l’élément FirstName vaut "Ewan".
XNode.AddAfterSelf() Dans l’exemple précédent, nous utilisions toute une gymnastique pour accéder au second élément BookParticipant. Ici, nous nous contenterons d’obtenir une référence
Linq.book Page 242 Mercredi, 18. février 2009 7:58 07
242
LINQ to XML
Partie III
au premier élément BookParticipant en utilisant la méthode Element et de la faire suivre d’un nouvel élément BookParticipant en utilisant la méthode AddAfterSelf. Listing 7.66 : Ajout d’un nœud à l’emplacement spécifié avec AddBeforeSelf. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); xDocument.Element("BookParticipants").Add( new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))); xDocument.Element("BookParticipants"). Element("BookParticipant").AddAfterSelf( new XElement("BookParticipant", new XAttribute("type", "Technical Reviewer"), new XElement("FirstName", "Fabio"), new XElement("LastName", "Ferracchiati"))); Console.WriteLine(xDocument);
Voici le résultat : Joe Rattz Fabio Ferracchiati Ewan Buckingham
Suppression de nœuds Deux méthodes permettent de supprimer des nœuds : Remove et RemoveAll. En complément des méthodes passées en revue dans cette section, vous pouvez également vous reporter à la section "XElement.SetElementValue() sur des objets enfants de XElement", un peu plus loin dans ce chapitre.
XNode.Remove() Cette méthode permet de supprimer un nœud quelconque dans un arbre XML, ainsi que ses éventuels nœuds enfants et attributs. Dans ce premier exemple, nous allons construire un arbre XML et mémoriser la référence au premier élément BookParticipant, en
Linq.book Page 243 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
243
utilisant la même technique que dans la section précédente. L’arbre XML sera affiché après la construction et avant toute suppression. Le premier élément BookParticipant sera alors supprimé et l’arbre XML, à nouveau affiché (voir Listing 7.67). Listing 7.67 : Suppression d’un nœud avec la méthode Remove. // L’objet firstParticipant sera utilisé pour mémoriser un élément dans l’arbre XML XElement firstParticipant; Console.WriteLine(System.Environment.NewLine + "Avant la suppression du nœud"); XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine(xDocument); firstParticipant.Remove(); Console.WriteLine(System.Environment.NewLine + " Après la suppression du nœud"); Console.WriteLine(xDocument);
Voici le résultat : Avant la suppression du nœud Joe Rattz Ewan Buckingham Après la suppression du nœud Ewan Buckingham
Le premier élément BookParticipant a bien été supprimé.
IEnumerable.Remove() où T est un XNode Dans l’exemple précédent, la méthode Remove a été appliquée à un seul nœud. Si nécessaire, il est également possible de l’appliquer à une séquence ( IEnumerable). Dans le Listing 7.68, la méthode Descendants est utilisée pour parcourir l’arbre XML.
Linq.book Page 244 Mercredi, 18. février 2009 7:58 07
244
LINQ to XML
Partie III
Elle est combinée à un opérateur Where, et seuls sont retournés les éléments dont le nom est FirstName. La méthode Remove est enfin appelée sur cette séquence. Listing 7.68 : Suppression d’une séquence de nœuds avec la méthode Remove. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); xDocument.Descendants().Where(e => e.Name == "FirstName").Remove(); Console.WriteLine(xDocument);
La méthode XDocument.Descendants retourne les nœuds enfants d’une séquence. L’opérateur de requête standard Where est alors appelé pour filtrer les nœuds qui correspondent au critère de sélection (ici, le nom du nœud doit être FirstName). La séquence retournée est alors passée à la méthode Remove pour supprimer les nœuds correspondants. Voici le résultat : Rattz Buckingham
Comme vous pouvez le voir, tous les nœuds FirstName ont été supprimés.
XElement.RemoveAll() Il est parfois nécessaire de supprimer le contenu d’un élément, mais pas l’élément luimême. Vous utiliserez pour cela la méthode RemoveAll (voir Listing 7.69). Listing 7.69 : Suppression du contenu d’un nœud avec RemoveAll. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine(System.Environment.NewLine + "Avant la suppression du contenu");
Linq.book Page 245 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
245
Console.WriteLine(xDocument); xDocument.Element("BookParticipants").RemoveAll(); Console.WriteLine(System.Environment.NewLine + "Après la suppression du contenu"); Console.WriteLine(xDocument);
Le document est affiché avant la suppression du contenu du nœud BookParticipants. Le contenu de ce nœud est alors supprimé puis le document est à nouveau affiché. Voici les résultats : Avant la suppression du contenu Joe Rattz Ewan Buckingham Après la suppression du contenu
Mise à jour de nœuds Plusieurs des classes de XNode, comme XElement, XText et XComment, ont une propriété Value qui peut être directement modifiée. D’autres, telles que XDocumentType et XProcessingInstruction, ont des propriétés spécifiques qui peuvent être modifiées. Les méthodes XElement.SetElementValue et XContainer.ReplaceAll (voir un peu plus loin dans ce chapitre) peuvent également être appelées pour modifier la valeur des éléments.
XElement.Value, XText.Value et XComment.Value Pour modifier la valeur d’un nœud XElement, XText et XComment, il suffit de modifier la propriété Value des sous-classes de XNode correspondantes (voir Listing 7.70). Listing 7.70 : Mise à jour de la valeur d’un nœud. // Définition d’une référence sur un élément de l’arbre XML XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("Nouvel auteur"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine("Avant la modification des nœuds"); Console.WriteLine(xDocument); // Modification d’un élément, d’un commentaire et d’un nœud texte firstParticipant.Element("FirstName").Value = "Joey";
Linq.book Page 246 Mercredi, 18. février 2009 7:58 07
246
LINQ to XML
Partie III
firstParticipant.Nodes().OfType().Single().Value = "Auteur du livre Pro LINQ en C# 2008"; ((XElement)firstParticipant.Element("FirstName").NextNode) .Nodes().OfType().Single().Value = "Rattz, Jr."; Console.WriteLine("Après la modification des nœuds"); Console.WriteLine(xDocument);
L’élément FirstName puis le commentaire sont modifiés en utilisant la propriété Value correspondante. L’élément LastName est ensuite modifié par l’intermédiaire de la propriété Value de son enfant XText. Cet exemple montre à quel point LINQ to XML est flexible lorsqu’il s’agit d’accéder aux objets à modifier. Bien entendu, il n’est pas nécessaire de passer par l’enfant XText de l’élément LastName pour modifier sa valeur. Le chemin de traverse emprunté par ce code n’a qu’un but démonstratif. Voici le résultat : Avant la mise à jour des nœuds Joe Rattz Après la mise à jour des nœuds Joey Rattz, Jr.
Les valeurs des nœuds ont bien été mises à jour. Les propriétés XDocumentType.Name, XDocumentType.PublicId, XDocumentType.SystemId et XDocumentType.InternalSubset Pour modifier les valeurs relatives à la définition de type de document (DTD), vous utiliserez quatre propriétés de la classe XDocumentType (voir Listing 7.71). Listing 7.71 : Modification de la définition de type de document. // Définition d’une référence sur le type de document pour un usage futur XDocumentType docType; XDocument xDocument = new XDocument( docType = new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XElement("BookParticipants")); Console.WriteLine("Avant la mise à jour du DTD"); Console.WriteLine(xDocument); docType.Name = "MyBookParticipants"; docType.SystemId = "http://www.somewhere.com/DTDs/MyBookParticipants.DTD"; docType.PublicId = "-//DTDs//TEXT Book Participants//EN";
Linq.book Page 247 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
247
Console.WriteLine("Après la mise à jour du DTD"); Console.WriteLine(xDocument);
Voici les résultats : Avant la mise à jour du DTD Après la mise à jour du DTD
XProcessingInstruction.Target sur les objets XProcessingInstruction Objects et XProcessingInstruction.Data sur les objets XProcessingInstruction Pour modifier la valeur d’une instruction de traitement, il suffit de modifier les propriétés Target et Data de l’objet XProcessingInstruction (voir Listing 7.72). Listing 7.72 : Mise à jour d’une instruction de traitement. // Définition d’une référence pour un usage futur XProcessingInstruction procInst; XDocument xDocument = new XDocument( new XElement("BookParticipants"), procInst = new XProcessingInstruction("BookCataloger", "out-of-print")); Console.WriteLine("Avant la modification de l’instruction de traitement"); Console.WriteLine(xDocument); procInst.Target = "BookParticipantContactManager"; procInst.Data = "update"; Console.WriteLine("Après la modification de l’instruction de traitement"); Console.WriteLine(xDocument);
Voici le résultat de ce code : Avant la modification de l’instruction de traitement Après la modification de l’instruction de traitement
XElement.ReplaceAll() La méthode ReplaceAll permet de remplacer l’arbre XML relatif à un élément. Il est possible de passer une simple valeur – une chaîne ou un nombre, par exemple – ou, si une méthode surchargée accepte des objets multiples via le mot-clé params, une portion d’arbre. La méthode ReplaceAll remplace également les attributs. Le Listing 7.73 donne un exemple d’utilisation de cette méthode.
Linq.book Page 248 Mercredi, 18. février 2009 7:58 07
248
LINQ to XML
Partie III
Listing 7.73 : Utilisation de la méthode ReplaceAll pour modifier l’arbre relatif à un élément. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(System.Environment.NewLine + "Avant la modification"); Console.WriteLine(xDocument); firstParticipant.ReplaceAll( new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")); Console.WriteLine(System.Environment.NewLine + "Après la modification"); Console.WriteLine(xDocument);
Les instructions en gras modifient l’arbre de l’élément firstParticipant. Comme vous pouvez le voir, l’attribut type n’a pas été spécifié. Voici le résultat : Avant la modification Joe Rattz Après la modification Ewan Buckingham
Bien que les attributs ne soient pas des nœuds enfants des éléments, la méthode ReplaceAll a été en mesure de supprimer l’attribut type de l’arbre XML.
XElement.SetElementValue() sur des objets enfants de XElement Cette méthode est très puissante. Elle permet d’ajouter, de modifier et de supprimer les éléments enfants de l’élément sur lequel elle est appelée. Cette méthode admet deux paramètres : le nom de l’élément enfant à atteindre et la valeur qui doit lui être affectée. Si un enfant portant ce nom est trouvé, et si la valeur passée est différente de null, l’enfant est mis à jour. Si la valeur passée vaut null, l’enfant est supprimé. Si aucun enfant portant ce nom n’est trouvé, il est créé et la valeur spécifiée lui est affectée.
Linq.book Page 249 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
249
La méthode SetElementValue n’affecte que le premier élément enfant portant le nom spécifié. Si un ou plusieurs autres éléments enfants portent le même nom, ils ne sont pas affectés. Le Listing 7.74 donne un exemple des trois possibilités de cette méthode. Listing 7.74 : Utilisation de SetElementValue pour mettre à jour, ajouter et supprimer des éléments enfants. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(System.Environment.NewLine + "Avant la mise à jour des éléments"); Console.WriteLine(xDocument); // Mise à jour de la valeur d’un élément // L’élément enfant FirstName étant trouvé, sa valeur sera initialisée à Joseph firstParticipant.SetElementValue("FirstName", "Joseph"); // Ajout d’un élément // L’élément enfant MiddleInitial n’étant pas trouvé, il est créé firstParticipant.SetElementValue("MiddleInitial", "C"); // Suppression d’un élément // La valeur de l’élément étant initialisée à null, l’élément est supprimé firstParticipant.SetElementValue("LastName", null); Console.WriteLine(System.Environment.NewLine + "Après la mise à jour des éléments"); Console.WriteLine(xDocument);
Dans un premier temps, la méthode SetElementValue est appelée sur l’élément enfant FirstName de l’élément firstParticipant. Comme un élément portant ce nom existe, sa valeur est mise à jour. Dans un deuxième temps, la méthode SetElementValue est appelée sur l’élément enfant MiddleInitial de l’élément firstParticipant. Comme aucun élément portant ce nom n’existe, il est créé. Enfin, dans un troisième temps, la méthode SetElementValue est appelée sur l’élément enfant LastName de l’élément firstParticipant. La valeur null étant passée dans le deuxième argument de la méthode, l’élément LastName est supprimé. Voici les résultats : Avant la mise à jour des éléments Joe Rattz Après la mise à jour des éléments
Linq.book Page 250 Mercredi, 18. février 2009 7:58 07
250
LINQ to XML
Partie III
Joseph C
L’élément FirstName a été mis à jour, l’élément MiddleInitial a été créé et l’élément LastName, supprimé. ATTENTION Lorsque la méthode SetElementValue est appelée avec un deuxième argument ayant pour valeur null, elle supprime l’élément spécifié dans le premier argument. Que ceci ne vous fasse pas croire qu’il suffise d’initialiser un élément avec la valeur null pour le supprimer d’un arbre XML. Si vous tentez de le faire en agissant sur sa propriété Value, une exception sera levée.
Attributs XML Lorsque l’on utilise l’API LINQ to XML, les attributs sont implémentés dans la classe XAttribute. Contrairement à ce qui avait cours dans l’API W3C XML DOM, ils n’héritent pas d’un nœud. Ils n’ont donc aucune relation d’héritage avec les éléments. Et, pourtant, grâce à l’API LINQ to XML, ils sont tout aussi simples à utiliser. Création d’un attribut Les attributs sont créés de la même manière que les éléments et que la plupart des autres classes LINQ to XML. Reportez-vous à la section "Création d’attributs avec XAttribute", au début de ce chapitre, pour en savoir plus à ce sujet. Déplacements dans un attribut Pour vous déplacer dans les attributs, vous utiliserez les propriétés XElement.FirstAttribute, XElement.LastAttribute, XAttribute.NextAttribute et XAttribute .PreviousAttribute et les méthodes XElement.Attribute et XElement.Attributes. Vous en saurez plus à leur sujet dans les prochaines pages. Premier attribut avec XElement.FirstAttribute Pour accéder au premier attribut d’un élément, vous pouvez utiliser la propriété FirstAttribute (voir Listing 7.75). Listing 7.75 : Accès au premier attribut d’un élément avec la propriété FirstAttribute. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant;
Linq.book Page 251 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
251
XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(firstParticipant.FirstAttribute);
Ce code produit le résultat suivant dans la console : type="Author"
Attribut suivant avec XAttribute.NextAttribute Pour accéder à l’attribut suivant, il suffit d’utiliser la propriété NextAttribute (voir Listing 7.76). Listing 7.76 : Accès à l’attribut suivant avec la propriété NextAttribute. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(firstParticipant.FirstAttribute.NextAttribute);
Avant d’utiliser la propriété NextAttribute, la propriété FirstAttribute a été appliquée à l’élément firstParticipant pour obtenir une référence au premier attribut de l’élément. Voici le résultat : experience="first-time"
Si la propriété NextAttribute d’un attribut a pour valeur null, cela signifie qu’il s’agit du dernier attribut de l’élément. Attribut précédent avec XAttribute.PreviousAttribute Pour accéder à l’attribut précédent, il suffit d’utiliser la propriété PreviousAttribute (voir Listing 7.77). Listing 7.77 : Accès à l’attribut précédent avec la propriété PreviousAttribute. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant;
Linq.book Page 252 Mercredi, 18. février 2009 7:58 07
252
LINQ to XML
Partie III
XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(firstParticipant.FirstAttribute.NextAttribute.PreviousAttribute);
Les propriétés FirstAttribute et NextAttribute ont été chaînées pour obtenir une référence au deuxième attribut de l’élément firstParticipant. En appliquant la propriété PreviousAttribute, l’attribut pointé est donc le premier. Voici le résultat : type="Author"
Si la propriété PreviousAttribute d’un attribut vaut null, cela signifie qu’il a été appliqué au premier attribut de l’élément. Dernier attribut avec XElement.LastAttribute Pour accéder au dernier attribut d’un élément, vous utiliserez la propriété LastAttribute (voir Listing 7.78). Listing 7.78 : Accès au dernier attribut avec la propriété LastAttribute. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage ➥futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(firstParticipant.LastAttribute);
L’instruction Writeln affiche le dernier attribut du XElement firstParticipant : language="English"
XElement.Attribute() S’il existe, cette méthode retourne le premier attribut dont le nom est passé en argument (voir Listing 7.79). Listing 7.79 : Accès à un attribut avec la méthode Attribute. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant;
Linq.book Page 253 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
253
XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(firstParticipant.Attribute("type").Value);
La méthode Attribute donne accès à l’attribut type. La valeur de cet attribut est alors affichée en utilisant la propriété Value. Voici le résultat : Author
À titre d’information, sachez que la valeur de l’attribut aurait également pu être obtenue en appliquant un casting de type string à l’attribut.
XElement.Attributes() La méthode Attributes() retourne tous les attributs de l’élément sur lequel elle est appliquée. Les attributs sont retournés sous la forme d’une séquence d’objets XAttribute (voir Listing 7.80). Listing 7.80 : Accès à tous les attributs d’un élément avec la méthode Attributes. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); foreach(XAttribute attr in firstParticipant.Attributes()) { Console.WriteLine(attr); }
Voici le résultat : type="Author" experience="first-time"
Modification d’attributs Comme il a été dit précédemment, les API W3C XML DOM et LINQ to XML manipulent les attributs d’une façon bien différente. Avec l’API W3C, les attributs sont des nœuds enfants du nœud dont ils sont l’attribut. Avec l’API LINQ to XML, les attributs
Linq.book Page 254 Mercredi, 18. février 2009 7:58 07
254
LINQ to XML
Partie III
sont des paires nom/valeur. Ils sont accessibles via la méthode Attributes ou la propriété FirstAttribute de l’élément. Il est important d’avoir cela en mémoire. Les méthodes et propriétés des attributs sont très proches de celles qui ont déjà été étudiées pour les éléments. Vous pouvez utiliser les méthodes suivantes pour ajouter un attribut à un élément : m
XElement.Add() ;
m
XElement.AddFirst() ;
m
XElement.AddBeforeThis() ;
m
XElement.AddAfterThis().
Ces méthodes ont déjà été illustrées dans la section "Ajout de nœuds", un peu plus tôt dans ce chapitre. Reportez-vous aux exemples de cette section pour voir comment ajouter des attributs. Consultez également la section relative à la méthode XElement.SetAttributeValue, un peu plus loin dans ce chapitre. Suppression d’attributs Pour supprimer un attribut, vous utiliserez la méthode XAttribute.Remove. Pour supprimer une séquence d’attributs, vous utiliserez la méthode IEnumerable.Remove.
Vous consulterez également la section XElement.SetAttributeValue(), un peu plus loin dans ce chapitre. XAttribute.Remove() Vous vous rappelez certainement que la méthode Remove de la classe XNode permettait de supprimer un nœud. Quant à elle, la méthode Remove de la classe XAttribute permet de supprimer un attribut (voir Listing 7.81). Listing 7.81 : Suppression d’un attribut. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(System.Environment.NewLine + "Avant la suppression de l’attribut"); Console.WriteLine(xDocument); firstParticipant.Attribute("type").Remove(); Console.WriteLine(System.Environment.NewLine + "Après la suppression de l’attribut"); Console.WriteLine(xDocument);
Linq.book Page 255 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
255
Dans cet exemple, nous utilisons la méthode Attribute pour obtenir la référence de l’attribut à supprimer. La méthode Remove est alors appliquée à cette référence. Voici le résultat : Avant la suppression de l’attribut Joe Rattz Après la suppression de l’attribut Joe Rattz
L’attribut type a bien été supprimé. IEnumerable.Remove() où T est un XNode Tout comme la méthode IEnumerable.Remove() de la classe XNode permet de supprimer une séquence de nœuds, la méthode IEnumerable.Remove() de la classe XAttribute permet de supprimer une séquence d’attributs (voir Listing 7.82). Listing 7.82 : Suppression de tous les attributs d’un élément. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(System.Environment.NewLine + "Avant la suppression des attributs"); Console.WriteLine(xDocument); firstParticipant.Attributes().Remove(); Console.WriteLine(System.Environment.NewLine + "Après la suppression des attributs"); Console.WriteLine(xDocument);
La méthode Attributes() retourne la séquence des attributs du XElement firstParticipant. La méthode Remove supprime cette séquence. Voici les résultats : Avant la suppression des attributs Joe Rattz
Linq.book Page 256 Mercredi, 18. février 2009 7:58 07
256
LINQ to XML
Partie III
Après la suppression des attributs Joe Rattz
Modification de la valeur des attributs Pour modifier la valeur d’un attribut, vous utiliserez la propriété XAttribute.Value (voir Listing 7.83). INFO Reportez-vous également à la section XElement.SetAttributeValue(), un peu plus loin dans ce chapitre. Listing 7.83 : Suppression de tous les attributs d’un élément. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(System.Environment.NewLine + "Avant la modification de la valeur de ➥l’attribut"); Console.WriteLine(xDocument); firstParticipant.Attribute("experience").Value = "beginner"; Console.WriteLine(System.Environment.NewLine + "Après la modification de la valeur
➥de l’attribut");
Console.WriteLine(xDocument);
La méthode Attribute a été utilisée pour obtenir une référence à l’attribut experience. La méthode Value a alors été appliquée à cette référence pour accéder à la valeur de l’attribut. Voici le résultat : Avant la modification de la valeur de l’attribut Joe Rattz
Linq.book Page 257 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
257
Après la modification de la valeur de l’attribut Joe Rattz
L’attribut experience avait pour valeur "first-time" avant l’exécution du code. Il a désormais pour valeur "beginner".
XElement.SetAttributeValue() La méthode SetAttributeValue est le pendant pour les attributs de la méthode SetElementValue. Tout aussi complète, elle permet d’ajouter, de supprimer et de modifier la valeur d’un attribut. Si un nom d’attribut inexistant lui est passé, cet attribut est ajouté à l’élément. Si un nom d’attribut existant ayant une valeur différente de null lui est passé, l’attribut est mis à jour avec la valeur passée. Enfin, si un nom d’attribut existant ayant la valeur null lui est passé, il est supprimé (voir Listing 7.84). Listing 7.84 : Suppression de tous les attributs d’un élément. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(System.Environment.NewLine + "Avant la modification des attributs"); Console.WriteLine(xDocument); // L’attribut "type" existe et le deuxième argument est différent de "null". // L’attribut "type" est donc mis à jour. firstParticipant.SetAttributeValue("type", "beginner"); // L’attribut "language" n’existe pas. Il est donc ajouté à l’élément. firstParticipant.SetAttributeValue("language", "English"); // L’attribut "experience" existe et le deuxième argument a pour valeur "null" // L’attribut "experience" est donc supprimé. firstParticipant.SetAttributeValue("experience", null); Console.WriteLine(System.Environment.NewLine + "Après la modification des attributs"); Console.WriteLine(xDocument);
Linq.book Page 258 Mercredi, 18. février 2009 7:58 07
258
LINQ to XML
Partie III
Ce code met à jour la valeur d’un attribut, définit un nouvel attribut et supprime un attribut existant. Voici les résultats : Avant la modification des attributs Joe Rattz Après la modification des attributs Joe Rattz
Annotations XML En utilisant les annotations de l’API LINQ to XML, il est possible d’associer une donnée utilisateur à une classe quelconque qui hérite de la classe XObject. Il est ainsi possible d’affecter une donnée quelconque (une clé supplémentaire, un objet qui parse les valeurs d’un élément) à un élément, à un document ou à un autre objet dont la classe est dérivée de XObject. Ajout d’annotations avec XObject.AddAnnotation() Voici le prototype de la méthode AddAnnotation() : void XObject.AddAnnotation(object annotation);
Accès aux annotations avec XObject.Annotation() ou
XObject.Annotations() Voici les prototypes de ces deux méthodes : object XObject.Annotation(Type type); T XObject.Annotation(); IEnumerable XObject.Annotations(Type type); IEnumerable XObject.Annotations();
ATTENTION Lorsque vous accédez à des annotations, veillez à passer le type actuel de l’objet, et non sa classe de base ou son interface. Sans quoi l’annotation ne serait pas trouvée.
Suppression d’annotations avec XObject.RemoveAnnotations() Voici les deux prototypes de la méthode RemoveAnnotations() : void XObject.RemoveAnnotations(Type type); void XObject.RemoveAnnotations();
Linq.book Page 259 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
259
Exemples d’annotations À titre d’exemple, nous allons définir un code qui ajoute, retrouve et supprime des annotations. Ici, nous utiliserons l’arbre XML désormais traditionnel BookParticipants. Nous allons associer un handler à chaque élément BookParticipant, en se basant sur son attribut type. Dans cet exemple, le handler affichera l’élément dans un format qui dépend de l’attribut type : un format pour les auteurs, un autre pour les éditeurs. Voici les classes handler utilisées (une pour les auteurs et une pour les éditeurs) : public class AuthorHandler { public void Display(XElement element) { Console.WriteLine("BIOGRAPHIE DE L’AUTEUR"); Console.WriteLine("--------------------------"); Console.WriteLine("Nom : {0} {1}", (string)element.Element("FirstName"), (string)element.Element("LastName")); Console.WriteLine("Langue : {0}", (string)element.Attribute("language")); Console.WriteLine("Expérience : {0}", (string)element.Attribute("experience")); Console.WriteLine("==========================" + System.Environment.NewLine); } } public class EditorHandler { public void Display(XElement element) { Console.WriteLine("BIOGRAPHIE DE L’EDITEUR"); Console.WriteLine("--------------------------"); Console.WriteLine("Nom: {0}", (string)element.Element("FirstName")); Console.WriteLine(" {0}", (string)element.Element("LastName")); Console.WriteLine("==========================" + System.Environment.NewLine); } }
Ce code définit deux classes au comportement distinct. Dans cet exemple, les données de l’élément sont affichées différemment. Bien entendu, le traitement pourrait être tout autre. Les annotations pourraient même ne pas être des handlers… Cet exemple étant plus complexe que les précédents, nous avons divisé le code en plusieurs sections. Chacune d’entre elles sera suivie d’explications (voir Listing 7.85). Listing 7.85 : Ajout, lecture et suppression d’annotations. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")),
Linq.book Page 260 Mercredi, 18. février 2009 7:58 07
260
LINQ to XML
Partie III
new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); // Affichage du document Console.WriteLine(xDocument + System.Environment.NewLine);
Ces quelques lignes de code définissent le document XML et l’affichent. Le bloc de code suivant énumère les participants. Pour chacun d’entre eux, un handler est instancié en fonction de l’attribut type et une annotation faisant référence à ce handler est ajoutée à l’élément. // Ajout d’annotations en fonction de la valeur de l’attribut type foreach(XElement e in xDocument.Element("BookParticipants").Elements()) { if((string)e.Attribute("type") == "Author") { AuthorHandler aHandler = new AuthorHandler(); e.AddAnnotation(aHandler); } else if((string)e.Attribute("type") == "Editor") { EditorHandler eHandler = new EditorHandler(); e.AddAnnotation(eHandler); } }
Après l’exécution de ce code, chaque élément BookParticipant possède une annotation qui référence un handler dont le code dépend de la valeur de l’attribut type. Nous allons maintenant énumérer les éléments BookParticipant, retrouver les annotations et exécuter les handlers associés. AuthorHandler aHandler2; EditorHandler eHandler2; foreach(XElement e in xDocument.Element("BookParticipants").Elements()) { if((string)e.Attribute("type") == "Author") { aHandler2 = e.GetAnnotation(); if(aHandler2 != null) { aHandler2.Display(e); } } else if((string)e.Attribute("type") == "Editor") { eHandler2 = e.GetAnnotation(); if(eHandler2 != null) { eHandler2.Display(e); } } }
Ce code exécute la méthode Display du handler associé à chaque élément. Le bloc de code suivant va supprimer les annotations de chaque élément : foreach(XElement e in xDocument.Element("BookParticipants").Elements()) {
Linq.book Page 261 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
261
if((string)e.Attribute("type") == "Author") { e.RemoveAnnotation(); } else if((string)e.Attribute("type") == "Editor") { e.RemoveAnnotation(); } }
Ce code est plus long que les précédents. Il est composé de quatre sections principales. La première section crée l’arbre XML et l’affiche. Ceci n’a rien d’exceptionnel, puisque nous l’avons déjà fait fréquemment dans les autres exemples de cet ouvrage. La deuxième section énumère les éléments BookParticipant et ajoute un handler en fonction de la valeur de leur attribut type. La troisième section énumère les éléments BookParticipant. En fonction de la valeur de leur attribut type, la méthode Display du handler correspondant est exécutée. Enfin, la quatrième section énumère les éléments BookParticipant et supprime les annotations. Dans les sections 2, 3 et 4 du code, l’accès aux attributs s’est fait via un casting au format string. C’est ainsi qu’il a été possible de les comparer aux valeurs "Author" et "Editor". Voici les résultats : Joe Rattz Ewan Buckingham BIOGRAPHIE AUTEUR -------------------------Nom : Joe Rattz Langue : English Expérience: first-time ========================== BIOGRAPHIE EDITEUR -------------------------Nom : Ewan Buckingham ==========================
Ce qu’il faut remarquer dans ces résultats, c’est que les deux handlers sont appelés en fonction de l’attribut type et via les annotations. Retenez également que les annotations peuvent être constituées d’objets quelconques, et pas seulement de handlers.
Linq.book Page 262 Mercredi, 18. février 2009 7:58 07
262
LINQ to XML
Partie III
Événements XML Grâce à l’API LINQ to XML, vous pouvez demander à être informé à tout moment de la modification des objets qui héritent de la classe XObject. Lorsque vous faites une telle demande auprès d’un objet, un événement sera levé si cet objet, ou un de ses descendants, est modifié. Cela signifie que si, par exemple, vous vous abonnez à un événement situé au niveau du document, toutes les modifications effectuées dans l’arbre provoqueront l’appel de la méthode à laquelle vous vous êtes abonné. C’est la raison pour laquelle vous ne devez faire aucune supposition sur le type de l’objet qui provoquera les événements. Lorsque la méthode de traitement est appelée, l’objet qui en est à l’origine est passé en tant qu’émetteur de l’événement. Son type est object. Faites attention lorsque vous lui appliquerez un opérateur de casting, lorsque vous accéderez à ses propriétés ou appellerez ses méthodes. Il se peut que son type ne corresponde pas à ce que vous attendez. Ceci sera illustré dans le Listing 7.86, où l’objet sera de type XText alors que l’on attend un type XElement. Sachez enfin que la construction d’un arbre XML ne génère aucun événement. Comment cela serait-il possible, puisque aucun événement ne peut être enregistré avant la construction de l’arbre ! Seule la modification ou la suppression d’un élément XML peut engendrer un événement, et seulement à condition que cet événement ait été enregistré.
XObject.Changing Cet événement est levé lorsqu’un objet qui hérite de XObject est sur le point d’être modifié. Pour vous abonner à l’événement, vous devez ajouter un objet de type EventHandler à l’événement Changing de l’objet : myobject.Changing += new EventHandler(MyHandler);
Le délégué doit avoir la signature suivante : void MyHandler(object sender, XObjectChangeEventArgs cea)
L’objet sender est celui qui est sur le point d’être modifié et qui provoque la levée de l’événement. La propriété ObjectChange de type XObjectChange de l’objet cea (Change Event Arguments) indique le type de changement qui est sur le point de survenir : XObjectChange.Add, XObjectChange.Name, XObjectChange.Remove ou XObjectChange.Value.
XObject.Changed Cet événement est levé lorsqu’un objet qui hérite de XObject a été modifié. Pour vous abonner à l’événement, vous devez ajouter un objet de type EventHandler à l’événement Changed de l’objet : myobject.Changed += new EventHandler(MyHandler);
Linq.book Page 263 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
263
Le délégué doit avoir la signature suivante : void MyHandler(object sender, XObjectChangeEventArgs cea)
L’objet sender est celui qui a été modifié et qui provoque la levée de l’événement. La propriété ObjectChange de type XObjectChange de l’objet cea (Change Event Arguments) indique le type de changement qui est sur le point de survenir : XObjectChange.Add, XObjectChange.Name, XObjectChange.Remove ou XObjectChange.Value. Quelques exemples d’événements Un exemple va vous aider à bien comprendre toute la logique mise en œuvre pour gérer les événements XObject. Avant d’entrer dans le vif du sujet, nous allons présenter les gestionnaires d’événements utilisés. Cette méthode est exécutée lorsque l’événement Changing d’un élément est levé. Elle permet d’être prévenu lorsqu’un élément est sur le point d’être modifié. public static void MyChangingEventHandler(object sender, XObjectChangeEventArgs cea) { Console.WriteLine("Type de l’objet qui va être modifié : {0}, Type du changement : {1}", sender.GetType().Name, cea.ObjectChange); }
Voici le gestionnaire utilisé pour générer un événement juste après qu’un élément a été modifié. Elle permet d’être prévenu lorsqu’un élément a été modifié. Cette méthode est exécutée lorsque l’événement Changed d’un élément est levé : public static void MyChangedEventHandler(object sender, XObjectChangeEventArgs cea) { Console.WriteLine("Type de l’objet qui a été modifié : {0}, Type du changement : {1}", sender.GetType().Name, cea.ObjectChange); }
Un peu plus tôt, j’ai indiqué qu’un événement serait levé si un descendant d’un objet auquel vous êtes abonné est modifié. Pour illustrer ce fait, nous allons définir une autre méthode que nous enregistrerons une fois le document modifié. Son unique but est de montrer que le document reçoit également un événement Changed, même s’il s’agit d’un descendant situé à plusieurs niveaux hiérarchiques de celui qui a été modifié. Cette méthode est exécutée lorsque l’événement Changed du document XML est levé : public static void DocumentChangedHandler(object sender, XObjectChangeEventArgs cea) { Console.WriteLine("Doc: Type de l’objet qui a été modifié : {0}, Type du changement : {1}{2}", sender.GetType().Name, cea.ObjectChange, System.Environment.NewLine); }
La seule différence entre les méthodes DocumentChangedHandler et MyChangedEventHandler se situe dans le début de l’affichage : l’affichage effectué dans DocumentChangedHandler débute par le terme "Doc:", afin de signaler que le gestionnaire est appelé par l’événement Changed du document, et non de l’élément. Examinons le code du Listing 7.86.
Linq.book Page 264 Mercredi, 18. février 2009 7:58 07
264
LINQ to XML
Partie III
Listing 7.86 : Le gestionnaire d’événements XObject. XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine("{0}{1}", xDocument, System.Environment.NewLine);
Rien de nouveau pour l’instant. Comme il a été fait à de nombreuses reprises dans les pages précédentes, un document XML a été créé en utilisant la construction fonctionnelle, puis affiché dans la console. Remarquez également qu’une référence au premier élément BookParticipant a été mémorisée. Les événements seront déclenchés par rapport à cet élément : firstParticipant.Changing += new EventHandler(MyChangingEventHandler); firstParticipant.Changed += new EventHandler(MyChangedEventHandler); xDocument.Changed += new EventHandler(DocumentChangedHandler);
Après l’exécution de ces lignes de code, un événement sera généré : m
juste avant (Changing) le changement du premier élément BookParticipant ;
m
juste après (Changed) le changement du premier élément BookParticipant ;
m
juste après (Changed) la modification du document.
Le dernier type d’événement a été mis en place pour prouver que des événements sont générés lorsqu’un objet decendant est modifié. Il ne reste plus qu’à effectuer une modification dans l’élément firstParticipant. firstParticipant.Element("FirstName").Value = "Seph"; Console.WriteLine("{0}{1}", xDocument, System.Environment.NewLine);
La première ligne change la valeur de l’élément FirstName du premier élément BookParticipant. La deuxième ligne affiche le document XML résultant. Voici les résultats : Joe Rattz Ewan Buckingham
Linq.book Page 265 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
265
Type de l’objet qui va être modifié : XText, Type du changement : Suppression Type de l’objet qui a été changé : XText, Type du changement : Suppression Doc: Type de l’objet qui a été changé : XText, Type du changement : Suppression Type de l’objet qui va être modifié : XText, Type du changement : Add Type de l’objet qui a été modifié : XText, Type du changement : Add Doc: Type de l’objet qui a été modifié : XText, Type du changement : Add Seph Rattz Ewan Buckingham
Cette sortie console montre le document avant et après l’utilisation du gestionnaire d’événements. Comme vous pouvez le constater, l’élément FirstName du premier BookParticipant a été modifié. Les lignes situées entre les deux affichages de l’arbre XML correspondent aux messages affichés par les gestionnaires d’événements. L’objet modifié est de type XText. Pour ma part, je m’attendais à ce qu’il soit de type XElement. Il est facile d’oublier que, lorsque vous affectez une chaîne à la valeur d’un élément, un objet XText est automatiquement créé, de façon transparente. En regardant d’un peu plus près le texte affiché par les gestionnaires d’événements, on comprend mieux ce qu’il se passe lorsqu’un élément est modifié : dans le premier bloc de trois lignes, la valeur XText est sur le point d’être supprimée, puis elle est supprimée. L’événement Changed du document est alors levé. Cela montre que les événements se propagent du niveau le plus bas au niveau le plus haut. Dans le deuxième bloc de trois lignes, la même suite d’événements est générée mais, ici, un objet XText est ajouté à l’arbre XML. Vous savez maintenant que, lorsque vous modifiez la valeur d’un élément, un objet XText est supprimé puis restauré. Dans cet exemple, nous avons utilisé des méthodes nommées. Cette démarche n’est nullement obligatoire : il est également possible d’utiliser des méthodes anonymes ou des expressions lambda. Le Listing 7.87 est identique au précédent mais, au lieu d’utiliser les gestionnaires d’événements déjà implémentés, nous définissons des expressions lambda pour définir à la volée le code appelé par les événements. Listing 7.87 : Gestion d’un événement XObject avec des expressions lambda. XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"),
Linq.book Page 266 Mercredi, 18. février 2009 7:58 07
266
LINQ to XML
Partie III
new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine("{0}{1}", xDocument, System.Environment.NewLine); firstParticipant.Changing += new EventHandler( (object sender, XObjectChangeEventArgs cea) => Console.WriteLine("Type de l’objet qui va être modifié : {0}, Type of change: {1}", sender.GetType().Name, cea.ObjectChange)); firstParticipant.Changed += (object sender, XObjectChangeEventArgs cea) => Console.WriteLine("Type de l’objet qui a été modifié : {0}, Type du changement : {1}", sender.GetType().Name, cea.ObjectChange); xDocument.Changed += (object sender, XObjectChangeEventArgs cea) => Console.WriteLine("Doc: Type de l’objet qui a été modifié : {0}, Type du changement : ➥{1}{2}", sender.GetType().Name, cea.ObjectChange, System.Environment.NewLine); xDocument.Changed += new XObjectChangeEventHandler((sender, cea) => Console.WriteLine("Doc: Type de l’objet qui a été modifié : {0}, Type du changement : ➥{1}{2}", sender.GetType().Name, cea.ObjectChange, System.Environment.NewLine)); firstParticipant.Element("FirstName").Value = "Seph"; Console.WriteLine("{0}{1}", xDocument, System.Environment.NewLine);
Ce code se suffit à lui-même. Il ne dépend d’aucun des gestionnaires d’événements précédemment écrits. Voici les résultats : Joe Rattz Ewan Buckingham Type of object changing: XText, Type of change: Remove Type of object changed: XText, Type of change: Remove Doc: Type of object changed: XText, Type of change: Remove Type de l’objet qui va être modifié : XText, Type du changement : Add Type de l’objet qui a été modifié : XText, Type du changement : Add Doc: Type de l’objet qui a été modifié : XText, Type du changement : Add Seph Rattz
Linq.book Page 267 Mercredi, 18. février 2009 7:58 07
Chapitre 7
L’API LINQ to XML
267
Ewan Buckingham
Les résultats sont identiques à ceux du listing précédent. Avouez que les expressions lambda sont vraiment pratiques et efficaces. Les développeurs qui donnent leurs premières impressions sur LINQ disent souvent qu’ils n’apprécient pas les expressions lambda. Peut-être est-ce parce qu’elles sont nouvelles et très différentes. Mais avouez que cet exemple a de quoi les réconcilier avec ce nouvel outil. Le bogue d’Halloween Vous rappelez-vous du "bogue d’Halloween", introduit au début de ce chapitre ? De grâce, résistez à l’envie qui vous poussera certainement à intervenir sur la portion d’arbre XML dans laquelle vous capturez des événements. Le contenu de l’arbre XML et les événements générés pourraient en effet prendre une tournure incontrôlable.
Résumé Dans ce chapitre, nous avons vu comment utiliser LINQ to XML pour créer, modifier et parcourir des documents XML, ainsi que pour interroger des objets XML à l’aide de requêtes. Vous avez pu voir que la nouvelle API apporte une grande flexibilité : elle permet de créer un élément XML à la volée, de l’initialiser et de le placer dans un arbre XML en une seule instruction. L’API W3C DOM XML en est totalement incapable. C’est la raison pour laquelle l’API LINQ to XML a été conçue. Ce chapitre vous a montré comment appliquer une requête LINQ sur un objet XML unique. Les requêtes portaient par exemple sur les descendants ou les ancêtres d’un élément. À travers de nouveaux opérateurs XML, le chapitre suivant va vous montrer comment appliquer une requête LINQ sur une séquence d’éléments (les descendants d’une séquence, par exemple).
Linq.book Page 268 Mercredi, 18. février 2009 7:58 07
Linq.book Page 269 Mercredi, 18. février 2009 7:58 07
8 Les opérateurs LINQ to XML Les requêtes prises en exemple au chapitre précédent se contentaient de retourner tous les éléments enfants ou tous les ancêtres d’un nœud. Vous rappelez-vous des exemples qui faisaient appel à la méthode XContainer.Elements ? Dans l’affirmative, vous savez ce qu’est une requête XML. C’est là une autre preuve de l’intégration parfaite des requêtes LINQ dans le langage : il est parfois facile d’oublier que l’on est en train d’effectuer une requête. Comme beaucoup des méthodes examinées jusqu’ici retournent une séquence d’objets XML, c’est-à-dire des IEnumerable (où T est une classe de l’API LINQ to XML), il est possible d’appeler les opérateurs de requête standard sur la séquence retournée, ce qui procure encore plus de puissance et de flexibilité. Il est donc possible d’obtenir une séquence d’objets XML à partir d’un objet XML unique (les descendants ou les ancêtres d’un objet, par exemple) mais, ce qui manque, ce sont des opérateurs qui pourraient s’appliquer sur chacun des éléments de ces séquences. À titre d’exemple, il n’existe aucune façon simple d’obtenir une séquence d’éléments et d’effectuer une autre opération XML spécifique sur chacun des éléments de la séquence retournée, comme connaître les éléments enfants de chacun des éléments de la séquence. Pour dire les choses autrement, vous pouvez obtenir une séquence des éléments enfants d’un élément en appelant la méthode Elements de cet élément, mais vous ne pouvez pas obtenir une séquence des éléments enfants des éléments enfants d’un élément. Ceci parce que la méthode Elements doit être appelée sur un XContainer (XElement ou XDocument, par exemple), mais pas sur une séquence d’objets XContainer. C’est à ce point précis que les opérateurs LINQ to XML vont vous venir en aide.
Linq.book Page 270 Mercredi, 18. février 2009 7:58 07
270
LINQ to XML
Partie III
Introduction aux opérateurs LINQ to XML L’API LINQ to XML étend les opérateurs de requête standard de LINQ to Objects en y ajoutant des opérateurs spécifiques au XML. Ces opérateurs sont des méthodes d’extension définies dans la classe System.Xml.Linq.Extensions, qui joue le rôle d’une classe conteneur. Chacun de ces opérateurs est appelé sur une séquence d’un type de donnée LINQ to XML et effectue une action sur chacune des entrées de cette séquence. Il retourne par exemple les ancêtres ou les descendants des différentes entrées. Virtuellement, chacun des opérateurs XML décrits dans ce chapitre a un équivalent dans le chapitre précédent. Cependant, les méthodes du chapitre précédent ne s’appliquent qu’à un objet unique, alors que les opérateurs de ce chapitre s’appliquent à une séquence d’objets. À titre d’exemple, au chapitre précédent, nous avons parlé de la méthode XContainer.Elements, dont voici le prototype : IEnumerable XContainer.Elements()
Dans ce chapitre, nous aborderons l’opérateur Extensions.Elements, dont voici le prototype : IEnumerable Elements (this IEnumerable source) where T : XContainer
Il existe une différence de taille entre ces deux méthodes : le premier prototype est appelé sur un objet unique dérivé de XContainer, alors que le second est appelé sur une séquence d’objets dont chacun est dérivé de XContainer. Pour bien différencier les méthodes du chapitre précédent des méthodes d’extensions de ce chapitre, nous qualifierons les secondes du terme "opérateurs". Et, maintenant, il est temps d’entrer dans le vif du sujet.
Opérateur Ancestors L’opérateur Ancestors est appelé sur une séquence de nœuds. Il retourne une séquence qui contient les éléments ancêtres de chacun des nœuds sources. Prototypes L’opérateur Ancestors a deux prototypes. Premier prototype public static IEnumerable Ancestors ( this IEnumerable source ) where T : XNode
Cette version de l’opérateur peut être appelée sur une séquence de nœuds ou d’objets dérivés de XNode. Elle retourne une séquence d’éléments contenant les ancêtres de chacun des nœuds de la séquence source.
Linq.book Page 271 Mercredi, 18. février 2009 7:58 07
Chapitre 8
Les opérateurs LINQ to XML
271
Second prototype public static IEnumerable Ancestors ( this IEnumerable source, XName name ) where T : XNode
Ce prototype est identique au précédent mais, ici, un nom est passé dans les arguments. Seuls les ancêtres qui correspondent à ce nom sont retournés dans la séquence de sortie. Exemples Le Listing 8.1 donne un exemple d’appel du premier prototype. Listing 8.1 : Un exemple d’appel du premier prototype de l’opérateur Ancestors. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Descendants("FirstName"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Element source : {0} : Valeur = {1}", element.Name, element.Value); } // Affichage des éléments ancêtres des éléments sources foreach (XElement element in elements.Ancestors()) { Console.WriteLine("Elément ancêtre : {0}", element.Name); }
Les premières lignes de ce code définissent un document XML. Une séquence d’éléments FirstName est alors générée (rappelez-vous, la méthode Ancestors est appelée sur une séquence de nœuds, et non sur un nœud unique. Il est donc nécessaire de créer une séquence). Pour faciliter l’identification des nœuds, nous allons afficher leurs noms. Étant donné que les éléments ont un nom, mais pas les nœuds, nous avons choisi de définir une séquence d’éléments, et non de nœuds. Le dernier bloc de code énumère les éléments retournés par la méthode Ancestors et les affiche. Voici les résultats : Élément Élément Élément Élément Élément Élément
source : FirstName : valeur = Joe source : FirstName : valeur = Ewan ancêtre : BookParticipant ancêtre : BookParticipants ancêtre : BookParticipant ancêtre : BookParticipants
Linq.book Page 272 Mercredi, 18. février 2009 7:58 07
272
LINQ to XML
Partie III
Comme vous pouvez le voir, ces résultats affichent les deux éléments de la séquence source, puis les ancêtres de ces éléments. L’opérateur Ancestors retourne tous les éléments ancêtres de chaque nœud sous la forme d’une séquence de nœuds. Dans cet exemple, la séquence utilisée est composée d’éléments, mais cela ne pose pas de problème, puisque les éléments sont dérivés de XNode. Assurez-vous que vous faites bien la différence entre l’opérateur Ancestors, appelé sur une séquence de nœuds, et la méthode Ancestors, étudiée au chapitre précédent. Cet exemple n’est pas aussi impressionnant qu’il peut le paraître. Le code a en effet été étendu à des fins démonstratives. Nous avons ainsi utilisé quelques lignes de code pour énumérer les éléments de la séquence FirstName (appel à la méthode Descendants et bloc foreach suivant). La seconde boucle foreach appelle l’opérateur Ancestors et affiche les ancêtres. Dans cette deuxième boucle, il aurait été possible d’appeler la méthode Ancestors du chapitre précédent sur chacun des éléments de la séquence d’éléments FirstName. Cette technique est illustrée dans le Listing 8.2. Listing 8.2 : Même résultat que le listing précédent, mais sans appeler l’opérateur Ancestors. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Descendants("FirstName"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source: {0} : valeur = {1}", element.Name, element.Value); } foreach (XElement element in elements) { // Appel de la méthode Ancestors sur chaque élément foreach(XElement e in element.Ancestors()) // Affichage des ancêtres de chaque élément source Console.WriteLine("Elément ancêtre : {0}", e.Name); }
Cet exemple est différent du précédent : ici, au lieu d’appeler l’opérateur Ancestors sur les éléments de la séquence dans la boucle foreach, la boucle applique la méthode
Linq.book Page 273 Mercredi, 18. février 2009 7:58 07
Chapitre 8
Les opérateurs LINQ to XML
273
Ancestors du chapitre précédent à chacun des éléments de la séquence. Le résultat est le même que celui du listing précédent : Elément Elément Elément Elément Elément Elément
source : FirstName : valeur = Joe source : FirstName : valeur = Ewan ancêtre : BookParticipant ancêtre : BookParticipants ancêtre : BookParticipant ancêtre : BookParticipants
Grâce à l’opérateur Ancestors et à la concision de LINQ, cette requête peut être résumée à une déclaration bien plus réduite (voir Listing 8.3). Listing 8.3 : Un exemple concis d’appel de l’opérateur Ancestors. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XElement element in xDocument.Element("BookParticipants").Descendants("FirstName").Ancestors()) { Console.WriteLine("Elément ancêtre : {0}", element.Name); }
Dans cet exemple, l’opérateur Ancestors est directement appelé sur la séquence d’éléments retournés par la méthode Descendants. Cette dernière retourne une séquence d’éléments, et l’opérateur Ancestors retourne une autre séquence d’éléments qui contient tous les ancêtres de chacun des éléments de la première séquence. Contrairement aux deux listings précédents, les éléments FirstName ne sont pas affichés. Mais, bien évidemment, les ancêtres sont les mêmes : Elément Elément Elément Elément
ancêtre ancêtre ancêtre ancêtre
: : : :
BookParticipant BookParticipants BookParticipant BookParticipants
En production, vous opterez certainement pour un code concis, semblable à celui présenté dans le Listing 8.3. Cependant, dans la suite de ce chapitre, nous utiliserons un code plus verbeux, comparable à celui du Listing 8.1. Pour illustrer le second prototype de l’opérateur Ancestors, nous utiliserons le même code que dans le Listing 8.1, mais nous changerons l’appel à l’opérateur Ancestors, de sorte qu’il limite la sortie aux ancêtres ayant pour valeur BookParticipant (voir Listing 8.4).
Linq.book Page 274 Mercredi, 18. février 2009 7:58 07
274
LINQ to XML
Partie III
Listing 8.4 : Appel du second prototype de l’opérateur Ancestors. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Descendants("FirstName"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des ancêtres de chaque élément source foreach (XElement element in elements.Ancestors("BookParticipant")) { Console.WriteLine("Elément ancêtre : {0}", element.Name); }
Les résultats sont semblables à ceux du Listing 8.1 mais, cette fois-ci, les ancêtres BookParticipants ne sont pas affichés : Elément Elément Elément Elément
source : FirstName : valeur = Joe source : FirstName : valeur = Ewan ancêtre : BookParticipant ancêtre : BookParticipant
Opérateur AncestorsAndSelf L’opérateur AncestorsAndSelf est appelé sur une séquence d’éléments. Il retourne une séquence qui contient les éléments ancêtres de chacun des éléments sources, ainsi que l’élément source. Cet opérateur est assez proche de l’opérateur Ancestors, si ce n’est qu’il ne peut être appelé que sur des éléments et qu’il inclut l’élément source dans la séquence de sortie. Prototypes L’opérateur AncestorsAndSelf a deux prototypes. Premier prototype public static IEnumerable AncestorsAndSelf ( this IEnumerable source )
Linq.book Page 275 Mercredi, 18. février 2009 7:58 07
Chapitre 8
Les opérateurs LINQ to XML
275
Ce prototype de l’opérateur AncestorsAndSelf est appelé sur une séquence d’éléments. Il retourne une séquence d’éléments composée des éléments sources et de leurs éléments ancêtres. Second prototype public static IEnumerable AncestorsAndSelf ( this IEnumerable source, XName name )
Ce prototype est identique au précédent mais, ici, un nom est passé dans les arguments. Seuls les éléments sources et les ancêtres qui correspondent à ce nom sont retournés dans la séquence de sortie. Exemples Pour illustrer le premier prototype de l’opérateur AncestorsAndSelf, nous utiliserons le même exemple que dans le Listing 8.1 mais, ici, nous appellerons l’opérateur AncestorsAndSelf et non l’opérateur Ancestors (voir Listing 8.5). Listing 8.5 : Appel du premier prototype de l’opérateur AncestorsAndSelf. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Descendants("FirstName"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des éléments sources et de leurs ancêtres foreach (XElement element in elements.AncestorsAndSelf()) { Console.WriteLine("Elément ancêtre : {0}", element.Name); }
Le premier bloc de code crée le document XML. Une séquence d’éléments FirstName est ensuite générée (la méthode AncestorsAndSelf étant appelée sur une séquence d’éléments, et non sur un élément unique, il est donc nécessaire de créer une séquence). Les éléments de la séquence source sont ensuite énumérés et affichés. Enfin, la séquence retournée par AncestorsAndSelf est énumérée et les éléments résultants, affichés.
Linq.book Page 276 Mercredi, 18. février 2009 7:58 07
276
LINQ to XML
Partie III
Si tout fonctionne comme prévu, les résultats devraient être identiques à ceux affichés par le premier exemple du prototype Ancestors mais, ici, les éléments de la séquence FirstName devraient également être inclus. Elément Elément Elément Elément Elément Elément Elément Elément
source : FirstName : valeur = Joe source : FirstName : valeur = Ewan ancêtre : FirstName ancêtre : BookParticipant ancêtre : BookParticipants ancêtre : FirstName ancêtre : BookParticipant ancêtre : BookParticipants
Pour illustrer le second prototype de l’opérateur AncestorsAndSelf, nous utiliserons le même code que dans l’exemple du second prototype de l’opérateur Ancestors. Mais ici, bien entendu, nous utiliserons l’opérateur AncestorsAndSelf et non l’opérateur Ancestors (voir Listing 8.6). Listing 8.6 : Appel du second prototype de l’opérateur AncestorsAndSelf. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Descendants("FirstName"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source: {0} : valeur = {1}", element.Name, element.Value); } // Affichage des ancêtres de chaque élément source foreach (XElement element in elements.AncestorsAndSelf("BookParticipant")) { Console.WriteLine("Elément ancêtre: {0}", element.Name); }
Voici les résultats. Les ancêtres FirstName et BookParticipants ont été éliminés, car ils ne correspondent pas au paramètre passé à l’opérateur AncestorsAndSelf : Elément Elément Elément Elément
source : FirstName : valeur = Joe source : FirstName : valeur = Ewan ancêtre : BookParticipant ancêtre : BookParticipant
Linq.book Page 277 Mercredi, 18. février 2009 7:58 07
Chapitre 8
Les opérateurs LINQ to XML
277
Le second prototype de cet opérateur semble avoir peu d’intérêt. En effet, pensez-vous que deux niveaux d’éléments ou plus portant le même nom puissent cohabiter dans un arbre XML ?
Opérateur Attributes L’opérateur Attributes est appelé sur une séquence d’éléments. Il retourne une séquence contenant les attributs de chacun des éléments sources. Prototypes L’opérateur Attributes a deux prototypes. Premier prototype public static IEnumerable Attributes ( this IEnumerable source )
Ce premier prototype est appelé sur une séquence d’éléments. Il retourne une séquence contenant tous les attributs des éléments sources. Second prototype public static IEnumerable Attributes ( this IEnumerable source, XName name )
Ce prototype est identique au précédent mais, ici, seuls les attributs qui correspondent au nom passé en argument sont retournés dans la séquence de sortie. Exemples Pour illustrer le premier prototype, nous allons ajouter des attributs à l’arbre XML utilisé dans les exemples précédents. Nous travaillerons donc avec une séquence d’éléments BookParticipant (voir Listing 8.7). Listing 8.7 : Appel du premier prototype Attributes. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources
Linq.book Page 278 Mercredi, 18. février 2009 7:58 07
278
LINQ to XML
Partie III
foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des attributs des éléments sources foreach (XAttribute attribute in elements.Attributes()) { Console.WriteLine("Attribut : {0} : valeur = {1}", attribute.Name, attribute.Value); }
La séquence d’éléments BookParticipant est générée puis affichée. L’opérateur Attributes est alors appelé sur cette séquence et les attributs des éléments sont affichés à l’aide d’une boucle foreach. Voici les résultats : Elément source : BookParticipant : valeur = JoeRattz Elément source : BookParticipant : valeur = EwanBuckingham Attribut : type : valeur = Author Attribut : type : valeur = Editor
Pour illustrer le second prototype, nous utiliserons le même code que dans l’exemple précédent, mais nous passerons un nom à l’opérateur Attributes. Seuls les attributs portant ce nom seront inclus dans la séquence de sortie (voir Listing 8.8). Listing 8.8 : Appel du second prototype Attributes. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des attributs des éléments sources foreach (XAttribute attribute in elements.Attributes("type")) { Console.WriteLine("Attribut : {0} : valeur = {1}", attribute.Name, attribute.Value); }
Linq.book Page 279 Mercredi, 18. février 2009 7:58 07
Chapitre 8
Les opérateurs LINQ to XML
279
Seuls les attributs portant le nom "type" sont retournés dans la séquence de sortie. Voici les résultats obtenus suite à l’appui sur Ctrl+F5 : Elément source : BookParticipant : valeur = JoeRattz Elément source : BookParticipant : valeur = EwanBuckingham Attribut : type : valeur = Author Attribut : type : valeur = Editor
Si nous avions passé le paramètre "type" à l’opérateur Attributes, les deux attributs n’auraient pas été affichés. Cet opérateur est donc sensible à la casse, ce qui n’a rien de surprenant, puisque XML est un langage sensible à la casse.
Opérateur DescendantNodes L’opérateur DescendantNodes est appelé sur une séquence d’éléments ou de documents. Il retourne une séquence contenant les nœuds descendants de chacun des éléments ou documents sources. Prototype L’opérateur DescendantNodes a un seul prototype : public static IEnumerable DescendantNodes ( this IEnumerable source ) where T : XContainer
Cet opérateur est différent de la méthode XContainer.DescendantNodes. Le premier est appelé sur une séquence d’éléments ou de documents, la deuxième, sur un élément ou un document unique. Exemple Nous utiliserons le même arbre XML que dans les exemples précédents mais, ici, nous ajouterons un commentaire dans le premier élément BookParticipant. Ceci afin que l’opérateur DescendantNodes retourne au moins un nœud qui n’est pas un élément. Les éléments BookParticipant ayant plusieurs descendants, nous leur appliquerons l’opérateur DescendantNodes (voir Listing 8.9). Listing 8.9 : Appel du prototype de l’opérateur DescendantNodes. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));
Linq.book Page 280 Mercredi, 18. février 2009 7:58 07
280
LINQ to XML
Partie III
IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des nœuds descendants des éléments sources foreach (XNode node in elements.DescendantNodes()) { Console.WriteLine("Nœud descendant : {0}", node); }
Les premières lignes définissent l’arbre XML. Une séquence d’éléments BookParticipant est alors définie. Les éléments de cette séquence sont affichés, puis l’opérateur DescendantNodes lui est appliqué. Voici les résultats : Elément source : Elément source : Noeud descendant Noeud descendant Noeud descendant Noeud descendant Noeud descendant Noeud descendant Noeud descendant Noeud descendant Noeud descendant
BookParticipant : valeur = JoeRattz BookParticipant : valeur = EwanBuckingham : : Joe : Joe : Rattz : Rattz : Ewan : Ewan : Buckingham : Buckingham
Comme vous pouvez le voir, l’opérateur DescendantNodes renvoie tous les nœuds descendants de la séquence BookParticipant : les éléments, mais également le commentaire. Remarquez aussi que chacun des éléments descendants donne lieu à deux nœuds. Par exemple, Joe et Joe sont les deux nœuds descendants relatifs à l’élément Joe. Le premier est l’élément lui-même et le deuxième, sa valeur XText. Je suis sûr que vous aviez oublié que des objets XText sont automatiquement créés pour chaque élément…
Opérateur DescendantNodesAndSelf L’opérateur DescendantNodesAndSelf est appelé sur une séquence d’éléments. Il retourne une séquence contenant les éléments sources et leurs nœuds descendants. Prototype L’opérateur DescendantNodesAndSelf a un seul prototype : public static IEnumerable DescendantNodesAndSelf ( this IEnumerable source )
Linq.book Page 281 Mercredi, 18. février 2009 7:58 07
Chapitre 8
Les opérateurs LINQ to XML
281
Exemple Nous utiliserons le même code que pour illustrer l’opérateur DescendantNodes mais, ici, nous appellerons l’opérateur DescendantNodesAndSelf (voir Listing 8.10). Listing 8.10 : Appel du prototype de l’opérateur DescendantNodesAndSelf. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des noeuds descendants des éléments sources foreach (XNode node in elements.DescendantNodesAndSelf()) { Console.WriteLine("Noeud descendant : {0}", node); }
Voici le résultat : Elément source : BookParticipant : valeur = JoeRattz Elément source : BookParticipant : valeur = EwanBuckingham Noeud descendant : Joe Rattz Noeud descendant : Noeud descendant : Joe Noeud descendant : Joe Noeud descendant : Rattz Noeud descendant : Rattz Noeud descendant : Ewan Buckingham Noeud descendant : Ewan Noeud descendant : Ewan Noeud descendant : Buckingham Noeud descendant : Buckingham
L’opérateur DescendantNodesAndSelf a retourné les éléments de la séquence d’entrée et leurs nœuds descendants, y compris le commentaire du premier élément BookParticipant.
Linq.book Page 282 Mercredi, 18. février 2009 7:58 07
282
LINQ to XML
Partie III
Comme vous avez pu le voir dans l’exemple précédent, l’opérateur DescendantNodes "oublie" le commentaire dans la séquence de sortie. Cette différence sera étudiée un peu plus loin dans ce chapitre.
Opérateur Descendants L’opérateur Descendants peut être appelé sur une séquence d’éléments ou de documents. Il retourne une séquence qui contient tous les éléments descendants des éléments ou documents sources. Prototypes L’opérateur Descendants a deux prototypes. Premier prototype public static IEnumerable Descendants ( this IEnumerable source ) where T : XContainer
Cet opérateur est différent de la méthode XContainer.Descendants. Le premier est appelé sur une séquence d’éléments ou de documents, la deuxième, sur un élément ou un document unique. Second prototype public static IEnumerable Descendants ( this IEnumerable source, XName name ) where T : XContainer
Ce prototype est identique au précédent mais, ici, seuls les descendants des éléments sources dont le nom correspond au paramètre sont retournés dans la séquence de sortie. Exemples Pour illustrer le premier prototype, nous allons utiliser le même code que pour l’opérateur DescendantNodes, mais nous allons appeler l’opérateur Descendants. Les résultats devraient être les mêmes, à ceci près que seuls les éléments devraient être retournés dans la séquence de sortie. Le Listing 8.11 représente le code utilisé pour illustrer ce prototype. Listing 8.11 : Appel du premier prototype de l’opérateur Descendants. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"),
Linq.book Page 283 Mercredi, 18. février 2009 7:58 07
Chapitre 8
Les opérateurs LINQ to XML
283
new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source: {0} : valeur = {1}", element.Name, element.Value); } // Affichage des nœuds descendants des éléments sources foreach (XNode node in elements.Descendants()) { Console.WriteLine("Nœud descendant : {0}", node); }
Seuls les éléments descendants des deux éléments BookParticipant sont inclus dans la séquence de sortie : Elément Elément Elément Elément Elément Elément
source : BookParticipant : valeur = JoeRattz source : BookParticipant : valeur = EwanBuckingham descendant : Joe descendant : Rattz descendant : Ewan descendant : Buckingham
En comparant ces résultats à ceux de l’opérateur DescendantNodes, nous pouvons noter plusieurs différences : m
les descendants apparaissent en tant qu’éléments et non en tant que nœuds ;
m
le commentaire n’est pas inclus dans la séquence de sortie ;
m
les nœuds descendants (Joe et Ratz, par exemple) sont exclus de la séquence de sortie, puisqu’ils sont de type XText et non XElement.
Nous illustrerons le second prototype avec le même code mais, ici, nous passerons un nom dans l’argument de l’opérateur. Seuls les descendants correspondants seront inclus dans la séquence de sortie (voir Listing 8.12). Listing 8.12 : Appel du second prototype de l’opérateur Descendants. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements =
Linq.book Page 284 Mercredi, 18. février 2009 7:58 07
284
LINQ to XML
Partie III
xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des nœuds descendants des éléments sources foreach (XNode node in elements.Descendants("LastName")) { Console.WriteLine("Nœud descendant : {0}", node); }
Voici les résultats. Comme on pouvait s’y attendre, seul le descendant LastName est inclus dans la séquence de sortie : Elément Elément Elément Elément
source : BookParticipant : valeur = JoeRattz source : BookParticipant : valeur = EwanBuckingham descendant : Rattz descendant : Buckingham
Opérateur DescendantsAndSelf L’opérateur DescendantsAndSelf est appelé sur une séquence d’éléments. Il retourne une séquence qui contient tous les éléments descendants des éléments sources. Prototypes L’opérateur DescendantsAndSelf a deux prototypes. Premier prototype public static IEnumerable DescendantsAndSelf ( this IEnumerable source )
Ce prototype est appelé sur une séquence d’éléments. Il retourne une séquence qui contient tous les éléments de la séquence et leurs descendants. Second prototype public static IEnumerable DescendantsAndSelf ( this IEnumerable source, XName name )
Le second prototype est semblable au premier, mais seuls les éléments qui correspondent au paramètre sont retournés dans la séquence de sortie. Exemples Pour illustrer le premier prototype, nous utiliserons le même code que dans le premier exemple de l’opérateur Descendants mais, ici, nous appellerons l’opérateur DescendantAndSelf (voir Listing 8.13).
Linq.book Page 285 Mercredi, 18. février 2009 7:58 07
Chapitre 8
Les opérateurs LINQ to XML
285
Listing 8.13 : Appel du premier prototype de l’opérateur DescendantsAndSelf. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des nœuds descendants des éléments sources foreach (XNode node in elements.DescendantsAndSelf()) { Console.WriteLine("Nœud descendant : {0}", node); }
Voici les résultats : Elément source : BookParticipant : valeur = JoeRattz Elément source : BookParticipant : valeur = EwanBuckingham Elément descendant : Joe Rattz Elément descendant : Joe Elément descendant : Rattz Elément descendant : Ewan Buckingham Elément descendant : Ewan Elément descendant : Buckingham
Les résultats sont identiques à ceux du premier prototype de l’opérateur Descendants, à ceci près qu’ils incluent également les éléments sources eux-mêmes, c’est-à-dire les éléments BookParticipant. Ne soyez pas trompé par la présence du commentaire dans les résultats. Cet objet est non pas un résultat retourné par l’opérateur, mais bel et bien une partie de la séquence
Linq.book Page 286 Mercredi, 18. février 2009 7:58 07
286
LINQ to XML
Partie III
d’entrée incluse dans les résultats (c’est la partie Self de l’opérateur DescendantsAndSelf). Pour illustrer le second prototype, nous utiliserons le même code, mais nous passerons un paramètre à l’opérateur pour limiter la sortie (voir Listing 8.14). Listing 8.14 : Appel du second prototype de l’opérateur DescendantsAndSelf. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des nœuds descendants des éléments sources foreach (XNode node in elements.DescendantsAndSelf("LastName")) { Console.WriteLine("Nœud descendant : {0}", node); }
Voici les résultats : Elément Elément Elément Elément
source : BookParticipant : valeur = JoeRattz source : BookParticipant : valeur = EwanBuckingham descendant : Rattz descendant : Buckingham
La sortie est bien plus limitée que dans l’exemple précédent. Il est même difficile de faire la différence entre les opérateurs Descendants et DescendantsAndSelf. Ceci vient du fait que les éléments sources n’ont pas été retournés, car ils ne correspondaient pas au paramètre passé à l’opérateur. Il est peu probable que vous ayez à utiliser la version "AndSelf" du second prototype de l’opérateur Descendants. En effet, les arbres XML que vous manipulerez n’ont que peu de chances d’avoir des éléments portant le même nom sur plusieurs niveaux hiérarchiques.
Linq.book Page 287 Mercredi, 18. février 2009 7:58 07
Chapitre 8
Les opérateurs LINQ to XML
287
Opérateur Elements L’opérateur Elements peut être appelé sur une séquence d’éléments ou de documents. Il retourne une séquence d’éléments qui contient tous les éléments enfants des éléments ou documents sources. Les opérateurs Elements et Descendants sont différents. En effet, l’opérateur Elements ne retourne que les éléments enfants de premier niveau, alors que l’opérateur Descendants retourne tous les enfants de la séquence d’entrée, en parcourant récursivement tous les niveaux hiérarchiques de l’arborescence. Prototypes L’opérateur Elements a deux prototypes. Premier prototype public static IEnumerable Elements ( this IEnumerable source ) where T : XContainer
Ce premier prototype est appelé sur une séquence d’éléments ou de documents. Il retourne une séquence d’éléments qui contient tous les éléments enfants des éléments ou documents sources. Cet opérateur est différent de la méthode XContainer.Elements. Le premier est appelé sur une séquence d’éléments ou de documents, la deuxième, sur un élément ou un document unique. Second prototype public static IEnumerable Elements ( this IEnumerable source, XName name ) where T : XContainer
Ce prototype est identique au premier mais, ici, seuls les éléments correspondant au paramètre passé à l’opérateur sont retournés dans la séquence de sortie. Exemples Nous utiliserons le même code que dans l’exemple du premier prototype de l’opérateur DescendantsAndSelf mais, ici, nous invoquerons l’opérateur Elements (voir Listing 8.15). Listing 8.15 : Appel du premier prototype de l’opérateur Elements. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"),
Linq.book Page 288 Mercredi, 18. février 2009 7:58 07
288
LINQ to XML
Partie III
new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des nœuds descendants des éléments sources foreach (XElement element in elements.Elements()) { Console.WriteLine("Elément enfant : {0}", element); }
Voici les résultats : Elément Elément Elément Elément Elément Elément
source source enfant enfant enfant enfant
: : : : : :
BookParticipant : valeur = JoeRattz BookParticipant : valeur = EwanBuckingham Joe Rattz Ewan Buckingham
Cet exemple retourne tous les éléments enfants de la séquence d’entrée. Pour limiter la séquence de sortie aux seuls éléments dont le nom est spécifié, nous utiliserons le second prototype de l’opérateur Elements (voir Listing 8.16). Listing 8.16 : Appel du second prototype de l’opérateur Elements. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des nœuds descendants des éléments sources foreach (XElement element in elements.Elements("LastName")) { Console.WriteLine("Elément enfant : {0}", element); }
Linq.book Page 289 Mercredi, 18. février 2009 7:58 07
Chapitre 8
Les opérateurs LINQ to XML
289
Voici les résultats : Elément Elément Elément Elément
source source enfant enfant
: : : :
BookParticipant : valeur = JoeRattz BookParticipant : valeur = EwanBuckingham Rattz Buckingham
Opérateur InDocumentOrder L’opérateur InDocumentOrder est appelé sur une séquence de nœuds. Il retourne une séquence composée des nœuds enfants des nœuds sources, dans l’ordre du document. Prototype L’opérateur InDocumentOrder a un seul prototype : public static IEnumerable InDocumentOrder ( this IEnumerable source ) where T : XNode
Cet opérateur doit être appelé sur une séquence composée de nœuds ou d’objets dérivés. Il retourne une séquence du même type composée des nœuds enfants des nœuds sources, dans l’ordre du document. Exemple Pour illustrer cet opérateur, nous avons besoin d’une séquence de nœuds, éléments et non éléments. Pour ce faire, nous utiliserons la séquence des nœuds enfants des éléments BookParticipant. L’un des nœuds est un commentaire, pas un élément. Nous verrons ainsi comment l’opérateur InDocumentOrder se comporte sur ce type de nœud (voir Listing 8.17). Listing 8.17 : Appel du prototype de l’opérateur InDocumentOrder. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable nodes = xDocument.Element("BookParticipants").Elements("BookParticipant"). Nodes().Reverse();
Linq.book Page 290 Mercredi, 18. février 2009 7:58 07
290
LINQ to XML
Partie III
// Affichage des nœuds sources foreach (XNode node in nodes) { Console.WriteLine("Noeud source : {0}", node); } // Affichage des noeuds enfants des noeuds sources foreach (XNode node in nodes.InDocumentOrder()) { Console.WriteLine("Noeud ordonné : {0}", node); }
Après avoir construit l’arbre XML, les nœuds enfants des éléments BookParticipants sont obtenus en invoquant l’opérateur Nodes. L’opérateur Reverse est appliqué au résultat de l’opérateur Nodes pour inverser l’ordre de la séquence (si nécessaire, reportez-vous à la section relative à l’opérateur LINQ to SQL Reverse, dans la deuxième partie de l’ouvrage, pour avoir des informations complémentaires). La séquence utilisée en entrée de l’opérateur InDocumentOrder est donc composée des nœuds des éléments BookParticipant, disposés dans l’ordre inverse de celui du document. Voici le résultat : Noeud Noeud Noeud Noeud Noeud Noeud Noeud Noeud Noeud Noeud
source : Buckingham source : Ewan source : Rattz source : Joe source : ordonné : ordonné : Joe ordonné : Rattz ordonné : Ewan ordonné : Buckingham
Comme vous pouvez le voir, les nœuds sources sont dans l’ordre inverse des nœuds de la séquence de sortie.
Opérateur Nodes L’opérateur Nodes peut être appelé sur une séquence d’éléments ou de documents. Il retourne une séquence de nœuds composée des nœuds enfants des éléments/documents sources. Cet opérateur est différent de l’opérateur DescendantNodes, car il ne retourne que les éléments enfants de premier niveau, alors que l’opérateur DescendantNodes retourne tous les enfants de la séquence d’entrée, en parcourant récursivement tous les niveaux hiérarchiques de l’arborescence. Prototype L’opérateur Nodes n’a qu’un seul prototype : public static IEnumerable Nodes ( this IEnumerable source ) where T : XContainer
Linq.book Page 291 Mercredi, 18. février 2009 7:58 07
Chapitre 8
Les opérateurs LINQ to XML
291
Cet opérateur est différent de la méthode XContainer.Nodes. Le premier est appelé sur une séquence d’éléments ou de documents, la deuxième, sur un élément ou un document unique. Exemple Nous allons définir un arbre XML, créer une séquence source d’éléments BookParticipant et lui appliquer l’opérateur Nodes. Comme toujours, nous afficherons les éléments sources et ceux retournés par l’opérateur (voir Listing 8.18). Listing 8.18 : Appel du prototype de l’opérateur Nodes. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des noeuds enfants des éléments sources foreach (XNode node in elements.Nodes()) { Console.WriteLine("Noeud enfant : {0}", node); }
L’opérateur Nodes retourne une séquence de nœuds (et non d’éléments) enfants de la séquence d’entrée. Le commentaire devrait donc être inclus dans la séquence de sortie. Voici les résultats : Elément source Elément source Noeud enfant : Noeud enfant : Noeud enfant : Noeud enfant : Noeud enfant :
: BookParticipant : valeur = JoeRattz : BookParticipant : valeur = EwanBuckingham Joe Rattz Ewan Buckingham
Étant donné que seuls les nœuds enfants de premier niveau sont retournés par l’opérateur Nodes, les nœuds XText, enfants des éléments FirstName et LastName, ne sont pas
Linq.book Page 292 Mercredi, 18. février 2009 7:58 07
292
LINQ to XML
Partie III
retournés. Si vous retournez quelques pages en arrière, vous verrez que l’opérateur DescendantNodes les incluait dans la séquence de sortie.
Opérateur Remove L’opérateur Remove est appelé sur une séquence de nœuds ou d’attributs à supprimer. Pour éviter le bogue d’Halloween, introduit au chapitre précédent, les nœuds/attributs sont mémorisés dans une liste. Prototypes L’opérateur Remove a deux prototypes. Premier prototype public static void Remove ( this IEnumerable source )
Ce prototype est appelé sur une séquence d’attributs. Il supprime tous les attributs de la séquence d’entrée. Second prototype public static void Remove ( this IEnumerable source ) where T : XNode
Ce prototype est appelé sur une séquence de nœuds (ou d’autres types qui en sont dérivés). Il supprime tous les nœuds de la séquence d’entrée. Exemples Pour illustrer le premier prototype, nous avons besoin d’une séquence d’attributs. Nous allons donc utiliser notre arbre XML standard et travailler sur une séquence composée des attributs des éléments BookParticipant. Nous allons afficher la séquence des attributs sources, appeler l’opérateur Remove sur cette séquence, puis afficher le document XML dans sa totalité, pour nous assurer que l’opérateur Remove a bien fait son travail (voir Listing 8.19). Listing 8.19 : Appel du premier prototype de l’opérateur Remove. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));
Linq.book Page 293 Mercredi, 18. février 2009 7:58 07
Chapitre 8
Les opérateurs LINQ to XML
293
IEnumerable attributes = xDocument.Element("BookParticipants").Elements("BookParticipant").Attributes(); // Affichage des attributs sources foreach (XAttribute attribute in attributes) { Console.WriteLine("Attribut source : {0} : valeur = {1}", attribute.Name, attribute.Value); } attributes.Remove(); // Affichage du document XML Console.WriteLine(xDocument);
Voici les résultats : Attribut source : type : valeur = Author Attribut source : type : valeur = Editor Joe Rattz Ewan Buckingham
Nous allons maintenant illustrer le second prototype. Plutôt que nous contenter d’obtenir puis de supprimer une séquence de nœuds, nous allons envisager quelque chose de plus intéressant : extraire la séquence de commentaires de certains éléments et supprimer uniquement ces objets (voir Listing 8.20). Listing 8.20 : Appel du second prototype. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable comments = xDocument.Element("BookParticipants").Elements("BookParticipant"). Nodes().OfType(); // Affichage des commentaires sources foreach (XComment comment in comments) { Console.WriteLine("Source comment: {0}", comment); }
Linq.book Page 294 Mercredi, 18. février 2009 7:58 07
294
LINQ to XML
Partie III
comments.Remove(); // Affichage du document XML Console.WriteLine(xDocument);
Après avoir construit la séquence source, les nœuds enfants (Nodes) de type XComment (OfType) sont placés dans la séquence comments. Reportez-vous si nécessaire à la deuxième partie de ce livre pour en savoir plus sur l’opérateur de requête standard OfType. La méthode Remove est alors appliquée à la séquence comments. Après l’exécution de cet opérateur, l’arbre XML est privé de tout commentaire dans les éléments BookParticipant. Voici le résultat : Source comment: Joe Rattz Ewan Buckingham
L’opérateur OfType est très pratique et il s’intègre parfaitement dans une requête LINQ to XML. Il pourrait se révéler très utile en situation réelle.
Résumé Au chapitre précédent, nous avons introduit l’API LINQ to XML et montré comment l’utiliser pour créer, modifier, sauvegarder et lire des arbres XML. Nous avons intentionnellement utilisé le mot "arbre" et non le mot "document", car avec LINQ to XML il n’est plus nécessaire de manipuler des documents. Nous avons également montré comment effectuer une requête sur un nœud/un élément pour atteindre les nœuds/éléments qui lui sont hiérarchiquement liés. Dans ce chapitre, vous avez également appris à interroger des séquences de nœuds ou d’éléments en utilisant les opérateurs de LINQ to XML. Arrivé à ce point dans la lecture du livre, vous devriez être en mesure d’effectuer des requêtes élémentaires sur des arbres XML en utilisant les opérateurs LINQ to XML. Cette nouvelle API devrait se révéler très utile pour interroger des données… en particulier si vous lui adjoignez des opérateurs de requête standard. Vous connaissez maintenant toutes les techniques de base permettant de définir des requêtes LINQ to SQL. Au chapitre suivant, nous aborderons des requêtes légèrement plus complexes et nous nous intéresserons à d’autres domaines d’action de LINQ to XML tels que la validation et la transformation.
Linq.book Page 295 Mercredi, 18. février 2009 7:58 07
9 Les autres possibilités de XML Dans les deux chapitres précédents, vous avez appris à créer, à modifier et à parcourir des données XML en utilisant l’API LINQ to XML. Nous avons également vu comment utiliser des blocs de construction pour créer des requêtes XML très puissantes. Je pense que, dès à présent, vous serez d’accord pour affirmer que LINQ to XML peut couvrir 90 % de vos besoins en matière de XML. Mais qu’en est-il des 10 % restants ? Voyons si nous pouvons diminuer ce pourcentage. Si Microsoft avait ajouté la validation de schéma, les transformations et les requêtes XPath, quel serait le pourcentage selon vous ? Nous avons vu les bases de l’API LINQ to XML et comment effectuer les requêtes élémentaires. Nous allons maintenant nous intéresser à des requêtes plus complexes et aussi plus proches du monde réel. Dans ce chapitre, nous allons passer en revue quelques exemples qui, je l’espère, rendront à vos yeux les requêtes XML des plus triviales lorsqu’elles seront effectuées via l’API LINQ to XML. Pour décrire plus complètement cette API, nous aborderons des fonctionnalités complémentaires (essentiellement la transformation et la validation) et vous donnerons diverses informations bonnes à connaître en LINQ to XML. D’une façon plus spécifique, nous verrons comment effectuer des transformations avec et sans XSLT, comment valider un document XML par rapport à un schéma et donnerons un exemple de requête utilisant le style XPath.
Espaces de noms référencés Outre les espaces de noms LINQ et LINQ to XML désormais traditionnels, System.Linq et System.Xml.Linq, les exemples de ce chapitre utilisent également les espaces de noms System.Xml, System.Xml.Schema, System.Xml.Xsl et System.Xml.XPath.
Linq.book Page 296 Mercredi, 18. février 2009 7:58 07
296
LINQ to XML
Partie III
À moins qu’elles ne soient déjà présentes dans votre code, vous devrez donc ajouter les directives using suivantes : using using using using using using
System.Linq; System.Xml; System.Xml.Linq; System.Xml.Schema; System.Xml.XPath; System.Xml.Xsl;
Requêtes Dans le chapitre précédent, nous avons vu les principes de base permettant d’exécuter des requêtes XML via LINQ to XML. La plupart des exemples avaient pour but l’illustration d’un opérateur ou d’une propriété. Dans cette section, nous allons passer en revue plusieurs exemples "orientés solution" et, donc, plus proches de la réalité. La description du chemin n’est pas une obligation Dans les chapitres précédents, la plupart des exemples "plongeaient" dans la hiérarchie XML pour obtenir une référence sur un élément particulier en utilisant les opérateurs Element ou Elements de façon récursive, jusqu’à ce que l’élément visé soit atteint. Ainsi, beaucoup d’exemples contenaient ce type d’instruction : IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant");
Cet exemple accède à l’élément enfant BookParticipants du document, puis aux éléments enfants BookParticipant de l’élément BookParticipants. Cette technique n’est pas toujours nécessaire. Vous pouvez en effet utiliser un code comparable au Listing 9.1. Listing 9.1 : Accès à des éléments sans décrire leur chemin. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Descendants("BookParticipant"); foreach (XElement element in elements) { Console.WriteLine("Elément: {0} : valeur = {1}", element.Name, element.Value); }
Linq.book Page 297 Mercredi, 18. février 2009 7:58 07
Chapitre 9
Les autres possibilités de XML
297
Dans cet exemple, l’instruction en gras obtient les descendants BookParticipant du document. Étant donné que l’accès ne se fait pas dans une branche particulière de l’arbre XML, il est nécessaire de connaître le schéma, car il serait possible d’accéder par erreur à certaines branches de l’arbre. Cependant, cette technique fonctionne dans de nombreux cas. Voici les résultats : Elément: BookParticipant : valeur = JoeRattz Elément: BookParticipant : valeur = EwanBuckingham
Si tous les éléments BookParticipant ne sont pas utiles, vous pouvez restreindre la requête. Le Listing 9.2, par exemple, ne retourne que les éléments dont l’élément FirstName a pour valeur "Ewan". Listing 9.2 : Accès restreint à des éléments sans décrire le chemin. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument .Descendants("BookParticipant") .Where(e => ((string)e.Element("FirstName")) == "Ewan"); foreach (XElement element in elements) { Console.WriteLine("Elément: {0} : valeur = {1}", element.Name, element.Value); }
Cette fois-ci, nous avons appliqué l’opérateur Where en suffixe dans la définition de l’objet elements. Remarquez l’utilisation de l’opérateur de casting (string) pour comparer la valeur de l’élément avec la chaîne "Ewan". Voici les résultats : Elément: BookParticipant : valeur = EwanBuckingham
Il est parfois nécessaire de contrôler l’ordre des résultats. Dans le Listing 9.3, nous allons modifier l’expression lambda de l’opérateur Where pour que deux éléments soient retournés. La requête portera sur l’attribut type. Listing 9.3 : Accès restreint à des éléments sans décrire le chemin, en définissant l’ordre et en utilisant la syntaxe d’interrogation des requêtes. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"),
Linq.book Page 298 Mercredi, 18. février 2009 7:58 07
298
LINQ to XML
Partie III
new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = from e in xDocument.Descendants("BookParticipant") where ((string)e.Attribute("type")) != "Illustrator" orderby ((string)e.Element("LastName")) select e; foreach (XElement element in elements) { Console.WriteLine("Elément: {0} : valeur = {1}", element.Name, element.Value); }
La requête porte toujours sur les éléments BookParticipant du document mais, ici, seuls les éléments dont l’attribut type a une valeur différente de "Illustrator" sont sélectionnés. Dans cet arbre, cela correspond à tous les éléments BookParticipant. Les résultats sont alors classés par éléments LastName croissants. Remarquez l’utilisation d’opérateurs de casting pour obtenir la valeur de l’attribut type et de l’élément LastName. Voici les résultats : Elément: BookParticipant : valeur = EwanBuckingham Elément: BookParticipant : valeur = JoeRattz
Une requête complexe Jusqu’ici, toutes les requêtes passées en revue étaient simplistes. Avant de passer à un autre sujet, nous allons étudier une requête complexe. Nous utiliserons des données mises à disposition par le W3C à des fins de tests. L’exemple du Listing 9.4 contient des données issues de trois documents XML différents. Ces documents sont obtenus en divisant une représentation texte des documents XML suggérés par le W3C. Nous allons expliquer de façon détaillée chacune des parties du code. La première étape va consister à définir les documents en utilisant du code XML. Listing 9.4 : Une requête complexe qui effectue une jointure sur trois documents en utilisant la syntaxe d’expression de requête de LINQ. XDocument users = XDocument.Parse( @" U01 Tom Jones B U02 Mary Doe
Linq.book Page 299 Mercredi, 18. février 2009 7:58 07
Chapitre 9
A U03 Dee Linquent D U04 Roger Smith C U05 Jack Sprat B U06 Rip Van Winkle B "); XDocument items = XDocument.Parse( @" 1001 Red Bicycle U01 1999-01-05 1999-01-20 40 1002 Motorcycle U02 1999-02-11 1999-03-15 500 1003 Old Bicycle U02 1999-01-10 1999-02-20 25 1004 Tricycle U01 1999-02-25 1999-03-08 15 1005 Tennis Racket U03 1999-03-19 1999-04-30
Les autres possibilités de XML
299
Linq.book Page 300 Mercredi, 18. février 2009 7:58 07
300
LINQ to XML
20 1006 Helicopter U03 1999-05-05 1999-05-25 50000 1007 Racing Bicycle U04 1999-01-20 1999-02-20 200 1008 Broken Bicycle U01 1999-02-05 1999-03-06 25 "); XDocument bids = XDocument.Parse( @" U02 1001 35 1999-01-07 U04 1001 40 1999-01-08 U02 1001 45 1999-01-11 U04 1001 50 1999-01-13 U02 1001 55 1999-01-15 U01 1002 400
Partie III
Linq.book Page 301 Mercredi, 18. février 2009 7:58 07
Chapitre 9
1999-02-14 U02 1002 600 1999-02-16 U03 1002 800 1999-02-17 U04 1002 1000 1999-02-25 U02 1002 1200 1999-03-02 U04 1003 15 1999-01-22 U05 1003 20 1999-02-03 U01 1004 40 1999-03-05 U03 1007 175 1999-01-25 U05 1007 200 1999-02-08 U04 1007 225 1999-02-12 ");
Les autres possibilités de XML
301
Linq.book Page 302 Mercredi, 18. février 2009 7:58 07
302
LINQ to XML
Partie III
Ces trois documents représentent les données (utilisateurs, objets vendus et enchères) manipulées sur un site web de vente aux enchères. Ils ont été créés en appelant la méthode XDocument.Parse sur des représentations chaînes des données. La requête va consister à extraire les enchères supérieures à 50 dollars. Les résultats doivent faire apparaître la date, le montant de l’enchère, le nom de la personne qui en est à l’origine, le numéro de l’objet et sa description. Voici la requête : var biddata = from b in bids.Descendants("bid_tuple") where ((double)b.Element("bid")) > 50 join u in users.Descendants("user_tuple") on ((string)b.Element("userid")) equals ((string)u.Element("userid")) join i in items.Descendants("item_tuple") on ((string)b.Element("itemno")) equals ((string)i.Element("itemno")) select new {Item = ((string)b.Element("itemno")), Description = ((string)i.Element("description")), User = ((string)u.Element("name")), Date = ((string)b.Element("bid_date")), Price = ((double)b.Element("bid"))};
La requête est plus complexe que celles étudiées jusqu’ici. La première ligne utilise la méthode Descendants pour accéder aux descendants bid_tuple du document bids. La ligne suivante utilise l’opérateur Where pour ne conserver que les enchères supérieures à 50 dollars. Il peut sembler inhabituel d’utiliser une clause Where si tôt dans la requête. Cette clause aurait tout aussi bien pu être spécifiée juste avant la clause select, mais cela aurait signifié que le Where aurait été appliqué sur la jointure entre les utilisateurs et les objets, y compris pour les enchères inférieures à 50 dollars. En ayant réduit le nombre de données avant la jointure, la charge de travail a ainsi été allégée pour la suite de la requête et les performances, améliorées. Une fois limitées aux seules enchères supérieures à 50 dollars, les données sont jointes au document XML users par l’intermédiaire de l’élément userid (lignes 3 à 5), afin d’obtenir le nom de chaque utilisateur. Arrivés à ce point dans la requête, nous avons joint les documents bids et users et limité les données aux enchères supérieures à 50 dollars. Les trois prochaines lignes (6 à 8) effectuent une jointure sur le document XML items par l’intermédiaire du champ itemno afin d’obtenir la description de l’objet. À ce point, les documents bids, users et items sont joints. Remarquez que différents opérateurs de casting ont été utilisés pour obtenir la valeur des éléments dans le type souhaité. Ainsi, par exemple, le montant de l’enchère a été obtenu avec un opérateur (double). Les enchères sont au format string mais, étant donné que leur contenu peut être converti en une valeur double, l’opérateur de casting a fait son travail.
Linq.book Page 303 Mercredi, 18. février 2009 7:58 07
Chapitre 9
Les autres possibilités de XML
303
La prochaine étape va consister à sélectionner une classe anonyme qui contient les éléments enfants des éléments issus de cette double jointure. Nous allons commencer par afficher un en-tête : Console.WriteLine("{0,-12} {1,-12} {2,-6} {3,-14} {4,10}", "Date", "User", "Item", "Description", "Price"); Console.WriteLine("===================================================");
Les instructions suivantes énumèrent la séquence et affichent les valeurs correspondantes : bid: foreach (var bd in biddata) { Console.WriteLine("{0,-12} {1,-12} {2,-6} {3,-14} {4,10:C}", bd.Date, bd.User, bd.Item, bd.Description, bd.Price); }
Cette portion de code est triviale. En fait, mis à part la requête elle-même, tout le reste du code est simplissime. Voici les résultats : Date
User
Item
Description
Price
=================================================================================== 1999-01-15 1999-02-14 1999-02-16 1999-02-17 1999-02-25 1999-03-02 1999-01-25 1999-02-08 1999-02-12
Mary Doe Tom Jones Mary Doe Dee Linquent Roger Smith Mary Doe Dee Linquent Jack Sprat Roger Smith
1001 1002 1002 1002 1002 1002 1007 1007 1007
Red Bicycle Motorcycle Motorcycle Motorcycle Motorcycle Motorcycle Racing Bicycle Racing Bicycle Racing Bicycle
$55.00 $400.00 $600.00 $800.00 $1,000.00 $1,200.00 $175.00 $200.00 $225.00
Quelques lignes de code ont suffi pour joindre trois documents XML ! Maintenant, je suis sûr que vous vous rendez compte de la puissance de LINQ to XML. Mais attendez un peu, d’autres possibilités très intéressantes vous attendent dans les pages suivantes…
Transformations LINQ to XML vous permet d’effectuer des transformations en utilisant deux approches diamétralement opposées. La première consiste à utiliser XSLT via les classes passerelles XmlReader et XmlWriter. La seconde approche consiste à utiliser LINQ to XML en construisant fonctionnellement le document XML cible et en incluant une requête LINQ to XML dans le document source XML.
Linq.book Page 304 Mercredi, 18. février 2009 7:58 07
304
LINQ to XML
Partie III
XSLT est une technologie XML standard. Des outils permettant d’écrire, de déboguer et de tester les transformations XSLT sont d’ores et déjà disponibles. Par ailleurs, il est possible que vous disposiez déjà de documents XSLT. Si tel est le cas, vous pouvez les utiliser dans vos nouvelles applications par l’intermédiaire de LINQ to XML. De nombreux documents XSLT sont disponibles. Vous n’avez qu’à choisir celui qui s’adapte le mieux à vos souhaits. De plus, l’utilisation de XSLT pour vos transformations se révèle plus dynamique. Contrairement à l’approche "construction fonctionnelle" de LINQ to XML, il n’est pas nécessaire de recompiler le code pour changer la transformation : le simple fait de modifier le document XSLT suffit pour changer la transformation à l’exécution. Enfin, la technologie XSLT est bien connue et bon nombre de développeurs experts dans ce domaine peuvent vous assister. Ce fait n’est bien entendu plus d’actualité si vous choisissez l’approche "construction fonctionnelle". L’approche "construction fonctionnelle" ne vous demandera pas un gros investissement. Les transformations XML seront en effet effectuées par l’intermédiaire de LINQ to XML. Si vous ne connaissez pas XSLT, et si vos besoins en matière de transformations sont modestes, cette approche peut vous convenir. Par ailleurs, bien que la construction fonctionnelle soit moins pratique que la modification d’un document XSLT, la nécessité d’avoir à recompiler le code pour modifier une transformation peut être considérée comme une sécurité supplémentaire : un tiers ne peut ainsi modifier un document externe pour changer le sens d’une transformation. Transformations avec XSLT Pour effectuer une transformation XML en utilisant XSLT, vous utiliserez les classes passerelles XmlWriter et XmlReader. Vous les obtiendrez à partir des méthodes CreateWriter et CreateReader des classes XDocument. L’exemple du Listing 9.5 demande quelques explications. Nous les donnerons au fur et à mesure, en séparant le code en plusieurs blocs fonctionnels. Listing 9.5 : Transformation d’un document XML avec XSLT. string xsl = @" Book Participants Role | First Name | Last Name |
Linq.book Page 305 Mercredi, 18. février 2009 7:58 07
Chapitre 9
Les autres possibilités de XML
305
| | |
";
Ce code se contente de définir quelques instructions XSL qui vont créer du code HTML afin d’afficher les données XML BookParticipant dans un tableau HTML. La prochaine étape va consister à créer le document XML avec les participants : XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));
Ce code a déjà été utilisé à de maintes reprises dans les pages précédentes. C’est à partir de maintenant que la magie va opérer. Nous allons créer un document XDocument pour la version transformée. À partir de ce document, nous définirons un XmlWriter, nous instancierons un objet XslCompiledTransform, nous chargerons l’objet transformé avec la feuille de style de transformation et nous transformerons le document XML d’entrée en la sortie XmlWriter : XDocument transformedDoc = new XDocument(); using (XmlWriter writer = transformedDoc.CreateWriter()) { XslCompiledTransform transform = new XslCompiledTransform(); transform.Load(XmlReader.Create(new StringReader(xsl))); transform.Transform(xDocument.CreateReader(), writer); } Console.WriteLine(transformedDoc);
Voici le résultat de la transformation. Comme vous pouvez le voir, nous utilisons les passerelles XmlWriter et XmlReader pour effectuer la transformation : Book Participants
Role | First Name | Last Name |
Author | Joe | Rattz |
Linq.book Page 306 Mercredi, 18. février 2009 7:58 07
306
LINQ to XML
Partie III
Editor | Ewan | Buckingham |
Transformations avec la construction fonctionnelle Cette section va vous montrer comment effectuer des transformations XSLT en utilisant l’API LINQ to XML. Logiquement parlant, une transformation peut être aussi simple que la combinaison d’un arbre XML défini par la construction fonctionnelle et d’une requête XML incorporée dans cet arbre. Nous allons expliquer les transformations XML à travers un exemple. Dans de nombreux autres exemples des chapitres dédiés à LINQ to XML, nous avons utilisé l’arbre XML suivant : Joe Rattz Ewan Buckingham
Supposons que nous devions transformer cet arbre XML comme suit :
Pour accomplir cette transformation, nous allons utiliser la construction fonctionnelle en incluant une requête dans l’arbre. Cette approche va consister à construire un nouveau document dont l’allure correspond à l’arbre XML cible en appliquant une requête LINQ to XML au document XML source pour y piocher les données. C’est la structure de l’arbre XML cible qui va guider la construction fonctionnelle et la logique de la requête. Étant donné que cette tâche est légèrement plus complexe que la plupart des exemples LINQ to XML précédents, nous donnerons des explications chaque fois que cela est nécessaire (voir Listing 9.6). Listing 9.6 : Transformation d’un document XML. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"),
Linq.book Page 307 Mercredi, 18. février 2009 7:58 07
Chapitre 9
Les autres possibilités de XML
307
new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine("Document XML original :"); Console.WriteLine("{0}{1}{1}", xDocument, System.Environment.NewLine);
Ce code définit le document XML source que nous allons transformer. La prochaine étape consiste à construire le nouveau document et l’élément racine : XDocument xTransDocument = new XDocument( new XElement("MediaParticipants",
Rappelez-vous que la structure de l’arbre XML de sortie guide la construction fonctionnelle. Arrivés à ce point, nous avons un document et l’élément racine, Mediaparticipants. Nous devons maintenant ajouter l’attribut type à l’élément racine : new XAttribute("type", "book"),
L’attribut type et sa valeur n’existent pas dans le document XML source. Ils ont donc été définis dans le code. Maintenant que l’attribut type est défini, nous allons générer un élément Participant pour chacun des éléments BookParticipant du document XML original. Pour ce faire, il va suffire d’exécuter la requête suivante : xDocument.Element("BookParticipants") .Elements("BookParticipant")
Ces deux lignes de code fournissent une séquence d’éléments BookParticipant. Nous allons maintenant générer et initialiser un élément Participant pour chaque élément BookParticipant. Pour ce faire, nous utiliserons l’opérateur de projection Select : .Select(e => new XElement("Participant",
Nous allons maintenant construire les attributs Role et Name de l’élément Participant en piochant leurs valeurs dans l’élément BookParticipant : new XAttribute("Role", (string)e.Attribute("type")), new XAttribute("Name", (string)e.Element("FirstName") + " " + (string)e.Element("LastName"))))));
Enfin, nous affichons le document XML transformé : Console.WriteLine("Document XML transformé :"); Console.WriteLine(xTransDocument);
Voici le résultat, tout à fait conforme aux attentes : Document XML original: Joe Rattz Ewan Buckingham
Linq.book Page 308 Mercredi, 18. février 2009 7:58 07
308
LINQ to XML
Partie III
Document XML transformé :