Vous êtes sur la page 1sur 632

Référence

LINQ

Language Integrated Query en C# 2008

   
Réseaux et télécom
Réseaux
et télécom

Programmationhttp://www.free-livres.com/   Réseaux et télécom Génie logiciel Sécurité Système d’exploitation Joseph

Génie logiciel
Génie logiciel
Sécurité
Sécurité
Système d’exploitation
Système
d’exploitation

Joseph C. Rattz

  Réseaux et télécom Programmation Génie logiciel Sécurité Système d’exploitation Joseph C. Rattz

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#

Relecture technique : Mitsuru Furuta, Microsoft France Pierrick Gourlain, MVP Client Application Matthieu Mezil, MVP C#

Pearson Education France a apporté le plus grand soin à la réalisation de ce livre afin de vous four- nir une information complète et fiable. Cependant, Pearson Education France n’assume de respon- sabilité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

Mise en pages : TyPAO

ISBN : 978-2-7440-4106-8 Copyright © 2009 Pearson Education France Tous droits réservés

Titre original :

Pro LINQ Language Integrated Query in C# 2008

Traduit de l’américain par Michel Martin

Relecture technique :

Mitsuru Furuta, Pierrick Gourlain, Matthieu Mezil

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.

               

Table des matières

À propos de l’auteur

XI

Traducteur et relecteurs techniques

XIII

 

Partie I

         
           

LINQ et C# 2008

       

1

Hello LINQ

3

Un changement de paradigme

3

4

Interrogation XML

5

Interrogation d’une base de données SQL Server Introduction

6

LINQ et l’interrogation des données

7

Composants

7

Comment travailler avec LINQ

9

LINQ ne se limite pas aux requêtes

9

Quelques conseils avant de commencer

12

Utilisez le mot-clé var si vous n’êtes pas à l’aise

12

 

14

Utilisez les opérateurs Cast ou OfType pour les collections héritées Préférez l’opérateur OfType à l’opérateur Cast

15

Les requêtes aussi peuvent être boguées

15

Sachez tirer parti des requêtes différées

16

Utiliser le log du DataContext

17

Utilisez le forum LINQ

18

Résumé

18

                             

2

Améliorations de C# 3.0 pour LINQ

19

Les nouveautés du langage C# 3.0

19

Les expressions lambda

20

Arbres d’expressions

25

 

26

Le mot-clé var, l’initialisation d’objets et les types anonymes Méthodes d’extension

31

Méthodes partielles

37

Expressions de requête

39

Résumé

49

IV

Table des matières

Partie II

LINQ to Objects

3

Introduction à LINQ to Objects

53

Vue d’ensemble de LINQ to Objects

53

 

54

IEnumerable<T>, séquences et opérateurs de requête standard IEnumerable<T>, yield et requêtes différées

55

Délégués Func

58

Les opérateurs de requête standard

59

Résumé

61

                         

4

Les opérateurs différés

63

Espaces de noms référencés

63

Assemblies référencés

64

Classes communes

64

Les opérateurs différés, par groupes fonctionnels

65

Restriction

65

Projection

67

Partage

76

Concaténation

83

Tri

85

Opérateurs de jointure

100

Opérateurs de regroupement

104

Opérateurs d’initialisation

110

Opérateurs de conversion

115

Opérateurs dédiés aux éléments

122

Opérateurs de génération

126

Résumé

129

                         

5

Les opérateurs non différés

131

Espaces de noms référencés

131

Classes communes

131

 

134

Les opérateurs non différés, par groupes fonctionnels Opérateurs de conversion

134

Opérateurs d’égalité

145

Opérateurs agissant au niveau des éléments

148

Quantificateurs

160

Fonctions de comptage

165

Résumé

178

Table des matières

V

Partie III

LINQ to XML

6

Introduction à LINQ to XML

183

Introduction

185

 

185

Se passer de l’API W3C DOM XML Résumé

187

                                 

7

L’API LINQ to XML

189

Espaces de noms référencés

189

190

Améliorations de l’API

190

La construction fonctionnelle simplifie la création d’arbres XML L’élément, point central d’un objet XML

192

Noms, espaces de noms et préfixes

194

Extraction de valeurs de nœuds

196

Le modèle d’objet LINQ to XML

199

 

200

Exécution différée des requêtes, suppression de nœuds et bogue d’Halloween Création XML

202

Création d’éléments avec XElement

202

Création d’attributs avec XAttribute

205

Création de commentaires avec XComment

206

Création de conteneurs avec XContainer

207

Création de déclarations avec XDeclaration

207

Création de types de documents avec XDocumentType

