Dot.Blog

C#, XAML, Xamarin, WinUI/Android/iOS, MAUI

C# MONO et OpenSuse, le tout virtualisé. Ou "comment occuper un geek" en une leçon.

Il faut bien se détendre un peu... Et que fait un geek pour se détendre ? Il allume son PC (en fait il ne l'éteint jamais !). Alors pour me changer les idées, je me suis mis en tête d'installer MONO pour "voir" où le projet en est. C'est cette petite aventure que je m'en vais vous conter, ça pourra vous servir...

Mais voilà, avant de jouer avec C# sous Linux il faut d'abord installer Linux (hmmm), choisir l'une de ses distribs et surtout arriver à le faire marcher ! Et comme je suis pervers mais pas fou, hors de question de faire un dual boot (j'en ai déjà un XP/Vista) ni même de reformater l'une de mes machines. Il va donc falloir virtualiser...

Abonné MSDN je dispose de tout plein de softs de Microsoft, dont Virtual PC 2007. J'ai commencé petit joueur par une Mandriva. Impossible à installer, le fameux problème d'écran 24 bits par défaut de Linux... Une vraie cata. J'ai passé des heures à chercher sur le Web, on trouve des trucs, mais aucun ne marche !

Après des heures à tourner en rond face à un écran très élargi et illisible, j'ai renoncé. J'ai donc laissé Virtual PC de côté, super chouette pour virtualiser du DOS, de l'XP ou du Vista, mais visiblement pas très open à Linux...

En farfouinant je suis tombé sur un virtualiseur gratuit écrit par l'ennemi juré de Microsoft, Sun. Du coup j'ai pas essayé de voir si leur machin savait lui gérer la virtualisation de Windows aussi mal que Virtual PC gère mal Linux, mais pour virtualiser du Linux ça semblait un bon choix. J'ai donc télécharger VirtualBox. Ca s'installe facilement, ça fait ce que ça dit et ça marche plutôt vite. Un bon point pour ce produit gratuit.

J'entreprends alors de virtualiser mon Mandriva que je n'avais pu installer sous Virtual PC. Et là, surprise, ça passe facile. Mon coeur s'emplit de joie en voyant Linux tourner dans une fenêtre... Mandriva, hélas, c'est KDE. L'avantage de KDE c'est d'être un clône de Windows XP niveau interface. On s'y retrouve facilement. Mais voilà, je n'y avais pas pensé avant, le kit de développement de MONO réclame visiblement GNOME ! Zut! me suis-je exclamé après toutes ses heures (en réalité c'était pas "zut", mais un truc du même genre en plus .. illustré :-) ). [EDIT:] Il semble que le site de MONO ignore Mandriva, mais depuis le gestionnaire de logiciels de ce dernier on peut accéder au téléchargement de MONO et des outils de développement. Je viens de le faire et oui, ça marche parfaitement. On peut donc utiliser Mandriva sans problème [/EDIT]. [EDIT2:] Comme je suis une quiche sous Linux, je n'avais pas pigé non plus que Mandriva permet de changer de bureau et de système d'affichage. On peut donc être en KDE ou GNOME sans problème la non plus. C'est en bûchant qu'on devient bucheron n'est-il pas... [/EDIT2]

Je farfouine à nouveau... et au final je me décide pour OpenSuse, promu par Novell qui se trouve derrière MONO aussi, ça devrait matcher. Bonne pioche ! Je prend la 11.0 mais ils préviennent que les fichiers de plus 4 Go posent des problèmes en téléchargement HTTP depuis Internet Explorer. J'avais téléchargé des DVD ISO il y a peu de temps avec IE sans problème et je me doutais bien qu'il s'agissait là de médisances de concurrents, mais par précaution j'ai pris l'option téléchargement par Torrent. Deux ou trois heures après j'avais mon ISO.

...Et on est reparti pour la création d'une machine virtuelle. Au passage je me dis, tiens, pourquoi ne pas redonner sa chance à Virtual PC, après tout. Je vous passe les détails, une heure de perdue pour exactement le même problème sans arriver à trouver une solution. Retour à VirtualBox.

Là les choses se passent comme avec Mandriva, c'est à dire bien et facilement.

Installation de OpenSuse. C'est pas mal. J'arrive ensuite à faire marcher l'installeur YaST pour ajouter les références au projet Mono et à installer l'ensemble des paquets.

