Les optimisations du compilateurs C# ne sont pas un sujet de discussion très courant, en tout cas on voit très nettement que le fait d'avoir quitté l'environnement Win32 pour l'environnement managé de .NET a fait changer certaines obsessions des codeurs... Ce n'est pas pour autant que le sujet a moins d'intérêt ! Nous allons voir cela au travers d'une optimisation particulière appelée "tail-calling" (appel de queue, mais je n'ai pas trouvé de traduction française, si quelqu'un la connaît, qu'il laisse un commentaire au billet).
Principe
On appelle "tail-calling" un mécanisme d'optimisation du compilateur qui permet d'économiser les instructions exécutées en même temps que des accès à la pile. Les circonstances dans lesquelles le compilateur peut utiliser cette optimisation sont celles où une méthode se termine par l'appel d'une autre, d'où le nom de tail-calling (appel de queue).
Prenons l'exemple suivant :
static public void Main()
{
Go();
}
static public void Go()
{
Première();
Seconde();
Troisième();
}
static public void Troisième()
{
}
Dans cet exemple le compilateur peut transformer l'appel de Troisième() en un appel de queue (tail-calling). Pour mieux comprendre regardons l'état de la pile au moment de l'exécution de Seconde() : Seconde()-Go()-Main()
Quand Troisième() est exécutée il devient possible, au lieu d'allouer un nouvel emplacement sur la pile pour cette méthode, de simplement remplacer l'entrée de Go() par Troisième(). La pile ressemble alors à Troisième()-Main().
Quand Troisième() se terminera elle passera l'exécution à Main() au lieu de transférer le trait à Seconde() qui immédiatement devra le transférer à Main().
C'est une optimisation assez simple qui, cumulée tout au long d'une application, et ajoutée aux autres optimisations, permet de rendre le code exécutable plus efficace.
Quand l'optimisation est-elle effectuée ?
La question est alors de savoir quand le compilateur décide d'appliquer l'optimisation de tail-calling. Mais dans un premier temps il faut se demander de quel compilateur nous parlons.... Il y existe en effet deux compilateurs dans .NET, le premier prend le code source pour le compiler en IL alors que le second, le JIT, utilisera ce code IL pour créer le code natif. La compilation en IL peut éventuellement placer certains indices qui permettront de mieux repérer les cas où le tail-calling est applicable mais c'est bien entendu dans le JIT que cette optimisation s'effectue.
Il existe de nombreuses règles permettant au JIT de décider s'il peut ou non effectuer l'optimisation. Voici un exemple de règles qui font qu'il n'est pas possible d'utiliser le tail-calling (par force cette liste peut varier d'une implémentation à l'autre du JIT) :
- L'appelant ne retourne pas directement après l'appel;
- Il y a une incompatibilité des arguments passés sur la pile entre l'appelant et l'appelé ce qui imposerait une modification des arguments pour appliquer le tail-calling;
- L'appelant et l'appelé n'ont pas le même type de retour (données de type différents, void);
- L'appel est est transformé en inline, l'inlining étant plus efficace que le tail-calling et ouvrant la voie à d'autres optimisations;
- La sécurité interdit ponctuellement d'utiliser l'optimisation;
- Le compilateur, le profiler, la configuration ont coupé les optimisations du JIT.
Pour voir la liste complète des règles, jetez un oeil à ce post.
Intérêt de connaitre cette optimisation ?
Normalement les optimisations du JIT ne sont pas un sujet intéressant au premier chef le développeur. D'abord parce qu'un environnement managé comme .NET fait qu'à la limite ce sont les optimisations du code IL qui regarde directement le développeur et beaucoup moins la compilation native qui peut varier d'une plateforme à l'autre pour une même application. Ensuite il n'est pas forcément judicieux de se reposer sur les optimisations du JIT puisque, justement, ce dernier peut être différent sans que l'application ne le sache.
Qui s'intéresse à l'optimisation du tail-calling alors ? Si vous écrivez un profiler c'est une information intéressante, mais on n'écrit pas un tel outil tous les jours... Mais l'information est intéressante lorsque vous déboguez une application car vous pouvez vous trouver face à une pile d'appel qui vous semble "bizarre" ou "défaillante" car il lui manque l'une des méthodes appelées !
Et c'est là que savoir qu'il ne faut pas chercher dans cette direction pour trouver le bug peut vous faire gagner beaucoup de temps... Savoir reconnaître l'optimisation de tail-calling évite ainsi de s'arracher les cheveux dans une session de debug un peu compliquée si on tombe en plus sur un pile d'appel optimisée. Un bon debug consiste à ne pas chercher là où ça ne sert à rien (ou à chercher là où c'est utile, mais c'est parfois plus difficile à déterminer !), alors rappelez-vous du tail-calling !
Et stay tuned !
nota: le présent billet est basé sur un post de l'excellent blog ASP.NET Debugging
Comment recenser toutes les utilisations d'une classe précise dans une grosse solution pleine de projets ?
Certains proposeront d'utiliser la fonction "find usage" de Resharper. Certes mais tout le monde n'a pas cet add-in. Et même si vous l'avez, vous n'êtes pas sûr que là où vous aurez à intervenir il sera toujours là...
D'autres proposeront le Ctrl-F. C'est pas mal mais ça trouvera aussi les bouts de texte qui citent la classe ou qui contiennent le nom de cette dernière. Les plus torturés proposeront alors d'utiliser une expression régulière. Techniquement c'est mieux mais concevoir une belle ER qui fasse bien le boulot, tout le monde ne sait pas forcément faire.
Non, moi je vous parle d'un moyen ultra simple et absolument sûr de trouver toutes les utilisations d'une classe dans des tas projets en quelques secondes sans trop se fatiguer.
... Vous séchez ? Alors voici la réponse : l'attribut Obsolete.
C'est tout bête, c'est une utilisation un peu détournée de la chose il faut l'avouer, mais il suffit d'ajouter devant la définition de la classe en question l'attribut
[Obsolete("blabla")]
public class TheClassARepérer ...
et l'affaire est jouée. Faites un Rebuild de la solution et dans les warnings vous aurez la liste de tous les endroits où la classe est utilisée. Un double-clic vous amènera directement dans le code en question.
Quand l'opération est terminée, il suffit de supprimer l'attribut. La manip est ultra légère, peu de chance d'introduire un bug, et si on onblit l'attribut ça se verra tout de suite dans les warnings.
Malin non ?
Alors Stay Tuned !
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)
Les dictionnaires sont des listes spécialisées permettant de relier deux objets, le premier étant considéré comme la clé, le second comme la valeur. Une fois clé et valeur associées au sein d'une entrée du dictionnaire ce dernier est capable de retourner rapidement la valeur de toute clé. Les dictionnaires peuvent être utilisés en de nombreuses circonstances, comme la conception de caches de données par exemple.
Un dictionnaire se créée à partir de la classe générique Dictionnary<Key,Value>. Comme on le remarque si la clé peut être de tout type elle reste monolithique, pas de clés composées donc, et encore moins de classes telles Dictionnary<Key1,Key2,Value> ou Dictionnary<Key1,Key2,Key3,Value> etc...
Or, il est assez fréquent qu'une clé soit composée (multi-part key ou composed key). Comment utiliser les dictionnaires génériques dans un tel cas ?
La réponse est simple : ne confondons pas une seule clé et un seul objet objet clé ! En effet, si le dictionnaire n'accèpte qu'un seul objet pour la partie clé, rien n'interdit que cet objet soit aussi complexe qu'on le désire... Il peut donc s'agir d'instances d'une classe créée pour l'occasion, classe capable de maintenir une clé composée.
Vous allez me dire que ce n'est pas bien compliqué, et vous n'aurez qu'à moitié raison...
Créer une classe qui contient 2 propriétés n'est effectivement pas vraiment ardu. Prenons un exemple simple d'un dictionnaire associant des ressources à des utilisateurs. Imaginons que l'utilisateur soit repéré grâce à deux informations, son nom et une clé numérique (le hash d'un password par ex) et imaginons, pour simplifier, que la ressource associée soit une simple chaîne de caractères.
La classe qui jouera le rôle de clé du dictionnaire peut ainsi s'écrire en une poignée de lignes :
1: public class LaClé
2: {
3: public string Name { get; set; }
4: public int PassKey {get; set; }
5: }
Oui, c'est vraiment simple. Mais il y a un hic !
En effet, cette classe ne gère pas l'égalité, elle n'est pas "comparable". De base, écrite comme ci-dessus, elle ne peut pas servir de clé à un dictionnaire...
Pour être utilisable dans un tel contexte il faut ajouter du code afin de gérer la comparaison entre deux instances. Il existe plusieurs façons de faire, l'une de celle que je préfère est l'implémentation de l'interface générique IEquatable<T>. On pourrait par exemple choisir une autre voie en implémentant dans la classe clé une autre classe implémentant IEqualityComparer<T>. Toutefois dans un tel cas il faudrait préciser au dictionnaire lors de sa création qu'il lui faut utiliser ce comparateur là bien précis, cela est très pratique si on veut changer de comparateur à la volée, mais c'est un cas assez rare. En revanche si demain l'implémentation changeait dans notre logiciel et qu'une autre structure soit substituée au dictionnaire il y aurait de gros risque que l'application ne marche plus: les objets clés ne seraient toujours pas comparables deux à deux "automatiquement".
L'implémentation d'une classe utilisant IEqualityComparer<T> est ainsi une solution partielle en ce sens qu'elle réclame une action volontaire pour être active. De plus cette solution se limite aux cas où un comparateur de valeur peut être indiqué.
C'est pour cela que je vous conseille fortement d'implémenter directement dans la classe "clé" l'interface IEquatable<T>. Quelles que soient les utilisations de la classe dans votre application l'égalité fonctionnera toujours sans avoir à vous soucier de quoi que ce soit, et encore moins, et surtout, des éventuelles évolutions du code.
Comme par enchantement l'excellent Resharper (add-in pour VS totalement indispensable) sait générer automatiquement tout le code nécessaire, je n'ai donc pas eu grand chose à saisir pour le code final... Ceux qui ne disposent pas de cet outil merveilleux pourront bien entendu s'inspirer de l'implémentation proposée pour leur propre code.
Le code de notre classe "clé" se transforme ainsi en quelque chose d'un peu plus volumineux mais de totalement fonctionnel :
1: public class ComposedKey : IEquatable<ComposedKey>
2: {
3: private string name;
4: public string Name
5: {
6: get { return name; }
7: set { name = value; }
8: }
9:
10: private int passKey;
11: public int PassKey
12: {
13: get { return passKey; }
14: set { passKey = value; }
15: }
16:
17: public ComposedKey(string name, int passKey)
18: {
19: this.name = name;
20: this.passKey = passKey;
21: }
22:
23: public override string ToString()
24: {
25: return name + " " + passKey;
26: }
27:
28: public bool Equals(ComposedKey obj)
29: {
30: if (ReferenceEquals(null, obj)) return false;
31: if (ReferenceEquals(this, obj)) return true;
32: return Equals(obj.name, name) && obj.passKey == passKey;
33: }
34:
35: public override bool Equals(object obj)
36: {
37: if (ReferenceEquals(null, obj)) return false;
38: if (ReferenceEquals(this, obj)) return true;
39: if (obj.GetType() != typeof (ComposedKey)) return false;
40: return Equals((ComposedKey) obj);
41: }
42:
43: public override int GetHashCode()
44: {
45: unchecked
46: {
47: return ((name != null ? name.GetHashCode() : 0)*397) ^ passKey;
48: }
49: }
50:
51: public static bool operator ==(ComposedKey left, ComposedKey right)
52: {
53: return Equals(left, right);
54: }
55:
56: public static bool operator !=(ComposedKey left, ComposedKey right)
57: {
58: return !Equals(left, right);
59: }
60: }
Désormais il devient possible d'utiliser des instances de la classe ComposedKey comme clé d'un dictionnaire générique.
Dans un premier temps testons le comportement de l'égalité :
1: // Test of IEquatable in ComposedKey
2: var k1 = new ComposedKey("Olivier", 589);
3: var k2 = new ComposedKey("Bill", 9744);
4: var k3 = new ComposedKey("Olivier", 589);
5:
6: Console.WriteLine("{0} =? {1} : {2}",k1,k2,(k1==k2));
7: Console.WriteLine("{0} =? {1} : {2}",k1,k3,(k1==k3));
8: Console.WriteLine("{0} =? {1} : {2}",k2,k1,(k2==k1));
9: Console.WriteLine("{0} =? {1} : {2}",k2,k2,(k2==k2));
10: Console.WriteLine("{0} =? {1} : {2}",k2,k3,(k2==k3));
Ce code produira le résultat suivant à la console :
Olivier 589 =? Bill 9744 : False
Olivier 589 =? Olivier 589 : True
Bill 9744 =? Olivier 589 : False
Bill 9744 =? Bill 9744 : True
Bill 9744 =? Olivier 589 : False
Ces résultats sont conformes à notre attente. Nous pouvons dès lors utiliser la classe au sein d'un dictionnaire comme le montre le code suivant :
1: // Build a dictionnary using the composed key
2: var dict = new Dictionary<ComposedKey, string>()
3: {
4: {new ComposedKey("Olivier",145), "resource A"},
5: {new ComposedKey("Yoda", 854), "resource B"},
6: {new ComposedKey("Valérie", 9845), "resource C"},
7: {new ComposedKey("Obiwan", 326), "resource D"},
8: };
9:
10: // Find associated resources by key
11:
12: var fk1 = new ComposedKey("Yoda", 854);
13: var s = dict.ContainsKey(fk1) ? dict[fk1] : "No Resource Found";
14: // must return 'resource B'
15: Console.WriteLine("Key '{0}' is associated with resource '{1}'",fk1,s);
16:
17: var fk2 = new ComposedKey("Yoda", 999);
18: var s2 = dict.ContainsKey(fk2) ? dict[fk2] : "No Resource Found";
19: // must return 'No Resource Found'
20: Console.WriteLine("Key '{0}' is associated with resource '{1}'", fk2, s2);
Code qui produira la sortie suivante :
Key 'Yoda 854' is associated with resource 'resource B'
Key 'Yoda 999' is associated with resource 'No Resource Found'
Et voilà ...
Rien de tout cela est compliqué mais comme on peut le voir il y a toujours une distance de la coupe aux lèvres, et couvrir cette distance c'est justement tout le savoir-faire du développeur !
Le projet VS 2008 : MultiKey.zip (5,84 kb)
.. Stay Tuned !
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)
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)
Microsoft a mis gratuitement à disposition des kits d'entraînement pour VS 2008 et .NET Framework 3.5. Ils contiennent de nombreuses aides et démos des technos les plus à jour : LINQ, C# 3.0, VB9, WCF, WF, PF, ASP.NET, VSTO, CardSpace, SilverLight, gestion et cycle de vie des applications mobiles. Le second kit se concentre plus sur le framework 3.5 lui-même en abordant par exemple ADO.NET Data Services (anciennement projet Astoria), le Client Profile du framework, ASP.NET routing, et plein d'autres choses.
Ce sont des mines d'informations à ne surtout pas louper !
L'adresse : Visual Studio 2008 and .NET Framework 3.5 Training Kit
L'autre kit : .NET Framework 3.5 Enhancements Training Kit
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)
RDL ? Cela ne vous dit rien ? SSRS ou BIDS non plus ? Et le composant gratuit de Microsoft ReportViewer ? Rien ? ... Alors il faut que vous lisiez absolument ce nouveau tutorial (PDF, 30 pages + exemples VS 2008) que je viens de mettre en ligne !
Il existe en effet une solution gratuite et plus que performante pour générer des états, elle se cache dans Visual Studio et un peu aussi dans les méandres des sites Microsofts pour télécharger "ReportViewer" qui fonctionne sous VS 2005 et VS 2008.
Cette solution est plus qu'honorable : sous-états, états avec noeuds dépliables, prévisualisation avec recherche de texte, fonctionnant sous Windows Forms et sous ASP.NET, utilisant toutes les sources de données possibles dont les grappes d'objets en mémoire, capable de produire des PDF, des fichiers Excel... Le tout gratuitement et assez facilement.
Rêve éveillé ? Non... mais en revanche, et pour des raisons que j'ignore, cette solution est méconnue. Elle mérite bien un tutor pour rétablir la justice (et vous faire économiser l'achat d'un générateur qui n'en fera peut-être pas la moitié) !
L'article RDL et ReportViewer est à télécharger en suivant le lien ou bien en se rendant sur www.e-naxos.com page Publications, rubrique Divers.
Bonne lecture ... et Stay Tuned !
[Vous pouvez aussi lire cet autre billet sur les best practices de RDL]
La syntaxe de C# et son orientation "tout objet" ont définitivement tourné la page de la programmation procédurale. Ce n'est certes pas un mal, bien au contraire, mais au passage nous avons perdu une petite facilité de langages tel que Pascal qui autorisaient la déclaration de procédures à l'intérieur de procédures. Le manque n'est pas cruel mais tout de même... Il semble souvent bien lourd et assez artificiel d'être obligé de créer une méthode private ou internal juste pour rendre un service à une seule méthode. De plus le morceau de code ainsi transformé en méthode, même private ou internal, ne sera encapsulé qu'au niveau de la classe et non de la méthode intéressée, d'où le risque de l'utiliser ailleurs (ce qui change sa statégie d"écriture). Le code sera aussi plus lourd en raison de la nécessité de passer en paramètre tout ce qui sera nécessaire à l'exécution de cette "sous méthode" alors qu'une procédure imbriquée peut référencer les variables de la procédure qui l'abrite.
Bref, l'affaire ne mérite certainement pas de grandes théories, mais il faut avouer que de temps en temps on aimerait bien pouvoir déclarer une petite méthode à l'intérieur d'une autre. Il s'agit là d'appliquer la logique des classes elle-mêmes : il est possible de déclarer une classe dans une autre lorsqu'elle ne sert exclusivement qu'à la première. Pourquoi ne pas retrouver cette possibilité au niveau des méthodes d'une classe ?
Les procédures imbriquées n'existent pas en C#. Cela vous semble une certitude. Avec C# 3.0 ce n'est plus aussi certain...
Les expressions Lambda utilisées comme des procédures imbriquées
Je n'entrerai pas ici dans le détail de la syntaxe des expressions Lambda que j'ai déjà présenté dans un long article sur les nouveautés de C# 3.0, article auquel je renvoie le lecteur s'il en ressent le besoin (Les nouveautés syntaxiques de C# 3.0 et Présentation des différentes facettes de LINQ)
Les procédures imbriquées ne sont rien d'autres que des procédures "normales" mais déclarées à l'intérieur d'autres procédures. En Pascal cela ne peut se faire qu'entre l'entête de la méthode principal et le corps de celle-ci :
Procedure blabla
Procedure imbrique begin ... end;
begin // blabla
...
end; // blabla
Avec les expressions Lambda de C# 3.0 on retrouve une possibilité sensiblement identique avec plus de souplesse encore puisque la "sous procédure" peut être déclarée n'importe où dans le corps de la "procédure principale".
Exemple
static void Main(string[] args)
{
// une "fonction" imbriquée (teste si un nombre est impair)
Func<int, bool> isOdd = i => (i & 1) == 1;
// une "procédure" imbriquée (formate et écrit un int à la console)
Action<int> format = i => Console.WriteLine(i.ToString("000"));
Console.WriteLine(isOdd(25));
Console.WriteLine(isOdd(24));
format(25);
format(258);
format(5);
}
La sortie sera :
True
False
025
258
005
Conclusion
La possibilité de déclarer des "sous procédures" est bien pratique, cela permet en général d'éviter les répétitions dans le coprs d'une méthode, donc de diminuer le risque de bug et d'améliorer sensiblement la lecture du code. C# ne supportait pas cette possibilité syntaxique, mais en utilisant les expressions Lambda nous retrouvons la même fonctionnalité...
Pour d'autres astuces, Stay Tuned !
Et pour les paresseux le projet VS 2008 : SubRoutines.zip (4,73 kb)