208

Création de documents avec XDocument

209

Création de noms avec XName

210

Création d’espaces de noms avec XNamespace

211

Création de nœuds avec XNode

211

 

211

Création d’instructions de traitement avec XProcessingInstruction Création d’éléments streaming avec XStreamingElement

213

Création de textes avec XText

215

Définition d’un objet CData avec XCData

215

Sauvegarde de fichiers XML

216

Sauvegardes avec XDocument.Save()

216

Sauvegarde avec XElement.Save

217

Lecture de fichiers XML

218

Lecture avec XDocument.Load()

218

Lecture avec XElement.Load()

219

Extraction avec XDocument.Parse() ou XElement.Parse()

220

Déplacements XML

221

Propriétés de déplacement

222

Méthodes de déplacement

225

VI

Table des matières

Modification de données XML

238

Ajout de nœuds

238

Suppression de nœuds

242

Mise à jour de nœuds

245

 

248

XElement.SetElementValue() sur des objets enfants de XElement Attributs XML

250

Création d’un attribut

250

Déplacements dans un attribut

250

Modification d’attributs

253

Annotations XML

258

Ajout d’annotations avec XObject.AddAnnotation()

258

Accès aux annotations avec XObject.Annotation()

     

ou XObject.Annotations()

258

 

258

Suppression d’annotations avec XObject.RemoveAnnotations() Exemples d’annotations

259

Événements XML

262

XObject.Changing

262

XObject.Changed

262

Quelques exemples d’événements

263

Le bogue d’Halloween

267

Résumé

267

                   

8

Les opérateurs LINQ to XML

269

 

270

Introduction aux opérateurs LINQ to XML Opérateur Ancestors

270

Prototypes

270

Exemples

271

Opérateur AncestorsAndSelf

274

Prototypes

274

Exemples

275

Opérateur Attributes

277

Prototypes

277

Exemples

277

Opérateur DescendantNodes

279

Prototype

279

Exemple

279

Opérateur DescendantNodesAndSelf

280

Prototype

280

Exemple

281

Opérateur Descendants

282

Prototypes

282

Exemples

282

Opérateur DescendantsAndSelf

284

Prototypes

284

Exemples

284

Table des matières

VII

   

287

 

Opérateur Elements Prototypes

287

   

Exemples

287

   

289

 

Opérateur InDocumentOrder Prototype

289

 

Exemple

289

 

Opérateur Nodes

290

 

Prototype

290

 

Exemple

291

 

Opérateur Remove

292

 

Prototypes

292

 

Exemples

292

 

Résumé

294

                         

9

Les autres possibilités de XML

295

 

Espaces de noms référencés

295

 

Requêtes

296

   

296

 

La description du chemin n’est pas une obligation Une requête complexe

298

 

Transformations

303

   

Transformations avec XSLT

304

   

306

 

Transformations avec la construction fonctionnelle Astuces

308

 

Validation

314

 

Les méthodes d’extension

314

 

Prototypes

314

 

Obtention d’un schéma XML

315

 

Exemples

317

 

XPath

328

   

Prototypes

328

 

Résumé

329

                 

Partie IV

   
               

LINQ to DataSet

   

10

333

 

LINQ to DataSet Référence des assemblies

334

 

Espaces de noms référencés

334

   

334

 

Code commun utilisé dans les exemples Opérateurs dédiés aux DataRow

336

 

Opérateur Distinct

336

 

Opérateur Except

340

 

Opérateur Intersect

342

VIII

Table des matières

344

Opérateur Union

346

Opérateur SequencialEqual Opérateurs dédiés aux champs

347

Opérateur Field<T>

351

Opérateur SetField<T>

356

Opérateurs dédiés aux DataTable

359

Opérateur AsEnumerable

359

 

360

Opérateur CopyToDataTable<DataRow> Résumé

365

                           

11

Possibilités complémentaires des DataSet

367

Espaces de noms référencés

367

DataSets typés

367

Un exemple plus proche de la réalité

369

Résumé

372

             

Partie V

         
           

LINQ to SQL

       

12

Introduction à LINQ to SQL

377

Introduction à LINQ to SQL

378

La classe DataContext

380

Classes d’entités

381

Associations

382

Détection de conflit d’accès concurrentiel

383

Résolution de conflit d’accès concurrentiel

383

Prérequis pour exécuter les exemples

383

Obtenir la version appropriée de la base de données Northwind

384

Génération des classes d’entité de la base de données Northwind

384

Génération du fichier de mappage XML de la base de données Northwind

385

Utilisation de l’API LINQ to SQL

386

IQueryable<T>