Cool. J'ai maintenant un joli OpenSuse avec MONO et l'environnement de développement ! Je fais un nouveau projet console et je tape deux ou trois truc pour voir. Pas mal. C'est du niveau C# 2.0 avec les génériques. Je n'ai pas encore creusé plus loin, mais voir mon petit programme s'animer dans une fenêtre Linux sur mon XP, ça fait plaisir...

J'ai voulu aller un peu plus loin en partageant un disque physique de ma machine avec la machine virtuelle. VirtualBox le permet en installant des extensions. Retour à la galère, il faut les sources du noyau, le make et GCC. Trouver ces merveilles, les placer dans YaST et les installer s'est révélé moins dur que je le pensais. Mais la fameuse extension de VirtualBox n'a jamais voulu s'installer... C'est un script (.run) et quand je double cliquait dessus il me disait que j'avais pas les privilège admin ce qui plantait le script visiblement. Pourtant le compte que je me suis créé est dans le group des admins... Mystère de Linux. En changeant de user et en me loggant comme "root" c'est allé un cran plus loin. Mais là c'est un autre problème un peu confus que je n'ai pas pu résoudre qui s'est pointé... VirtualBox et ses extensions ne sont donc pas trop au point si on veut utiliser toutes les astuces. [EDIT] Devant l'impossibilité de partager un disque XP avec la VM j'ai essayé de faire un partage réseau... Mandriva ou OpenSus voient le réseau et Internet, mais malgré tous mes efforts à ce jour, impossible de leur faire voir les autres machines du réseau et encore moins leurs disques, malgré le daemon Lisa, malgré Samba et autres pares-feux aux configurations ésotériques... Si quelqu'un sait... laissez un commentaire![/EDIT]

Conclusion

Les leçons à tirer sont les suivantes : Il faut utilise VirtualBox et non Virtual PC si on veut virtualiser du Linux, Il faut prendre une distrib Linux compatible avec Mono, OpenSuse semble parfaite pour ça [EDIT] Mandriva va très bien aussi [/EDIT]. Sinon pour le reste ça s'installe correctement, YaST simplifiant les choses (mais il faut comprendre comment marche YaST, notamment l'ajout d'une source d'installation, c'est là l'astuce !).

Les dernières moutures de Linux, Suse ou Mandriva sont des pâles copies de Windows XP, en moins chouette visuellement, et en plus complexe à faire marcher. Esthétiquement ça fait un peu Matrix, parfois l'interface graphique disparaît (lancement et extinction par exemple) et on voit des trucs défilés dans une console, très geek mais pas très "end user" donc. Toutefois il faut saluer les gros efforts de ces dernières années pour rendre Linux utilisable, mais on l'impression d'un truc "cheap", on voit bien que c'est gratuit quoi... Surtout quand on utilise Vista, dont on peut dire ce qu'on veut, mais quand on en a pris l'habitude, c'est autrement plus joli et plus agréable que XP ou les distribs Linux. Les fous de la customisations diront qu'ils peuvent installer des bureaux super chouettes sous Linux, c'est vrai. Mais que d'effort pour arriver à faire, de façon compliquée, ce qu'un Vista fait de base de façon simple... Reste que la concurrence Linux est une bonne chose, et les essais que je vous conte ici sont la preuve que j'attache de l'importance à cette alternative même si elle est loin de me convaincre, pour le moment, de reformater toutes mes machines sous Linux.

Mais bon, le truc ce n'était pas de tester Linux ni de donner mon avis sur la question, Linux est un monde fermé, si un "vendu" à la cause Microsoft fait des réserves c'est forcément qu'il est corrompu et donc que son avis n'a pas d'intérêt. Les Linuxiens ne lisent donc de toute façon pas les avis des non linuxiens sur Linux. Non, mon but était d'installer MONO et de faire des tests. De ce côté là c'est concluant. Juste une chose qu'il me reste à creuser, la gestion des fenêtres (l'équilavent de Windows Forms) en tout cas dans la version dont je dispose, utilise un truc vraiment très différent donc pas portable du tout. Il va falloir que je regarde s'il existe une émulation des Windows Forms sinon cela limite la compatibilité entre .NET et MONO aux libs de classes. C'est déjà pas mal, mais ça me semble trop juste. [Edit:] Il y a bien un System.Windows.Forms dans les libs installées, mais point de designer dans l'IDE.. Il semble qu'il en existe un, mais j'ai pas tout pigé comment l'obtenir, est ce un truc à part, un plugin, en tout cas ça demande d'obtenir le source de MONO par svn et de tout recompiler.. Brrr. L'esprit Linux c'est, il faut l'avouer, très très éloigné des "user experiences" de MS ! [/Edit]

Voilà pour cette petite histoire, qui, au fil des paragraphes vous aura peut-être donné l'envie de tenter l'expérience aussi, en utilisant directement les bonnes solutions et sans perdre de temps :-)

