Class Helper, LINQ et fichiers CSV 30. septembre 2008 Olivier C#, LINQ (0) C# 3.0 a apporté des nouveautés syntaxiques qui ne se bornent pas à être seulement des petits suppléments qu'on peut ignorer, au contraire il s'agit de modifications de premier plan qui impactent en profondeur la façon d'écrire du code si on sait tirer partie de ces nouveautés. J'en ai souvent parlé, au travers de billets de ce blog ou d'articles comme celui décrivant justement les nouveautés syntaxiques de C#3.0. Je ne vais donc pas faire de redites, je suppose que vous connaissez maintenant tous ces ajouts au langage. Ce que je veux vous montrer c'est qu'en utilisant correctement C# 3.0 on peut écrire un code incroyable concis et clair pour résoudre les problèmes qui se posent au quotidien au développeur. Le problème à résoudre Vous recevez un fichier CSV, disons le résultat d'une exportation depuis un soft XY des ventes à l'export de votre société. Vous devez rapidement ajouter la possibilité d'importer ces données dans l'une de vos applications mais après les avoir interprétées, triées, voire filtrées. La méthode la plus simple Rappel : un fichier CSV est formé de lignes comportant plusieurs champs séparés par des virgules. Les champs peuvent être délimités par des double quotes. A partir de cette description voyons comment avec une requête LINQ nous pouvons lire les données, les filtrer, les trier et les mettre en forme. Le but ici sera de générer en sortie une chaîne par enregistrement, chaîne contenant des champs "paddés" par des espaces pour créer un colonnage fixe. On tiendra compte des lignes débutant par le symbole dièse qui sont considérées comme des commentaires. 1: string[] lines = File.ReadAllLines("Export Sales Info-demo.csv"); 2: var t1 = lines 3: .Where(l => !l.StartsWith("#")) 4: .Select(l => l.Split(',')) 5: .OrderBy(items=>items[0]) 6: .Select(items => String.Format("{0}{1}{2}", 7: items[1].PadRight(15), 8: items[2].PadRight(30), 9: items[3].PadRight(10))); 10: var t2 = t1 11: .Select(l => l.ToUpper()); 12: foreach (var t in t2.Take(5)) 13: Console.WriteLine(t); La sortie (avec le fichier exemple fourni) sera : SAN JOSE CITY CENTER LODGE CA SAN FRANCISCO KWIK-E-MART CA SEATTLE LITTLE CORNER SWEETS WA SEATTLE LITTLE CORNER SWEETS WA IRVINE PENNY TREE FOODS CORPORATION CA Cette méthode, très simple, ne réclame rien d'autre que le code ci-dessus. La solution est applicable à tout moment et s'adapte facilement à toute sorte de fichiers sources. Elle possède malgré tout quelques faiblesses. Par exemple les champs contenant des doubles quotes ou les champs mal formés risquent de faire échouer la séquence. Dans certains cas cela sera inacceptable, dans d'autres on pourra parfaitement protéger la séquence dans un bloc try/catch et rejeter les fichiers mal formés. Une fois encore il ne s'agit pas ici de montrer un code parfaitement adapté à un problème précis, mais bien de montrer l'esprit, la façon d'utiliser C# 3.0 pour résoudre des problèmes courants. Expliquons un peu ce code : la ligne 1 charge la totalité du fichier CSV dans un tableau de strings. La méthode peut sembler "brutale" mais en réalité elle est souvent très acceptable car de tels fichiers dépassent rarement les quelques dizaines ou centaines de Ko et la RAM de nos machines modernes n'impose en rien une lecture bufferisée, tout peut tenir d'un bloc en mémoire sans le moindre souci. Cela nous arrange ici il faut l'avouer mais l'utilisation d'un flux bufferisé resterait parfaitement possible. Nous disposons maintenant d'un tableau de chaînes, chacune étant formatée en CSV. La première requête LINQ (variable "t1" en ligne 2) fait l'essentiel du travail : gestion des commentaires (ligne 3) décomposition des champs (ligne 4) tri sur l'un des champs (ligne 5) génération d'une sortie formatée (lignes 6 à 9) Ce qui est merveilleux ici c'est que nous avons en quelques lignes un concentré de ce qu'est une application informatique : acquisition de données, traitement de ces données, production de sorties exploitables par un être humain ou une autre application. En si peu de lignes nous avons réalisé ce qui aurait nécessité une application entière avec les langages de la génération précédente comme C++ ou Delphi. C'est bien ce bond en avant que représente C# 3.0 qui est ici le vrai sujet du billet. Une méthode plus complète La séquence que nous avons vu plus haut répond au problème posé. Elle possède quelques lacunes. Celles liées à sa trop grande simplicité (certains cas du parsing CSV ne sont pas correctement pris en compte) et celles liées à sa forme : c'est un bout de code qu'il faudra copier/coller pour le réutiliser et qui viendra "polluer" nos requêtes LINQ les rendant plus difficiles à lire. Si ces lacunes sont acceptables dans certains cas (règlement ponctuel d'un problème ponctuel) dans d'autres cas on préfèrera une approche plus facilement réutilisable. Notamment si nous sommes amenés à traiter plus ou moins souvent des fichiers CSV nous avons intérêt à encapsuler le plus possible le parsing et à le rendre plus facilement reexploitable. L'une des voies serait de créer une classe totalement dédiée à cette tâche. C'est une solution envisageable mais elle est assez lourde et amène son lots de difficultés. Nous allons choisir ici une autre approche, celle de la création d'un class helper. C'est à dire que nous souhaitons non pas créer une classe qui traite un fichier CSV comme un tout, mais nous voulons pouvoir parser n'importe quelle chaîne de caractères formatée en CSV. L'approche est très différente. Dans le premier cas il nous faudra complexifier de plus en plus notre classe, voire créer une hiérarchie de classes pour traiter les fichiers CSV mais aussi les flux mémoire CSV, et puis encore les services Web retournant du CSV, etc, etc. Dans le second cas nous allons plutôt ajouter la capacité à la classe string de parser une chaîne donnée. La source de la chaîne ne nous importe pas. Il pourra s'agir d'un élément d'un tableau de chaîne comme dans le premier exemple autant que d'une chaîne lue depuis un flux mémoire, une section data d'un fichier XML, etc. D'un certain sens nous acceptons d'être moins "complet" que l'option "classe dédiée CSV", mais nous gagnons en agilité et en "réutilisabilité" en faisant abstraction de la provenance de la chaîne à parser. Bien entendu nous pouvons nous offrir le luxe d'une telle approche car nous savons que nous pouvons nous reposer sur C# 3.0, ses class helpers et LINQ... Le projet qui accompagne ce billet contient tout le code nécessaire et même un fichier CSV d'exemple, nous n'entrerons pas dans les détails de l'implémentation du class helper lui-même qui étend la classe string, parser du CSV n'est qu'un prétexte sans plus d'intérêt dans ce billet. Le code utilisé pour l'exemple provient d'ailleurs d'un billet de Eric White dont vous pouvez visiter le blog si vous lisez l'anglais. la façon d'écrire un class helper est décrite dans mon article sur C# 3.0, regardons juste sa déclaration : public static string[] CsvSplit(this String source) Cette méthode est déclarée au sein d'une classe statique de type "boîte à outils" pouvant centraliser d'autres méthodes utilitaires. La déclaration nous montre que le class helper s'applique à la classe String uniquement (this String source) et qu'elle retourne un tableau de chaîne (string[]). Une fois le class helper défini (code complet dans le projet accompagnant l'article) il nous est possible d'écrire des requêtes LINQ du même type que celle du premier exemple. Mais cette fois-ci le parsing CSV est réalisé par le class helper ce qui permet à la fois de le rendre plus sophistiqué et surtout de ne plus avoir à le copier/coller dans les requêtes LINQ... Voici un exemple d'utilisation du class helper sur le même fichier CSV. Ici nous parsons la source, nous la trions, nous éliminons les lignes de commentaire mais aussi nous créons en réponse une classe anonyme contenant le nom du contact, sa ville et le nom de sa société. Il est dès lors possible de traiter la liste d'objets résultat comme n'importe quelle liste : affichage, traitement, génération d'état, etc. 1: var data = File.ReadAllLines("Export Sales Info-demo.csv").Where(s=>!s.StartsWith("#")) 2: .Select(l => 3: { 4: var split = l.CsvSplit(); 5: return new 6: { 7: Contact = split[0], 8: City = split[1], 9: Company = split[2] 10: }; 11: } ).OrderBy(x=>x.Contact); 12: 13: foreach (var d in data.Take(5)) 14: Console.WriteLine(d); Ce qui, avec le même fichier source, affichera à la console : { Contact = Allen James, City = San Jose, Company = City Center Lodge } { Contact = Bart H. Perryman, City = San Francisco, Company = Kwik-e-mart } { Contact = Beth Munin, City = Seattle, Company = Little Corner Sweets } { Contact = Beth Munin, City = Seattle, Company = Little Corner Sweets } { Contact = Bruce Calaway, City = Irvine, Company = Penny Tree Foods Corporation } Conclusion La bonne utilisation de C# 3.0 permet de réduire significativement la taille du code donc de réduire dans les mêmes proportions les bugs et d'augmenter la productivité. Apprendre à utiliser cette nouvelle approche c'est gagner sur tous les plans... Le projet exemple : LinqToCsv.zip (9,46 kb)
Simplifier les gestionnaires d'événement grâce aux expressions Lambda 7. septembre 2008 Olivier C# (0) Les expressions Lambda ont été introduites dans C# 3.0. Utilisées correctement, tout comme LINQ to Object, elles permettent une grande simplification du code entraînant dans un cercle vertueux une meilleure lisibilité de ce dernier favorisant maintenabilité et fiabilité de ce même code. Il n'y a donc aucune raison de rester "frileux" vis à vis de cette nouveauté syntaxique comme encore trop de développeurs que je rencontre dans mes formations ou ailleurs. Pour démontrer cette souplesse, voici un petit exemple qui valide un document XML en fonction d'un schéma. La méthode Validate() de la classe XDocument attend en paramètre un delegate de type ValidationEventHandler. Dans un code très court il n'est pas forcément judicieux de déclarer une méthode juste pour passer son nom en paramètre à Validate(). Les "sous procédures" ou procédures imbriquées de Pascal n'existent pas en C# et l'obligation de déclarer à chaque fois des méthodes private pour décomposer la moindre action est une lourdeur syntaxique de ce langage dont on aimerait se passer parfois (une méthode même private est visible par toutes les autres méthodes de la classe ce qui n'est pas toujours souhaitable. Seules les méthodes imbriquées de Pascal sont des "méthodes privées de méthodes"). En fin de billet vous trouverez d'ailleurs le lien vers un autre billet dans lequel j'explique comment utiliser les expressions Lambda en les nommant pour retrouver la souplesse des procédures imbriquées. Même si "l'astuce" présentée ici peut y ressembler dans l'esprit, dans la pratique les choses sont assez différentes puisque nous n'utiliserons pas une expression nommée. Revenons à l'exemple, il s'agit de valider un document XML à partir d'un schéma. Et de le faire de la façon la plus simple possible, c'est à dire en évitant l'écriture d'un delegate, donc en utilisant directement une expression Lambda en paramètre de Validate(). Supposons que nous ayons déjà le schéma (variable schema) et le document XML (variable doc), l'appel à Validate pourra donc s'écrire : doc.Validate(schema,(obj, e) => errors += e.Message + Environment.NewLine); "errors" est déclarée comme une chaîne de caractères. A chaque éventuelle erreur détectée par la validation l'expression concatène le message d'erreur à cette dernière. En fin de validation il suffit d'afficher errors pour avoir la liste des erreurs. Mais cela n'est qu'un exemple d'utilisation de l'expression Lambda. Ce qui compte c'est bien entendu de comprendre l'utilisation de cette dernière en place et lieu d'un delegate de tout type, ici ValidationEventHandler d'où les paramètres (obj,e) puisque ce delegate est déclaré de cette façon (un objet et un argument spécifique à cet handler). C'est simple, nul besoin de déclarer un gestionnaire d'événement donc une méthode avec un nom et une visibilité. On évite que cette méthode soit réutilisée dans le code de la classe considérée (si elle existe on est tenté de la réutiliser mais sa stratégie d'écriture n'est pas forcément adaptée à une telle utilisation d'où possible bug), etc. Que des avantages donc. Pour plus d'information vous pouvez lire mon article sur les nouveautés syntaxiques de C# 3.0 ainsi que mon billet montrant comment retrouver en C# le bénéfice des procédures imbriquées de Pascal grâce aux expressions Lambda. Si vous voulez jouez avec l'exemple, vous pouvez télécharger le projet VS 2008 qui montre tout cela : lambda.zip (21,71 kb)
Le Mythe du StringBuilder 29. août 2008 Olivier C#, Méthodologie (1) Sur l'excellent blog du non moins excellent Mitsu, dans l'une de mes interventions sur l'un de ses (excellents aussi) petits quizz LINQ, j'aurai parlé du "Mythe du StringBuilder". Cela semble avoir choqué certains lecteurs qui m'en ont fait part directement. Le sujet est très intéressant et loin de toute polémique j'en profiterais donc ici pour préciser le fond de cet avis sur le StringBuilder et aussi corriger ce qui semble être une erreur de lecture (trop rapide?) de la part de ces lecteurs. J'ai même reçu un petit topo sur les avantages de StringBuilder (un bench intéressant par ailleurs). D'abord, plantons le décor Le billet de Mitsu dans lequel je suis intervenu se trouve ici. Dans cet échange nous parlions en fait de la gestion des Exceptions que l'un des intervenants disait ne pas apprécier en raison de leur impact négatif en terme de performance. Et c'est là que j'ai répondu : "...Dans la réalité je n'ai jamais vu une application (java, delphi ou C#) "ramer" à cause d'une gestion d'exception, c'est un mythe à mon sens du même ordre que celui qui veut que sous .NET il faut systématiquement utiliser un StringBuilder pour faire une concaténation de chaînes." Comme on le voit ici, seule une lecture un peu trop rapide a pu faire croire à certains lecteurs que je taxe les bénéfices du StringBuilder de "mythe". Je pensais que la phrase était assez claire et qu'il était évident que si mythe il y a, c'est dans le bénéfice systématique du StringBuilder... Nuance. Grosse nuance. Mythe ou pas ? La gestion des exceptions est un outil fantastique "légèrement" plus sophistiqué que le "ON ERROR GOTO" de mon premier Basic Microsoft interprété sous CP/M il y a bien longtemps, mais le principe est le même. Et si en 25 ans d'évolution, la sélection darwinienne a conservé le principe malgré le bond technologique c'est que c'est utile... Est-ce coûteux ? Je répondrais que la question n'a pas de sens... Car quel est le référenciel ? Les développeurs ont toujours tendance à se focaliser sur le code, voire à s'enfermer dans les comparaisons en millisecondes et en octets oubliant que leur code s'exprime dans un ensemble plus vaste qui est une application complète et que celle-ci, pour être professionnelle a des impératifs très éloignés de ce genre de débat très techniques. Notamment, une application professionnelle se doit avant tout de répondre à 3 critères : Répondre au cachier des charges, avoir un code lisible, être maintenable. De fait, si les performances ne doivent pas être négligées pour autant, le "coût" d'une syntaxe, de l'emploi d'un code ou d'un autre, doit être "pesé" à l'aulne d'un ensemble de critères où celui de la pure performance ne joue qu'un rôle accessoire dans la grande majorité des applications. Non pas que les performances ne comptent pas, mais plutôt que face à certaines "optimisations" plus ou moins obscures, on préfèrera toujours un code moins optimisé s'il est plus maintenable et plus lisible. Dès lors, le "coût" de l'utilisation des exceptions doit être évalué au regard de l'ensemble de ces critères où les millisecondes ne sont pas l'argument essentiel. De la bonne mesure Tout est affaire de bonne mesure dans la vie et cela reste vrai en informatique. Si une portion de code effectue une boucle de plusieurs milliers de passages, et si ce traitement doit absolument être optimisé à la milliseconde près, alors, en effet, il sera préférable d'éviter une gestion d'exception au profit d'un ou deux tests supplémentaires (tester une valeur à zéro avant de s'en servir pour diviser au lieu de mettre la division dans un bloc Try/Catch par exemple). Mais comme on le voit ici, il y plusieurs "si" à tout cela. Et comment juger dans l'absolu si on se trouve dans le "bon cas" ou le "mauvais cas" ? Loin de la zone frontière, aux extrèmes, il est toujours facile de décider : une boucle de dix millions de passages avec une division ira plus vite avec un test sur le diviseur qu'avec un Try/Catch (si l'exception est souvent lancée, encore un gros "si" !). De même, un Try/Catch pontuel sur un chargement de document sur lequel l'utilisateur va ensuite travailler de longues minutes n'aura absolument aucun impact sur les performances globales du logiciel. Mais lorsqu'on approche de la "frontière", c'est là que commence la polémique, chacun pouvant argumenter en se plaçant du point de vue performance ou du point de vue qualité globale du logiciel. Et souvent chacun aura raison sans arriver à le faire admettre à l'autre, bien entendu (un informaticien ne change pas d'avis comme ça :-) ) ! Où est alors la "bonne mesure" ? ... La meilleure mesure dans l'existence c'est vous et votre intelligence. Votre libre arbitre. Il n'y a donc aucun "dogme" qui puisse tenir ni aucune "pensée unique" à laquelle vous devez vous plier. A vous de juger, selon chaque cas particulier si une gestion d'exception est "coûteuse" ou non. N'écoutez pas ceux qui voudraient que vous en mettiez partout, mais n'écoutez pas non plus ceux qui les diabolisent ! Et le StringBuilder dans tout ça ? Si dans mon intervention sur le blog de Mitsu j'associais gestion des exceptions et StringBuilder c'est parce qu'on peut en dire exactement les mêmes choses ! Dans les cas extrêmes de grosses boucles il est fort simple de voir que le StringBuilder est bien plus performant qu'une concaténation de chaînes avec "+". Cela ne se discute même pas. Mais, comme pour la gestion des exceptions, c'est lorsqu'on arrive aux "frontières" que la polémique pointe son nez. Pour concaténer quelques chaînes le "+" est toujours une solution acceptable car le StringBuilder a un coût, celui de son instanciation et du code qu'il faut écrire pour le gérer. Il s'agit là des cas les plus fréquents. On concatène bien plus souvent quelques chaines de caractères dans un soft qu'on écrit des boucles pour en concaténer 1 million le tout dans un traitement time critical... (encore des tas de "si" !). Même du point de vue de la mémoire les choses ne sont pas si simple. Le StringBuilder utilise un buffer qu'il double quand sa capacité est dépassée. Dans certains cas courants (petites boucles dans lesquelles il y a quelques concaténation), le stress mémoire infligé par le StringBuilder peut être très supérieur à celui de l'utilisation de "+" ou de String.Concat. C'est dans ces cas les plus fréquents que l'utilisation du StringBuilder comme panacée apparaît n'être qu'un mythe. Conclusion Vous et moi écrivons du code, ce code se destine à un client / utilisateur. La première exigence de ce dernier est que le logiciel réponde au cahier des charges et qu'il fonctionne sans bug gênant. Cela impose de votre côté que vous utilisiez une "stylistique" rendant le code lisible et facilement maintenable car un code sans bug n'existe pas et qu'il faudra tôt ou tard intervenir ici ou là. Dans un tel contexte réaliste, ce ne sont pas les millisecondes qui comptent ni les octets, mais bien la qualité globale de l'application. Et c'est alors que les dogmes techniques tombent. Car ils n'ont d'existence que dans un idéal purement technique qui fait abstraction de la réalité. Un Try/Catch ou un StringBuilder ne peuvent pas être étudiés en tant que tels en oubliant les conditions plus vastes de l'application où ils apparaissent. Leur impact n'a de sens que compris comme l'une des milliers d'autres lignes de code d'une application qui doit répondre à un autre critère, celui de la qualité et de la maintenabilité. Un seul juge existe pour trancher car chaque cas est particulier : vous. Pour ceux qui veulent jouer un peu avec les StringBuilder, voici un mini projet que vous n'aurez qu'à modifier pour changer les conditions de tests : ConsoleApplication2.zip (6,10 kb)
Mettre des données en forme en une requête LINQ 19. août 2008 Olivier C#, LINQ (0) Losqu'on traite de LINQ, la majorité des exemples utilisent des sources de données "bien formées" : flux RSS (mon dernier billet), collections d'objets, etc. Mais dans la "vraie vie" les sources de données à traiter sont parfois moins "lisses", moins bien formées. LINQ peut aussi apporter son aide dans de tels cas pour transformer des données brutes en collection d'objets ayant un sens. C'est ce que nous allons voir dans ce billet. Prenons un exemple simple : imaginons que nous récupérons une liste de rendez-vous depuis une autre application, cette liste est constituée de champs qui se suivent, les initiales de la personne, son nom et l'heure du rendez-vous. Tous les rendez-vous se suivent en une longue liste de ces trois champs mais sans aucune notion de groupe ou d'objet. Pour simplifier l'exemple, fixons une telle liste de rendez-vous dans un tableau de chaînes : string[] data = new[] { "OD", "Dahan", "18:00", "MF", "Furuta", "12:00", "BG", "Gates", "10:00" }; La question est alors comment extraire de cette liste "linéaire" les trois objets représentant les rendez-vous ? La réponse passe par trois astuces : L'utilisation de la méthode Select de LINQ qui sait retourner l'index de l'entité traitée La syntaxe très souple de LINQ permettant d'utiliser des expressions LINQ dans les valeurs retournées Et bien entendu la possibilité de créer des types anonymes Ce qui donne la requête suivante : var personnes = data.Select ( (s, i) => new { Value = s, Bloc = i / 3 } ).GroupBy(x => x.Bloc) .Select ( g => new { Initiales = g.First().Value, Nom = g.Skip(1).First().Value, RendezVous = g.Skip(2).First().Value } ); L'énumération suivante : foreach (var p in personnes.OrderBy(p=>p.Nom)) Console.WriteLine(p); donnera alors cette sortie console de toutes les personnes ayant un rendez-vous, classées par ordre alphabétique de leur nom : { Initiales = OD, Nom = Dahan, RendezVous = 18:00 } { Initiales = MF, Nom = Furuta, RendezVous = 12:00 } { Initiales = BG, Nom = Gates, RendezVous = 10:00 } Voici des objets bien formés (on pourrait ajouter un DateTime.Parse à la création du champ RendezVous pour récupérer une heure plutôt qu'une chaîne) qui pourront être utilisés pour des traitements, des affichages, une exportation, etc... LINQ to Object ajoute une telle puissance à C# que savoir s'en servir au quotidien pour résoudre tous les petits problèmes de développement qui se posent permet réellement d'augmenter la productivité, ce que tous les langages et IDE promettent falacieusement (aucune mesure de ce supposé gain n'existe). Ici, essayez d'écrire le même code sans LINQ, vous verrez tout suite que le gain de productivité et de fiabilité est bien réel, et que la maintenance aura forcément un coup moindre. Pour d'autres infos, Stay Tuned ! Le projet VS 2008 : LinqChunk.zip (5,35 kb)
Nouveau tutorial : RDL et ReportViewer, une solution gratuite de génération d'état 14. août 2008 Olivier C#, Visual Studio (12) RDL ? Cela ne vous dit rien ? SSRS ou BIDS non plus ? Et le composant gratuit de Microsoft ReportVie [Plus]