386

Quelques méthodes communes

386

La méthode GetStringFromDb()

387

La méthode ExecuteStatementInDb()

388

Résumé

388

                           

13

391

Astuces et outils pour LINQ to SQL

391

Introduction aux astuces et aux outils pour LINQ to SQL Astuces

392

La propriété DataContext.Log

392

La méthode GetChangeSet()

393

 

393

Utilisation de classes partielles ou de fichiers de mappage Utilisation de méthodes partielles

394

Table des matières

IX

Outils

394

 

394

 

SQLMetal Le Concepteur Objet/Relationnel

401

 

414

Utiliser SQLMetal et le Concepteur O/R Résumé

415

                               

14

417

Opérations standard sur les bases de données Prérequis pour exécuter les exemples

417

Méthodes communes

418

Utilisation de l’API LINQ to SQL

418

Opérations standard de bases de données

418

Insertions

418

Requêtes

423

Mises à jour

446

 

Suppressions

450

 

453

Surcharger les méthodes de mise à jour des bases de données Surcharge de la méthode Insert

453

Surcharge de la méthode Update

454

Surcharge de la méthode Delete

454

Exemple

454

Surcharge dans le Concepteur Objet/Relationnel

457

Considérations

457

Traduction SQL

457

Résumé

459

                               

15

461

Les classes d’entité LINQ to SQL

461

Prérequis pour exécuter les exemples Les classes d’entité

461

Création de classes d’entité

462

 

493

 

Schéma de fichier de mappage externe XML Projection dans des classes d’entité/des classes de non-entité

494

         

Dans une projection, préférez l’initialisation

496

d’objet à la construction paramétrée

499

Extension des classes d’entité avec des méthodes partielles Les classes API importantes de System.Data.Linq

501

EntitySet<T>

502

EntityRef<T>

502

Table<T>

504

IExecuteResult

505

ISingleResult<T>

506

IMultipleResults

506

Résumé

508

                               

16

509

La classe DataContext Prérequis pour exécuter les exemples

509

X

Table des matières

509

Méthodes communes

509

Utilisation de l’API LINQ to SQL La classe [Your]DataContext

510

La classe DataContext

510

 

Principaux objectifs

513

 

520

Datacontext() et [Your]DataContext() SubmitChanges()

532

DatabaseExists()

539

CreateDatabase()

540

DeleteDatabase()

541

CreateMethodCallQuery()

542

ExecuteQuery()

543

Translate()

546

ExecuteCommand()

547

ExecuteMethodCall()

549

GetCommand()

557

GetChangeSet()

558

GetTable()

560

Refresh()

562

Résumé

568

                         

17

Les conflits d’accès concurrentiels

571

Prérequis pour exécuter les exemples

571

Méthodes communes

571

Utilisation de l’API LINQ to SQL

571

Conflits d’accès concurrentiels

571

Contrôle d’accès concurrentiel optimiste

572

 

Contrôle d’accès concurrentiel pessimiste

585

 

588

Une approche alternative pour les middle-tier et les serveurs Résumé

591

                         

18

Informations complémentaires sur SQL

593

593

Prérequis pour exécuter les exemples Utilisation de l’API LINQ to SQL

593

 

593

Utilisation de l’API LINQ to XML Les vues d’une base de données

593

Héritage des classes d’entité

595

Transactions

601

Résumé

603

Index

607

À 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 maga- zines 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 Olympi- ques, 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ébo- gage.

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éve- loppe 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.

Traducteur et relecteurs techniques

À propos du traducteur

Michel Martin est un passionné des technologies Microsoft. Nommé MVP par Micro- soft depuis 2003, il anime des ateliers de formation, réalise des CD-ROM d’autoforma- tion 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).

I

LINQ et C# 2008

1

Listing 1.1 : Hello Linq.

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
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

4

LINQ et C# 2008

Partie I

tableau de Strings 1 . 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). Remar- quez 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( @"<books> <book> <title>Pro LINQ: Language Integrated Query en C# 2008</title> <author>Joe Rattz</author> </book> <book> <title>Pro WF: Windows Workflow en .NET 3.0</title> <author>Bruce Bukovics</author> </book> <book> <title>Pro C# 2005 et la plateforme.NET 2.0, Troisième édition</title> <author>Andrew Troelsen</author> </book> </books>");

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
INFO

Si l’assembly System.Xml.Linq.dll n’apparaît pas dans les références du projet, ajoutez- la. 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".

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 exten- sions 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
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
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.

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 clas- ses d’entités qui pointent vers la base de données Northwind. Reportez-vous au Chapi- tre 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
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.

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étec- té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<T>, 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<int>. Comme vous le verrez dans la suite du livre, la plupart des méthodes LINQ retournent un IEnumerable<T>.