avant de lâcher mon traditionnel "Stay Tuned!" voici une petite image, ça fait toujours plaisir :

 

OpenSuse 11.0 avec MonoDevelop

 

Sous les pavés la plage... Chez moi c'est vrai, mais en plus sous la fenêtre OpenSuse il y a aussi le bureau XP avec VirtualBox (et l'icone de lancement de VS 2008 mon compagnon de tous les jours) ! Cool isn't it ?

Alors... pour d'autres aventures : Stay Tuned !

 

[EDIT:] Comme j'ai réussi à faire pareil sous Mandriva, une petite image aussi ![/EDIT]

De l'intérêt d'overrider GetHashCode()

Les utilisateurs de Resharper ont la possibilité en quelques clics de générer un GetHashCode() et d'autres méthodes comme les opérateurs de comparaison pour toute classe en cours d'édition. Cela est extrêment pratique et utile à plus d'un titre. Encore faut-il avoir essayer la fonction de Resharper et s'en servir à bon escient... Mais pour les autres, rien ne vient vous rappeler l'importance de telles fonctions. Pourtant elles sont essentielles au bon fonctionnement de votre code !

GetHashCode()

Cette méthode est héritée de object et retourne une valeur numérique sensée être unique pour une instance. Cette unicité est toute relative et surtout sa répartition dans le champ des valeurs possibles est inconnue si vous ne surchargez pas GetHashCode() dans vos classes et structures ! Il est en effet essentiel que le code retourné soit en rapport direct avec le contenu de la classe / structure. Deux instances ayant des valeurs différentes doivent retourner un hash code différent. Mieux, ce hash code doit être représentatif et générer le minimum de collisions...

Si vous utilsez un structure comme clé d'une Hashtable par exemple, vous risquez de rencontrer des problèmes de performances que vous aurez du mal à vous expliquer si vous n'avez pas conscience de ce que j'expose ici...
Je ne vous expliquerais pas ce qu'est un hash code ni une table Hashtable, mais pour résumer disons qu'il s'agit de créer des clés représentant des objets, clés qui doivent être "harmonieusement" réparties dans l'espace de la table pour éviter les collisions. Car en face des codes de hash, il y a la table qui en interne ne gère que quelques entrées réelles. S'il y a collision, elle chaîne les valeurs.
Moralité, au lieu d'avoir un accès 1->1 (une code hash correspond à une case du tableau réellement géré en mémoire) on obtient plutôt n -> 1, c'est à dire plusieurs valeurs de hash se partageant une même entrée, donc obligation de les chaîner, ce que fait la Hashtable de façon transparente mais pas sans conséquences !

Il découle de cette situation que lorsque vous programmez un accès à la table de hash, au lieu que l'algorithme (dans le cas idéal 1->1) tombe directement sur la cellule du tableau qui correspond à la clé (hash code), il est obligé de parcourir par chaînage avant toutes les entrées correspondantes... De là une dégration nette des performances alors qu'on a généralement choisi une Hashtable pour améliorer les performances (au lieu d'une simple liste qu'il faut balayer à chaque recherche). On a donc, sans trop le savoir, recréé une liste qui est balayée là où on devrait avoir des accès directs...

La solution : surcharger GetHashCode()

Il existe plusieurs stratégies pour générer un "bon" hash code. L'idée étant de répartir le plus harmonieusement les valeurs de sorties dans l'espace de la table pour éviter, justement, les collisions de clés. Ressortez vos cours d'informatique du placard, vous avez forcément traité le sujet à un moment ou un autre ! Pour les paresseux et ceux qui n'ont pas eu de tels cours, je ne me lancerais pas dans la théorie mais voici quelques exemples d'implémentations de GetHashCode() pour vous donner des idées :

La méthode "bourrin"

Quand on ne comprends pas forcément les justifications et raisonnements mathématiques d'un algorithme, le mieux est de faire simple, on risque tout autant de se tromper qu'en faisant compliqué, mais au moins c'est facile à mettre en oeuvre et c'est facile à maintenir :-)

