Référence
LINQ
Language Integrated Query en C# 2008
http://www.free-livres.com/ 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);