Dans les exemples étudiés jusqu’ici, toutes les requêtes ont retourné un IEnumerable<T> 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.

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<T> pour les opérateurs de requête standard. Vous l’utiliserez par exemple pour requêter des tableaux et des collec- tions 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 inter- face é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<T>, 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

Chapitre 1

Hello LINQ

9

semble pas assez flexible, vous devriez vous intéresser à LINQ to Entities ; en particu- lier 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, connectez- vous 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);

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();

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<LINQDev.HR.Employee>() .Select(e => new LINQDev.Common.Contact { Id = e.id, Name = string.Format("{0} {1}", e.firstName, e.lastName)

})

.ToArray<LINQDev.Common.Contact>();

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<Employee> en utilisant l’opéra- teur 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é.

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’interroga- tion 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é intro- duits, 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 compi- lation, 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<T> mais, parfois, en particulier lorsque vous commencez en program- mation 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);

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 mot- clé 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<nwind.Order>

{System.Data.Linq.DataQuery<nwind.Order>}

La

séquence

est

donc

de

type

nwind.Order. Il s’agit en fait d’un IQuerya-

ble<nwind.Order>, mais vous pouvez l’affecter à un IEnumerable<nwind.Order>, puisque IQueryable<T> hérite de IEnumerable<T>.

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<Order> 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
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).

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<T>. Aucune des collections héri- té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<T> : Cast et OfType (voir Listing 1.9).

Listing 1.9 : Conversion d’une collection héritée en un IEnumerable<T> 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<string> names = arrayList.Cast<string>().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<string> names = arrayList.OfType<string>().Where(n => n.Length < 7); foreach(string name in names) Console.WriteLine(name);

Ces deux exemples produisent le même résultat :

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<T>, 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’initialisa- tion 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.

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<string> 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 parti- culier 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 IEnu- merable<T>, 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’IEnumera- ble<T> retourné dans une variable. Une instruction foreach est alors appliquée sur la séquence IEnumerable<T> à 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éces- saires. 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.

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.Data- Context. 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<Order> 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

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 ajou- tez à 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.

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é quel- ques exemples pour attiser votre appétit et des astuces qui pourront vous paraître quel- que 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.

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 utili- sation.

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éri- que 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.

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 anony- mes, afin de passer du code comme argument et ainsi d’éviter l’utilisation de délégués.

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 virgules 1 , 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.

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 équi- valente à 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);

}

AATTENTIONTTENTION
AATTENTIONTTENTION

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.

24

LINQ et C# 2008

Partie I

En

Listing 2.3.

utilisant

une

expression

lambda,

l’exemple

précédent

se

transforme

en

le

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
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

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 expres- sions 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<int> 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 poten- tiellement à 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<T> Where<T>( this IEnumerable<T> source, Func<T, bool> predicate);

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<T> Where<T>( this IQueryable<T> source, System.Linq.Expressions.Expression<Func<int, bool>> 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’expres- sions pour représenter les données.

Les opérateurs qui admettent une séquence IEnumerable<T> comme premier argument utilisent des délégués pour manipuler les expressions lambda. En revanche, les opéra- teurs qui admettent une séquence IQueryable<T> comme premier argument utilisent des arbres d’expressions.

INFO
INFO

Le compilateur produit du code IL pour les méthodes d’extension des séquences IEnumera- ble<T>, alors qu’il produit des arbres d’expressions pour les méthodes d’extension des

séquences IQueryable<T>.

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 nouveau- tés du langage C#, je vais vous les présenter simultanément. Examinez la déclaration ci- après :

var 1 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.

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 nouveau- té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 sous- jacent : 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’initia- liser.

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 à

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 implici- tement 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 unnamedType- Var, 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
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.

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<Address> 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’instan- ciation 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.

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 éven- tuels 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<T>. Les collections C# héri-

té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<string> presidents = new List<string> { "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 affec- tée à un objet d’un type anonyme doit pouvoir le déclarer. C’est là qu’intervient le mot- clé new de C# 3.0.

Un type anonyme ne peut pas être estimé s’il est issu d’un opérateur Select ou Select- Many. Sans les types anonymes, des classes nommées devraient être définies pour rece-

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’omet- tre 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.

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 varia- ble 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 exem- ple 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 proto- type 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

Chapitre 2

Améliorations de C# 3.0 pour LINQ

33

objet

Listing 2.13).

string. Essayons d’invoquer cette méthode sur un objet

string (voir

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