Imaginons une structure simple du genre :

public struct MyStruct
{
   
public int Entier { get; set; }
   
public string Chaine { get; set; }
   
public DateTime LaDate { get; set; }
}

Ce qui différencie une instance d'une autre ce sont les valeurs des champs. Le plus simple est alors de construire une "clé" constituée de toutes les valeurs concaténées et séparées par un séparateur à choisir puis de laisser le framework calculer le hash code de cette chaîne. Toute différence dans l'une des valeurs formera une chaine-clé différente et par conséquence un hash code différent. Ce n'est pas super subtile, mais ça fonctionne. Regardons le code :

public string getKey()
{
return Entier + "|" + Chaine + "|" + LaDate.ToString("yyyyMMMddHHmmss"); } public override int GetHashCode() {return getKey().GetHashCode(); }

J'ai volontairement séparé la chose en deux parties en créant une méthode getKey pour pouvoir l'afficher.

La sortie (dans un foreach) de la clé d'un exemple de 5 valeurs avec leur hash code donne :

1|toto|2008juil.11171952 Code: -236695174
10|toto|2008juil.11171952 Code: -785275536
100|zaza|2008juil.01171952 Code: -684875783
0|kiki|2008sept.11171952 Code: 888726335
0|jojo|2008sept.11171952 Code: 1173518366 

La méthode Resharper

Ce merveilleux outil se propose de générer pour vous la gestion des égalités et du GetHashCode, laissons-le faire et regardons le code qu'il propose (la structure a été au passage réécrite, les propriétés sont les mêmes mais elles utilisent des champs privés) :

D'abord le code de hachage :

public override int GetHashCode()
{
   unchecked
   {
      int result = entier;
      result = (result*397) ^ (chaine !=
null ? chaine.GetHashCode() : 0);
      result = (result*397) ^ laDate.GetHashCode();
      return result;
   }
}

On voit ici que les choix algorithmiques pour générer la valeur sont un peu plus subtiles et qu'ils ne dépendent pas de la construction d'une chaîne pour la clé (ce qui est consommateur de temps et de ressource).

Profitons-en pour regarder comment le code gérant l'équalité a été généré (ainsi que le support de l'interface IEquatable<MyStruct> qui a été ajouté à la définition de la structure)  - A noter, la génération de ce code est optionnel - :

public static bool operator ==(MyStruct left, MyStruct right)
{
return left.Equals(right); }

public static bool operator !=(MyStruct left, MyStruct right)
{
return !left.Equals(right); }

public bool Equals(MyStruct obj)
{ return obj.entier == entier && Equals(obj.chaine, chaine) && obj.laDate.Equals(laDate); }

public override bool Equals(object obj)
{
   
if (obj.GetType() != typeof(MyStruct)) return false;
    return Equals((MyStruct)obj);
}

Bien que cela soit optionel et n'ait pas de rapport direct avec GethashCode, on notera l'intérêt de la redéfinition de l'égalité et des opérateurs la gérant ainsi que le support de IEquatable. Une classe et encore plus une structure se doivent d'implémenter ce "minimum syndical" pour être sérieusement utilisables. Sinon gare aux bugs difficiles à découvrir (en cas d'utilisation d'une égalité même de façon indirecte) !

De même tout code correct se doit de surcharger ToString(), ici on pourrait simplement retourner le champ LaChaine en supposant qu'il s'agit d'un nom de personne ou de chose, d'une description. Tout autre retour est possible du moment que cela donne un résultat lisible. Ce qui est très pratique si vous créez une liste d'instances et que vous assignez cette liste à la propriété DataSource d'un listbox ou d'une combo... Pensez-y !

Conclusion

Créer des classes ou des structures, si on programme sous C# on en a l'habitude puisque aucun code ne peut exister hors de telles constructions. Mais "bien" construire ces classes et structures est une autre affaire. Le framework propose notamment beaucoup d'interfaces qui peuvent largement améliorer le comportement de votre code. Nous avons vu ici comment surcharger des méthodes héritées de object et leur importance, nous avons vu aussi l'interface IEquatable. IDisposable, INotityPropertyChanged, ISupportInitialize, et bien d'autres sont autant d'outils que vous pouvez (devez ?) implémenter pour obtenir un code qui s'intègre logiquement au framework et en tire tous les bénéfices.

Bon dev, et Stay Tuned !

Les class helper : s'en servir pour gérer l'invocation des composants GUI en multithread

Les class helper dont j'ai déjà parlé ici peuvent servir à beaucoup de choses, si on se limite à des services assez génériques et qu'on ne s'en sert pas pour éclater le code d'une application qui deviendra alors très difficile à maintenir. C'est l'opinion que j'exprimais dans cet ancien billet et que je conserve toujours.

Dès lors trouver des utilisations pertinentes des class helpers n'est pas forcément chose aisée, pourtant il ne faudrait pas les diaboliser non plus et se priver des immenses services qu'ils peuvent rendent lorsqu'ils sont utilisés à bon escient.

Dans le blog de Richard on trouve un exemple assez intéressant à plus d'un titre. D'une part il permet de voir que les class helpers sont des alliés d'une grande efficacité dès qu'il s'agit de trouver une solution globale à un problème répétitif. Mais d'autre part cet exemple, par l'utilisation des génériques et des expressions lambda, a l'avantage de mettre en scène tout un ensemble de nouveautés syntaxiques de C# 3.0 en quelques lignes de code. Et les  de ce genre sont toujours formateurs.

Pour ceux qui lisent l'anglais, allez directement sur le billet original en cliquant ici. Pour les autres, voici non pas un résumé mais une interprétation libre sur le même sujet :

Le problème à résoudre : l'invocation en multithread.
Lorsqu'un thread doit mettre à jour des composants détenus par le thread principal cela doit passer par un appel à Invoke car seul le thread principal peut mettre à jour les contrôles qu'il possède. Cette situation est courante. Par exemple un traitement en tâche de fond qui doit mettre à jour une barre de progression.
Bien entendu il ne s'agit pas de bricoler directement les composants d'une form depuis un thread secondaire, ce genre de programmation est à proscrire, mais même en créant dans la form une propriété publique accessible au thread, la modification de cette propriété se fera à l'intérieur du thread secondaire et non pas dans le thread principal...
Il faut alors détecter cette situation et trouver un moyen de faire la modification de façon "détournée", c'est à dire de telle façon à ce que ce soit le thread principal qui s'en charge.

Les Windows Forms et les contrôles conçus pour cette librairie mettent à la disposition du développeur la méthode InvokeRequired qui permet justement de savoir si le contrôle nécessite l'indirection que j'évoquais plus haut ou bien s'il est possible de le modifier directement. Le premier cas correspond à une modification depuis un thread secondaire, le dernier à une modification du contrôle depuis le thread principal, cas le plus habituel.

La méthode classique
Sous .NET 1.1 le framework ne détectait pas le conflit et les applications mal conçues pouvait planter aléatoirement si des modifications de contrôles étaient effectuées depuis des threads secondaires. Le framework 2.0 a ajouté plus de sécurité en détectant la situation qui déclenche une exception, ce qui est bien préférable aux dégâts aléatoires...

Donc, pour s'en sortir on doit écrire un code du genre de celui-ci :

[...]
NetworkChange.NetworkAddressChanged += new NetworkAddressChangedEventHandler(NetworkChange_NetworkAddressChanged);
[...]

delegate void SetStatus(bool status);

void NetworkChange_NetworkAddressChanged(object sender, EventArgs e)
{
      bool isConnected = IsConnected();

      if (InvokeRequired)
        Invoke(new SetStatus(UpdateStatus), new object[] { isConnected });
      else
        UpdateStatus(isConnected);
}

void UpdateStatus(bool connected)
{
      if (connected)
         this.connectionPictureBox.ImageLocation = @"..\bullet.green.gif";
      else
         this.connectionPictureBox.ImageLocation = @"..\bullet.red.gif";
}
[...]
 

Cette solution classique impose la création d'un délégué et beaucoup de code pour passer d'une modification directe à une modification indirecte selon le cas. Bien entendu le code en question doit être dupliqué pour chaque contrôle pouvant être modifié par un thread secondaire... C'est assez lourd, convenons-en...
(l'exemple ci-dessus provient d'un vieux post de 2006 d'un blog qui n'est plus actif mais que vous pouvez toujours visiter en cliquant ici).
Pour la compréhension, le code ci-dessus change l'image d'une PictureBox pour indiquer l'état (vert ou rouge) d'une connexion à un réseau et le code appelant cette mise à jour de l'affichage peut émaner d'un thread secondaire.

Comme on le voit, la méthode est fastidieuse et va avoir tendance à rendre le code plus long, moins fiable (coder plus pour bugger plus...), et moins maintenable. C'est ici que l'idée d'utiliser un class helper prend tout son intérêt...

La solution via un class helper

La question qu'on peut se poser est ainsi "n'existe-t-il pas un moyen générique de résoudre le problème ?". De base pas vraiment. Mais avec l'intervention d'un class helper, si, c'est possible (© Hassan Céhef - joke pour les amateurs des "nuls"). Voici le class helper en question :

public static TResult Invoke<T, TResult>(this T controlToInvokeOn, Func<TResult> code) where T : Control
{
   if (controlToInvokeOn.InvokeRequired)
   {
    return (TResult)controlToInvokeOn.Invoke(code);
   }
   else
   {
    return (TResult)code();
   }
}

Il s'agit d'ajouter à toutes les classes dérivées de Control (et à cette dernière aussi) la méthode "Invoke". Le class helper, tel que conçu ici, prend en charge le retour d'une valeur, ce qui est pratique si on désire lire la chaîne d'un textbox par exemple. Le premier paramètre "ne compte pas", il est le marqueur syntaxique des class helpers en quelque sorte. Le second paramètre qui apparaitra comme le seul et unique lors de l'utilisation de la méthode est de type Func<TResult>, il s'agit ici d'un prototype de méthode. Il sera donc possible de passer à Invoke directement un bout de code, voire une expression lambda, et de récupérer le résultat.

Un exemple d'utilisation :  string value = this.Invoke(() => button1.Text);

Ici on va chercher la valeur de la propriété Text de "button1" via un appel à Invoke sur "this", supposée ici être la form. Le résultat est récupéré dans la variable "value". On note l'utilisation d'une expression lambda en paramètre de Invoke.

Mais si le code qu'on désire appeler ne retourne pas de résultat ? Le class helper, tel que défini ici, ne fonctionnera pas puisqu'il attend en paramètre du code retournant une valeur (une expression). Il est donc nécessaire de créer un overload de Invoke pour gérer ce cas particulier :

public static void Invoke(this Control controlToInvokeOn, Func code)
{
    if (controlToInvokeOn.InvokeRequired)
    {
       controlToInvokeOn.Invoke(code);
    }
    else
    {
       code();
    }
}

Avec cet overload la solution est complète et gère aussi bien le code retournant une valeur que le code "void".

On peut écrire alors: this.Invoke(() => progressBar1.Value = i);

Sachant que pour simplifier l'appel est ici effectué dans la form elle-même (this). L'appel à Invoke contient une expression lambda qui modifie la valeur d'une barre de progression. Mais peu importe les détails, c'est l'esprit qui compte.

Conclusion
Les class helpers peuvent fournir des solutions globales à des problèmes récurrents. Utilisés dans un tel cadre ils prennent tout leur sens et au lieu de rendre le code plus complexe et moins maintenable, au contraire, il le simplifie et centralise sa logique.

L'utilisation des génériques, des prototypes de méthodes et des expressions lambda montrent aussi que les nouveautés syntaxiques de C#, loin d'être des gadgets dont on peut se passer forment la base d'un style de programmation totalement nouveau, plus efficace, plus sobre et plus ... générique. L'exemple étudié ici illustre parfaitement cette nouvelle façon de coder de C# 3.0 et les avantages qu'elle proccure à ceux qui la maîtrise.

Bon dev !

(et Stay Tuned, of course !)

Quizz C#. Vous croyez connaître le langage ? et bien regardez ce qui suit !

C# est un langage merveilleux, plein de charme... et de surprises !

En effet, s'écartant des chemins battus et intégrant de nombreuses extensions comme Linq aujourd'hui ou les méthodes anonymes hier, il est en perpétuelle évolution. Mais ces "extensions" transforment progressivement un langage déjà subtile à la base (plus qu'il n'y paraît) en une jungle où le chemin le plus court n'est pas celui que les explorateurs que nous sommes auraient envisagé...

Pour se le prouver, 6 quizz qui vous empêcheront de bronzer idiot ou de vous endormir au boulot !

Un petit projet exemple avec les questions et les réponses sera téléchargeable en fin d'article demain...
[EDIT 4-7-08] Le lien se trouve désormais en fin d'article. Le code source contient les réponses au quizz.

Quizz 1

Etant données les déclarations suivantes :

class ClasseDeBase
{
   
public virtual void FaitUnTruc(int x)
    {
Console.WriteLine("Base.FaitUnTruc(int)"); }
}

class ClasseDérivée : ClasseDeBase
{
   
public override void FaitUnTruc(int x)
   { Console.WriteLine("Dérivée.FaitUnTruc(int)"); }

   public void FaitUnTruc(object o)
  {
Console.WriteLine("Dérivée.FaitUnTruc(object)"); }
}

Pouvez-vous prédire l'affiche du code suivant et expliquer la sortie réelle :

    ClasseDérivée d = new ClasseDérivée();
    int i = 10;
    d.FaitUnTruc(i);

Gratt' Gratt' Gratt'.....

Quizz 2

Pouvez-vous prédire l'affichage de cette séquence et expliquer l'affichage réel ?

double d1a = 1.00001;
double d2a = 0.00001;
Console.WriteLine((d1a - d2a) == 1.0);

double d1b = 1.000001;
double d2b = 0.000001;
Console.WriteLine((d1b - d2b) == 1.0);

double d1c = 1.0000001;
double d2c = 0.0000001;
Console.WriteLine((d1c - d2c) == 1.0);

Je vous laisse réfléchir ...

Quizz 3

Toujours le même jeu : prédire ce qui va être affiché et expliquer ce qui est affiché réellement... c'est forcément pas la même chose sinon le quizz n'existerait pas :-)

List<Travail> travaux = new List<Travail>();
Console.WriteLine("Init de la liste de delegates");
for (int i = 0; i < 10; i++)
{ travaux.Add(
delegate { Console.WriteLine(i); }); }

Console.WriteLine("Activation de chaque delegate de la liste");

foreach (Travail travail in travaux) travail();

 Les apparences sont trompeuses, méfiez-vous !

Quizz 4

Ce code compile-t-il ?

public enum EtatMoteur { Marche, Arrêt }

public static void DoQuizz()
{
   
EtatMoteur etat = 0.0;
   
Console.WriteLine(etat);
}

Quizz 5

 Etant données les déclarations suivantes :

private static void Affiche(object o)
{
Console.WriteLine("affichage <object>"); }

private static void Affiche<T>(params T[] items)
{
Console.WriteLine("Affichage de <params T[]>"); }

 Pouvez-vous prédire et expliquer la sortie de l'appel suivant :

  Affiche("Qui va m'afficher ?");

 Je sens que ça chauffe :-)

Quizz 6

 Etant données les déclarations suivantes :

 delegate void FaitLeBoulot(); private static FaitLeBoulot FabriqueLeDélégué()
{
  
Random r = new Random();
  
Console.WriteLine("Fabrique. r = "+r.GetHashCode());

  
return delegate {
                           
Console.WriteLine(r.Next());
                           
Console.WriteLine("delegate. r = "+r.GetHashCode());
                         };

}

 Quelle sera la sortie de la séquence suivante :

    FaitLeBoulot action = FabriqueLeDélégué();
    action();
   
action();

 

Conclusion

C# est un langage d'une grande souplesse et d'une grande richesse. Peut-être qu'à devenir trop subtile il peut en devenir dangereux, comme C++ était dangereux car trop permissif.

Avec C#, même de très bons développeurs peuvent restés interloqués par la sortie réelle d'une séquence qui n'a pourtant pas l'air si compliquée.
Ses avantages sont tels qu'il mérite l'effort de compréhension supplémentaire pour le maîtriser réellement, mais tout développeur C# (les moyens comme ceux qui pensent être très bons!) doit être mis au moins une fois dans sa vie face à un tel quizz afin de lui faire toucher la faiblesse de son savoir et les risques qu'il prend à coder sans vraiment connaître le langage.

Comme toujours l'intelligence est ce bel outil qui nous permet de mesurer à quel point nous ne savons rien.
Nous sommes plutôt habitués à envisager cette question sous un angle métaphysique, le développement nous surprend lorsque lui aussi nous place face à cette réalité...

Pour le projet exemple dont le source contient les réponses cliquez sur le lien en fin de billet.

So (et plus que jamais!) .. Stay Tuned !

Quizz.rar (103,41 kb)

Donner du peps à Linq ! (Linq dynamique et parser d'expressions)

J'ai eu moulte fois ici l'occasion de dire tout le bien que  je pense de LINQ, de sa puissance, de sa souplesse, de sa versatilité (de xml à SQL en passant par les objets et l'Entity Framework).

Mais une petite chose me chagrinait : toute cette belle puissance s'entendait "hard coded". Je veux dire par là que les expressions et requêtes LINQ s'écrivent en C# dans le code et qu'il semblait très difficile de rendre la chose très dynamique, sous entendu dépendant d'expressions tapées à l'exécution de l'application. En un mot, faire du LINQ dynamique comme on fait du SQL dynamique.

Ce besoin est bien réel et ne concerne pas que les sources SQL. Prenez une simple liste d'objets que l'utilisateur peut vouloir trier selon l'une des propriétés ou filtrer selon le contenu des objets... Comment implémenter une telle feature ?

Contourner ce manque en écrivant des parsers, en jonglant avec les expressions lamba et la réflexion n'est pas, il faut l'avouer, ce qu'il y a de plus simple. Mais rassurez-vous chez Microsoft des gens y ont pensé pour nous ! Seulement voila, la chose est assez confidentielle il faut bien le dire, et on ne tombe pas dessus par hasard. Ou alors c'était un jour à jouer au Lotto, voire à contrôler, selon les mauvaises langues, l'emploi du temps de sa femme !

La chose s'appelle "LINQ Dynamic Query Library", un grand nom pour une petite chose puisqu'il s'agit en réalité d'un simple fichier source C# à ajouter à ses applications.  Mais quel fichier source !

Toute la difficulté consiste donc à savoir que ce fichier existe et mieux, où il se cache... C'est là que c'est un peu vicieux, la chose se cache dans un sous-répertoire d'un zip d'exemples Linq/C#, fichier lui-même astucieusement planqué dans la jungle des milliers de téléchargement de MSDN...

Sans plus attendre allez chercher le fichier cliquant ici.

Une fois que vous aurez accepté les termes de la licence ("I Accept" tout en bas à gauche) vous recevrez le fichier "CSharpSamples.zip". Dans ce dernier (dont tout le contenu est vraiment intéressant) aller dans le répertoire "LinqSamples" puis dans le projet "DynamicQuery" vous descendrez dans une autre sous-répertoire appelé lui aussi "DynamicQuery" (non, ce n'est pas un jeu de rôle, juste un téléchargement MSDN!). Et là, le Graal s'y trouve, "Dynamic.cs".

Copiez-le dans vos projets, et ajouter un "using System.Linq.Dynamic". A partir de là vous pourrez utiliser la nouvelle syntaxe permettant de faire du LINQ dynamique. A noter que dans le répertoire de la solution vous trouverez aussi un fichier "Dynamic Expressions.html" qui explique la syntaxe du parser.

Quelles sont les possibilités de LINQ Dynamic Query Library ?

C'est assez simple, pour vous expliquer en deux images je reprend deux captures écrans tirées du blog de Scott Guthrie (un excellent blog à visiter si vous parlez l'anglais).

Voici un exemple de requête LINQ (en VB.NET, si j'avais refait la capture cela aurait été du C#, mais je ne vais pas rouspéter hein ! )

Maintenant voici la même requête utilisant du LINQ dynamique :

Vous voyez l'astuce ? Le filtrage et le tri sont maintenant passés en chaîne de caractères... Bien entendu cette chaîne peut avoir été construite dynamiquement par code (c'est tout l'intérêt) ou bien saisie depuis un TextBox rempli par l'utilisateur.

Ici pas de risque d'attaque par "SQL injection", d'abord parce que LINQ n'est pas limité à SQL et que le problème ne se pose que dans ce cas, mais surtout parce que LINQ to SQL utilise des classes issues d'un modèle de données "type safe" et que par défaut LINQ to SQL est protégé contre les attaques de ce type. Pas de souci à se faire donc.

Pour terminer j'insisterai lourdement sur le fait que LINQ ne se limite pas aux données SQL, et que l'astuce de LINQ dynamique ici décrite s'applique même à LINQ to Object par exemple. Laisser la possibilité à un utilisateur de trier une List<> ou une Collection<>, de la filtrer, tout cela en mémoire et sans qu'il y ait la moindre base de données est bien entendu possible.

LINQ est vraiment une innovation majeure en matière de programmation, pouvoir le dynamiser simplement c'est atteindre le nirvana...

Je vous laisse vous jeter sur vos claviers pour expérimenter du LINQ dynamique, amusez-vous bien !

..et surtout.. Stay tuned !