Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

C# by night

[new:30/07/2014]Chacun d’entre nous connait les rudiments de C#, moins nombreux sont les experts qui jonglent avec toutes ses possibilités. Parmi celles-ci j’en ai relevé que vous ne connaissez peut-être pas ou que vous oublieriez d’utiliser, ou que vous ne sauriez pas utiliser sans consulter la documentation. Un peu comme une visite de nuit d’une ville fait découvrir des paysages nouveaux dans des rues qu’on connait si bien de jour, C# by night c’est ça, alors suivez le guide !

C# un langage riche

Je pense qu’aucun langage de programmation n’a jamais connu autant de modifications et d’améliorations, de changements de paradigmes que C# depuis sa création. 

C# est un langage à la fois simple, défini par une poignée de mots clés, et à la fois d’une grande subtilité. Il est à la fois fortement typé tout en laissant beaucoup de liberté. Il peut être utilisé de façon simple ou on peut s’adonner à des constructions sophistiquées qui tiennent en quelques instructions pourtant (Linq par exemple).

Même s’il ne s’agit pas de troller sur le thème rabâché de la guerre des langages il faut convenir que C#, à défaut d’être “le meilleur” langage est vraisemblablement ce qui se fait de mieux en la matière.

Dans le foisonnement des langages de Php à JScript en passant par Java ou C++, C# apparait tel un soleil levant au-dessus d’un nuage de pollution.

C# est la cerise sur le gâteau, la chantilly sur le banana split. Bref un truc super chouette. Si je pense vous faire découvrir prochainement F#, C# reste et restera pour moi encore longtemps “le” langage de référence et de préférence.

Toutefois il reste difficile d’en mesurer toutes les finesses. J’ai déjà écrit sur ce thème, mais pour aujourd’hui je vous ai cuisiné un mélange de saveurs qui balayeront un peu tous les domaines du langage, des attributs à la syntaxe pure et dure, des mots-clés au méthodes et propriétés, rien de très savant à attendre (quoi que !), une sorte de révision, un devoir de vacances “cool” (vous comprendres plus loin)…

Syntaxe

C’est un peu le B.A.BA d’un langage, ce qu’il offre vraiment. C# a été conçu après l’échec du Java Microsoft, écrit par Hejlsberg qui avait justement été débauché de chez Borland, où il avait créé Delphi et son Pascal Objet. On sait les problèmes que Microsoft a rencontré avec son Java, à mon sens fort injustement, ce qui a obligé à battre en retraite. Il fallait une nouvelle idée et sur les bases syntaxiques de Java et les restes de Delphi C# est né. Hejlsberg a certainement aussi été influencé par C++ (la surcharge des opérateurs par exemple) et par des langages fonctionnels puisque des extensions comme Linq ont fini de faire de C# un langage si unique. Historiquement le langage s’appelait au départ (vers 2000) “C-like Object Oriented Language”, noté “Cool”, le nom C# est venu plus tard et le # semble être une incrémentation de “++” répété deux fois graphiquement indiquant clairement l’ambition du langage… C# est le dernier grand langage “all purpose” (tout usage) créé et il n’a pas été égalé ni dépassé depuis sa sortie il y a 12 ans déjà (2002) !

Alors puisque la syntaxe est à la base du langage, regardons de plus près certains éléments de cette syntaxe qui ne sont pas forcément les plus connus ou les plus utilisés. Forcément ce n’est pas exhaustif, chaque billet de Dot.Blog ne pouvant être un livre ! (Même si je sens bien que celui-là risque d’être long !).

Opérateur ??

Cet opérateur est appelé en anglais “null-coalescing operator” ce qu’on pourrait traduire par “opérateur d’union pour les valeurs nulles”. Traduction pas fantastique je dois bien l’avouer.

Il est plus facile d’en comprendre le fonctionnement mais étrangement il reste trop peu utilisé.

Son rôle est de faire un OR logique entre deux valeurs possibles, choisissant la seconde systématiquement si la première est nulle. Cet opérateur est très utile quand on veut s’assurer d’obtenir une valeur non nulle sur une affectation sans avoir à écrire un test préliminaire de la valeur. l’opérateur a été introduit principalement pour simplifier l’utilisation des types “nullable”, comme dans utilisé dans l’exemple ci-dessous.

int? x = null;
int y = x ?? -1;
// ici y vaut -1 et nul besoin de le déclarer nullable...


Facile donc. Il suffit d’y penser car l’opérateur s’avère fort utile dans beaucoup d’autres situations.

Marquage des nombres

Peu de code utilise des nombres correctement marqués (sauf quand le compilateur rouspète). Cela est pourtant souvent important même si les conversions implicites entre les types arrangent souvent les choses.

Ainsi on connait généralement

  • M, m pour le type decimal, exemple: var t=30.2m;
  • F, f pour le type float, exemple: var f=30.2f;
  • D, d pour le type double, exemple: var d=30.2D;

 

C’est volontairement que j’ai utilisé la même constante dans les exemples et le mot clé var pour définir les champs. Lorsqu’on utilise ce dernier il est très important de bien marquer les nombres car sinon le compilateur ne peut le deviner et choisi un type standard (double) qui n’est pas forcément adapté au code. C’est le cas aussi dans le typage implicite des paramètres d’une méthode générique (voir plus loin dans ce billet).

Mais ces marqueurs sont assez connus. Il en existe de plus exotiques :

  • U,u pour Unsigned Int
  • UL, ul, Ul, uL … pour Unsigned Long
  • L, l pour Long

 

On notera que les minuscules et les majuscules sont acceptées, même dans UL où toutes les combinaisons sont légales.

Pour les Long l’utilisation du L minuscule (“l”) est déconseillée car ce caractère est dans la majorité des fontes source de confusion avec le chiffre un “1”. Mais l’usage de la minuscule est légale à défaut d’être lisible.

Bien entendu, au-delà des marqueurs eux-mêmes mon intention est d’attirer votre attention sur la richesse des types disponibles. En dehors des int et des double (souvent par défaut) je ne vois pas souvent les autres types utilisés autant qu’ils le devraient. Bien choisir les types numériques a pourtant une influence assez grande sur l’occupation mémoire et les temps de calculs sans parler de la précision (utiliser un double au lieu d’un decimal lorsqu’on traite des sommes d’argent est par exemple une erreur qui peut avoir des conséquences importantes).

Le double double point ::

Le “double double-point” (plus loin je noterai DDP) est tellement utilisé rarement que je ne l’ai jamais vu lors de mes audits. Certains doivent bien s’en servir mais peu. Et forcément lorsqu’on utilise peu voire jamais un élément du langage on oublie même qu’il existe le jour où il pourrait être utile !

A quoi sert le DDP ? Il s’utilise en réalité avec une autre possibilité du langage : la définition d’alias pour les espaces de noms.

Ainsi on peut écrire :

using web = System.Web.UI.WebControls;
using win = System.Windows.Forms;

web::Control aWebControl = new web::Control();
var aFormControl = new win::Control();

 

Dans un tel contexte on pourrait parfaitement utiliser un point de ponctuation simple après les alias “web” et “win”, cela fonctionnerait parfaitement.

Alors pourquoi le DDP ?

Il sert à forcer l’interprétation de ce qui le précède comme étant un alias d’espace de noms. Et en quoi cela est-il important dans certains contextes ? Tout simplement parce que si demain nous ajoutons à cette application un espace de noms “web” et que nous y définissons une classe “Control”, l’écrire avec un seul point fera que le compilateur ne saura plus de quoi on parle, il y aura collision de noms… Choisir d’utiliser le DDP évite toute confusion possible dans l’immédiat mais aussi dans l’avenir… A utiliser systématiquement lorsqu’on met des alias de namespace donc.

Portée des accesseurs des propriétés

Un peu mieux connu mais pas très souvent utilisée, la possibilité existe de définir une portée différente pour les accesseurs d’une propriété. Par exemple l’utilisation la plus fréquente est d’avoir un getter public et un setter privé.

Bien entendu de nombreuses autres possibilités sont autorisées comme l’utilisation de internal ou de protected. Choses plus subtiles mais qui peuvent bien entendu tout changer…

Ce qui me fait penser à un joke américain que j’ai lu sur G+ dernièrement et que je vais traduire de mémoire :

“Ecrire une application c’est la même chose qu’écrire un livre. Sauf que si vous oublier un mot page 246 c’est tout le livre qui n’a plus sens et n’est plus lisible.”

Génériques implicites

Ce n’est pas grand chose mais tout de même cela allège la lecture du code. Et tout ce qui rend le code plus simple à lire le rend plus facile à maintenir. C’est donc finalement plus important qu’il n’y parait !

void GenericMethod<T>( T input ) { ... }

GenericMethod<int>(23);   // <> n'est pas utile
GenericMethod(23);        // inférence du type par C#

 

L’inférence de type de C# rend l’indication du type inutile. On est toujours dans les petites choses comme “var” qui rend l’écriture du code C# moins lourde, presque comme un langage non typé alors qu’il reste très fortement typé. Nous verrons dans de prochains billets que ce dénuement est justement l’avantage de F# tout en restant typé. Mais restons en C# pour le moment !

On revient ici à ce que je disais sur les marqueurs de types numériques. Si vous n’utilisez pas de marqueur vous êtes dans l’obligation de spécifier le type sinon “23” sera interprété comme un integer et sera ensuite traité comme tel par tout le code de la méthode “GenericMethod” (exemple ci-dessus). Si cette dernière fait des calculs, comme des divisions par exemple, c’est l’arithmétique des entiers qui s’appliquera avec un effet de bord pas forcément désiré. Si dans un tel cas on souhaite s’assurer que le numérique sera traité comme un double (simple exemple) il faudra soit ajouter le type à l’appel et ne pas utiliser l’inférence, soit utiliser un marqueur (“d” ici).

Si on souhaite définir des guidelines pour une équipe de développement on s’aperçoit que les subtilités de C# obligent à spécifier des règles très précises comme toujours utiliser des marqueurs pour les constantes numériques afin d’éviter une mauvaise inférence lors d’un appel de méthode générique sans le type… Et puis toujours et encore : écrire un bon code c’est écrire un code dont l’intention apparait évidente. Une telle stylistique évite les commentaires (qu’on oublie de mettre à jour…) et allège la lecture. Ecrire 23d au lieu de 23 marque de façon évidente l’intention d’utiliser un double par exemple. Pensez-y !

Le mode verbatim

Cette possibilité est à la limite d’être présentée ici, mais je me suis aperçu qu’elle n’était pas utilisée tout le temps, alors juste un rappel rapide :

// au lieu d'écrire:
var s = "c:\\program files\\oldway"
// il est préférable d'écrire:
var s =@"c:\program file\newway"

 

L’arobas est utilisé comme marqueur pour la chaîne de caractères définies juste derrière. Les caractères d’échappement ne sont plus interprétés ce qui simplifie l’écriture (et la lisibilité) notamment des chemins Windows… On notera qu’on peut toutefois utiliser certains caractères comme le double guillemet à condition de le doubler.

Attention, l’arobas a deux utilisations très différentes, celle dont je viens de parler et la suivante :

Arobas

Utilisé pour la définition des chaînes de caractères en mode verbatim, l’arobas a aussi une seconde signification : cela permet de définir et d’utiliser des noms de variables identiques à des mots clés du langage.

Ce genre de mélange est plutôt à éviter, mais parfois définir une variable “return” ou “interface” peut avoir une cohérence sémantique avec le code qui l’impose plutôt que d’utiliser d’autres noms qui auront moins de sens. L’intention visible étant essentielle cela peut se justifier ponctuellement. Mais dans ce cas il faudra écrire “@return” et “@interface”. Il s’agit bien sûr de simples exemples, cela fonctionne pour tous les mots clés de C#. Once more, à n’utiliser que dans des cas particuliers. Un truffé d’arabas et de variables identiques aux mots clés serait illisible et non maintenable.

Valeurs spécifiques pour les énumérations

Celle-ci est facile mais comme tout le reste, quand on sait c’est enfantin mais sinon…

Il est possible de spécifier des valeurs particulières aux items d’une énumération au lieu de laisser le compilateur utiliser généralement une suite de 0 à la valeur maximale autorisée.

Fixer les valeurs soi-même est très pratique lorsqu’on souhaite avoir une énumération qui limite et encadre les choix dans l’application tout en pouvant être “mappés” sur des valeurs utilisée par une API externe à l’application. Imaginons un librairie de reconnaissance OCR dont l’une des méthodes possède un paramètre qui peut prendre les valeurs 1, 5 ou 8. De telles curiosités existent j’en ai rencontrées… Imaginons cette API et notre application qui elle est écrite proprement. Nous avons intérêt à définir une énumération de trois valeurs mais avec des noms très explicites comme “Manuscrit”,”Photo”,”Livre” qui pourraient être la signification des trois valeurs évoquées plus haut.

En faisant ainsi l’application pourra utiliser une notation claire et lisible, comme “var a = ModeReconnaissance.Photo;” ce qui est tout de même plus clair que “var a = 5;”.

Au moment où l’appel à l’API un peu rustique sera effectué il suffira de caster la valeur en integer et l’affaire sera dans le sac : ApiBizarre((int)maVarEnum); !

Pour résumer, on peut donc écrire quelque chose comme cela :

public enum ModeReconnaissance  
{
   Manuscrit = 1,
   Photo = 5,
   Livre = 8
}

 

On n’oublie pas au passage qu’une énumération est définie par défaut comme un type int mais qu’il est possible de forcer ce type à tout type intégral comme byte, short, ushort, int, uint, long ou ulong. Il suffit pour cela de faire suivre le nom du type par deux points “:” suivi du nom du type intégral.

Ici encore il ne s’agit pas forcément d’élément du langage qui serait exotique, c’est juste qu’on l’a parfois lu une fois ou deux mais qu’on ne l’a jamais utilisé. Du coup on ne pense pas à s’en servir quand cela serait utile pour clarifier le code.

Event == Delegate

On l’oublie parfois mais la gestion des évènements n’est qu’une façon d’utiliser des delegates… Et tout delegate peut recevoir plusieurs méthodes attachées en utilisant += (et son contraire –= pour se désabonner).

Les évènements ne sont pas réservés aux seules gestions des clics des boutons ou autres contrôles. Souvent le développeur débutant ne comprends pas qu’il peut lui aussi utiliser ce mécanisme pour ces propres classes qu’elles soient d’UI ou des POCO’s agissant au niveau du DAL ou du BOL de l’application, voire ailleurs (ViewModels ou services par exemple).

C’est une façon de programmer très puissante. Sous Delphi (créé par Hejlsberg) les évènements existaient aussi mais un seul et unique abonnement était possible ce qui affaiblissait l’intérêt du mécanisme même s’il s’agissait déjà d’un grand progrès. C# a amené la souplesse à ce procédé en autorisant les abonnés multiples (pattern Observer dans les guides de design patterns).

Mieux on peut écrire totalement le code de son évènement pour obtenir un contrôle très fin (automatiser un processus dès lors qu’un abonnement est créé par exemple). il est ainsi possible de définir l’évènement SelectionEvent de la façon suivante :

public event EventHandler SelectionEvent(object sender, EventArgs args) 
  { 
     add 
     	{
	   if (value.Target == null) throw new Exception("No static handlers!");
	  // faire d'autres choses ici pourquoi pas...
       	  _SelectiveEvent += value;
	}
    remove
     	{ 
	  // ici aussi il est possible d'insérer du code...
	  _SelectiveEvent -= value;  
          // ici aussi... 
	}
  } 

...
private EventHandler _SelectiveEvent;

 

Bien entendu dans de nombreux cas une telle écriture n’est pas nécessaire puisque C# nous offre des moyens plus commodes et modernes pour définir de nouveau évènements. Mais les “anciennes pratiques” existent toujours dans le langage et elles ont l’avantage d’offrir plus de souplesse.

Utiliser les techniques les plus récentes est une bonne chose, mais cela donne l’impression qu’implicitement on est forcé d’oublier les anciennes constructions et leurs avantages. C’est une mauvaise approche du langage. Chaque construction possède ses avantages et ses inconvénients. Bien maitriser un langage pour écrire du bon code implique d’avoir le choix des constructions les mieux adaptées. Il est donc essentiel de se souvenir de toutes celles que le langage considéré offre aux développeurs, sinon il n’y a plus de choix éclairé. Les langages informatiques sont comme les autres, tombent facilement dans le dogme ou la pensée unique. Y échapper réclame un effort…

Parenthèses américaines et Chaînes de format

Un rappel hyper rapide : dans une chaîne de format les parenthèses américaines de type “curly”, c’est à dire { et } servent à insérer des variables passées à la fonction de formatage (comme String.Format par exemple.

Si on veut utiliser ces caractères dans la chaîne de sortie il suffit de les doubler dans la chaîne de format :

String.Format(“{{coucou}} M. {0}”,”Albert”) donnera en sortie “{coucou} M. Albert”

Le français utilise peu ces caractères raison pour laquelle certainement peut de gens s’intéressent à cette possibilité… mais parfois cela peut être pratique !

Checked et Unchecked

Maitriser quand un calcul doit “planter” ou non est une attention qui se perd… On espère certainement que tout ira bien et qui si ça va mal il se passera bien quelque chose et qu’il “suffira” de déboguer. Enfin j’imagine. Car en effet l’utilisation de Checked et Unchecked est vraiment rare dans les codes que je peux auditer !

Pourtant c’est utile.

short x = 32767;   // rappel de la valeur max pour un short
short y = 32767;
int a1 =  checked((short)(x + y));   	// OverflowException
int a2 =  unchecked((short)(x + y)); 	// retournera en silence -2
int a3 =  (short)(x + y);            	// retournera en silence -2

 

Détecter un calcul qui génère un overflow (débordement) est souvent crucial, alors pourquoi ne voit-on pas plus souvent Checked utilisé ? Mystère !

#error et #warning

En utilisant ces deux indicateurs on peut déclencher à volonté soit des erreurs de compilations (directive #error) ou des warnings (directive #warning).

Cela peut être très utile et plus efficace qu’un TODO car dépendant directement du code et du compilateur et non de l’EDI qui va présenter plus ou moins clairement les fameux TODO. Ensuite on peut utiliser ces directives dans des constructions #if ce qui leur donne encore plus d’intérêt.

Par exemple si votre code est conçu et optimisé pour le mode x64 vous pouvez déclencher une erreur de compilation uniquement si le développeur utilise “any CPU” à la compilation ou des choses encore plus spécifiques en jouant sur des #define.

L’intérêt de #error est de bloquer la compilation avec un message personnalisé. L’intérêt de #warning est de faire apparaitre un message personnalisé sous la forme d’un avertissement seulement.

Le petit GIF suivant montre tout cela en mouvement (sous LinqPad) :

ifelse

Encore une fois rien de bien époustouflant, juste une fonctionnalité de C# rarement utilisée alors qu’elle peut rendre de grands services parfois…

Les attributs

La plateforme .NET définie de nombreux attributs, chaque espace de noms ou presque ayant les siens. Les attributs servent à tout. C’est autant une feature du langage qu’un style de programmation. Certains frameworks MVVM en font par exemple usage pour marquer des classes : comportement, navigation… L’Entity Framework s’en sert pour reconnaitre les clés primaires ou les identifiants (et d’autres possibilités) alors que l’EDI utilise certains attributs pour afficher le nom des propriétés d’un Control. Bref, les attributs sont une possibilité du langage très utilisée par les frameworks mais relativement peu par les développeurs eux-mêmes alors qu’il est si facile d’en définir… Mais regardons ici quelques attributs touchant plus le langage et son interprétation.

DefaultValueAttribute

Cet attribut est surtout utile lorsqu’on développe un Control ou User Control. Il permet d’indiquer la valeur par défaut d’une propriété. Visual Studio ne mettra pas en gras la valeur si elle est égale à la valeur par défaut déclarée ce qui facilite la lecture de toutes les propriétés par l’utilisateur de la classe… Les valeurs par défaut sont ignorées à la sérialisation ce qui confère aussi un avantage sur la taille des instances sérialisées et du code XAML s’il y en a.

Curieusement l’attribut ne positionne pas de valeur. Il est juste informatif et la valeur par défaut doit tout de même être spécifiée ailleurs dans le code d’initialisation de l’instance (ou la déclaration du champ privé pour un backing field de propriété par exemple).

En utilisant la Réflexion VS sait aussi rétablir la valeur par défaut lorsqu’on demande un Reset d’une propriété dans l’EDI.

Il est possible d’utiliser la même astuce dans son propre code :

foreach (PropertyInfo p in this.GetType().GetProperties())
{
    foreach (Attribute attr in p.GetCustomAttributes(true))
    {
        if (attr is DefaultValueAttribute)
        {
            DefaultValueAttribute dv = (DefaultValueAttribute)attr;
            p.SetValue(this, dv.Value);
        }
    }
}

 

L’attribut lui-même se pose de la façon la plus standard qu’il soit. Imaginons une propriété Color dont la valeur par défaut est vert :

[DefaultValue(typeof(Color), "Green")]
public Color MaCouleur { get; set; }

 

Ne pas oublier d’ajouter dans le constructeur ou ailleurs dans le circuit d’initialisation de l’instance une affectation du type MaCouleur=Color.Green; sinon l’attribut n’aura pas l’effet escompté…

ObsoleteAttribute

Cet attribut est très pratique et peu utilisé aussi. Il ne sert pas seulement dans les frameworks à marquer les méthodes, membres ou classes qui deviennent obsolètes (et à préciser une description contenant généralement le nom de la classe, membre ou méthode à utiliser à la place), il peut aussi rendre de grands services dans une application tout à fait classique.

Par exemple supposons qu’on désire créer une méthode B qui prend en charge une opération de façon plus complète que la méthode A originale. En marquant obsolete cette dernière et en compilant on obtiendra immédiatement tous les endroits de l’application qui font usage de cette méthode de façon plus fiable qu’avec un Search… Pratique.

C’est aussi une bonne habitude de faire évoluer son code sans “tout casser”, créer de nouvelles interfaces, de nouvelles API’s le tout sans rendre l’ancien code totalement inutilisable. Et si c’est le cas raison de plus pour prévenir le développeur avec des messages de compilation.

L’une des causes de bogue est souvent le changement de sens, de qualité, ou de fonctionnalité pour une propriété ou une méthode. Le nouveau code marche très bien mais on a oublié que d’anciennes parties de code utilisent encore la méthode ou la propriété dans son ancienne signification. Et Boom! un jour ça casse et c’est difficile à déboguer ! Dans un tel cas il est bien préférable de créer une nouvelle propriété, une nouvelle classe ou nouvelle méthode, quitte à le faire par copier/coller et à changer juste ce qu’il faut dans la nouvelle copie. On utilisera un nom similaire avec une version par exemple Class Auto, devient Class Auto2 ou Auto2014… Bien entendu dans le même temps la classe Auto sera marquée obsolète. Tout code utilisant Auto sera assuré de fonctionner comme avant mais recevra un warning d’obsolescence, tout code utilisant Auto2 sait qu’il choisit la nouvelle version et assume les différences sans recevoir de warning.

Bref, marquer son code (classe, méthode, propriété, interface) avec l’attribut obsolete n’est pas réservé aux concepteurs de frameworks, c’est aussi une bonne pratique pour toute application !

La définition est évidente :

[Obsolete("Utiliser la méthode B à la place.")]
static void MethodA()

 

DebuggerDisplay

Cet attribut (défini dans System.Diagnostics) permet de contrôler comment une classe ou un champ sont affichés dans la fenêtre des variable du débogueur de VS. L’attribut peut être utilisé avec : les classes, les structures, les delegates, les énumérations, les champs, les propriétés et les assemblages.

Cet attribut joue un peu le même rôle que la surcharge de ToString() qui est une guideline essentielle pour aider à déboguer une application. En effet, au lieu de <NomdeType>, un ToString() surchargé peut retourner les valeurs essentielles de l’instance. Parfois même cela évite d’avoir à créer des convertisseurs de valeurs ou à écrire du code spécifique dans les ViewModels ! Que du bonheur !

Mais si on doit déboguer un code dans lequel ToString() n’a pas été surchargé ou bien qui ne retourne pas les informations dont on a besoin, il est bien plus intelligent d’utiliser l’attribut DebuggerDisplay car toucher au ToString() peut potentiellement casser du code existant…

Si les deux sont définis, pour la fenêtre du débogueur c’est l’attribut qui a la précédence.

Ce qu’on sait encore moins généralement sur cet attribut c’est qu’il est défini avec une chaine de caractères et que celle-ci peut contenir des accolades { } qui enchâssent un nom qui sera évalué dynamiquement. Ce nom peut être celui d’un champ, d’une propriété ou même d’une méthode !

exemples :

// affichera x = 22 y = 2 par exemple et il sera utilisé sur les
// deux propriétés x et y
[DebuggerDisplay("x = {x} y = {y}")]

// plus subtile en faisant appel à une méthode
// notamment getString()
[DebuggerDisplay("String value is {getString()}")]
// sortie possible : String value is [4,2,9,12]

// autre utilisation du même type
[DebuggerDisplay("{value}", Name = "{key}")]
public class KeyValuePairs { ... public object Key {get; set;} }

 

La souplesse de cet attribut est vraiment un plus qui peut faire gagner des heures lors d’une session de débogue et sans utiliser d’outils complexes ou de code spécifique pour les tests qui eux-mêmes font parfois perdre plus de temps qu’autre chose…

DebuggerBrowsable et DebuggerStepThrough

Comme on vient de le voir certains attributs ne servent qu’à Visual Studio et son débogueur afin de simplifier le travail de test ou de débogue d’une application (ou de tout code). C’est aussi le cas des deux attributs présentés ici.

Le premier permet d’indiquer au débogueur si une propriété ou autre est visible dans la fenêtre de débogue. Cela est très pratique pour cache une propriété un peu bidon, intermédiaire, ou qu’on a ajouté juste pour les tests ou qui n’a pas grand sens pour la session de débogue. En évitant d’avoir une vision brouillée par des choses inutiles dans la fenêtre de débogue on s’assure un débogage plus rapide !

Le second attribut permet d’indiquer que le code qui le suit doit être sauté par le débogueur. Cela aussi est très pratique car parfois le jeu des “step through” (sauts internes) lors d’un débogue fini par devenir un jeu de piste harassant dans lequel on se perd au bout d’un moment. Faire en sorte que le débogueur saute les parties n’ayant pas d’intérêt (et sans bricoler le code !) peut s’avérer payant et faire gagner beaucoup de temps …

Exemple d’utilisation :

[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string nickName;
public string NickName    {
    [DebuggerStepThrough]
    get { return nickName; }
    [DebuggerStepThrough]
    set { this.nickName = value; }
}

 

ThreadStaticAttribute

Cet attribut permet d’indiquer qu’un champ statique sera unique pour chaque thread. De fait le champ a beau être statique il peut prendre plusieurs valeurs… Une par thread. Cela s’avère très intéressant pour conserver des données dans un code thread-safe comme une connexion à une base de données différente pour chaque thread.

ThreadStatic permet d’éviter l’utilisation de Lock en multithreading dans certains cas, notamment quand la valeur doit être protégée contre les accès concurrents mais qu’elle peut être différente dans chaque thread. Dans un tel cas l’attribut permet de créer autant de variables qu’il y a de threads supprimant toute collision éventuelle mais en conservant une écriture fortement typée du code et la sémantique de “static”.

L’utilisation de cet attribut ayant du sens au sein de code multithreadé uniquement et ce type de code étant long à démontrer le plus souvent je laisse le lecteur faire des recherches sur Google pour trouver plus de documentation.

FlagsAttribute

Les énumérations sont souvent utilisées pour indiquer des options possibles, c’est presque leur unique utilisation. La plupart du temps il s’agit options simples : un objet peut être d’une couleur ou d’une autre, un employé peut être en activité ou non, une machine peut être dans un état ou un autre… Mais il existe des cas où les options doivent pouvoir se combiner : un objet peut avoir une couleur qui est une combinaison de celles définies, certains états d’une machine peuvent être superposables, des options de recherche par exemple peuvent être cumulables, etc.

Pour simplifier, si on doit affiché des choix avec ces énumérations visuellement les premières énumérations peuvent être affichées dans des ListBox ou des ComboBox, voire des boutons radio, alors que les secondes n’ont de sens qu’avec un ensemble de CheckBox.

Si le premier cas d’utilisation est disponible par défaut et s’il est bien compris et bien utilisé la plupart du temps, le second cas est souvent négligé. Je rencontre très peu de code qui en fait (bon) usage et c’est dommage.

Car il existe un attribut, FlagsAttribute, dont le but est justement de marquer une énumération de telle sorte à ce que ces différents valeurs soient cumulables.

Il est vrai que cet attribut est plus informatif que fonctionnel. C’est à dire que son utilisation ne dispense pas de définir soi-même les valeurs correctes (puissances de 2) pour les constantes de chaque item de l’énumération et que attribut ou pas attribut si on respecte cette règle les choses marcheront de la même façon…

Toutefois FlagsAttribute confère au moins deux avantages au code : la clarté de l’intention (les options peuvent être combinées avec un OU) et l’accès à certaines méthodes comme HasFlag() dont l’effet n’est pas garanti sinon.

Alors lorsque vos énumérations représentent des options cumulables par un OU, n’oubliez pas de les définir comme des flags en veillant à ce que les options soient bien des puissances de 2.

Comme on le voit dans le code ci-dessous on peut définir dans l’énumération des valeurs qui sont déjà des combinaisons numériques de plusieurs valeurs, ce qui facilite l’utilisation de l’énumération pour les combinaisons les plus fréquentes.

void Main()
{
	var none = TestEnum.None;
	var a = TestEnum.Read;
	var b = TestEnum.Write;
	var c = TestEnum.ReadWrite;
	var d = TestEnum.Locked;
	Console.WriteLine("None; Valeurs : {0}; {1}; {2}; {3}; {4}",
none,a,b,c,d); Console.WriteLine("None; (short)Valeurs : {0}; {1}; {2}; {3}; {4}",
(short)none,(short)a,(short)b,(short)c,(short)d); var e = TestEnum.WriteLocked; var f = TestEnum.Write | TestEnum.Locked; Console.WriteLine("e= {0}; f={1}; e==f? {2}",
e,f, e==f?"OUI":"NON"); Console.WriteLine("Has Flag 'Locked' ? {0}",
e.HasFlag(TestEnum.Locked)); } // Define other methods and classes here [FlagsAttribute] public enum TestEnum : short { None = 0, Read = 1, Write = 2, Locked = 8, ReadWrite = 3, WriteLocked = 10 }

 

Ce qui produira la sortie suivante :

None; Valeurs : None; Read; Write; ReadWrite; Locked
None; (short)Valeurs : 0; 1; 2; 3; 8
e= WriteLocked; f=WriteLocked; e==f? OUI
Has Flag 'Locked' ? True

ConditionalAttribute

Ce dernier attribut que je vous présente est vraiment un truc à connaitre. En effet il permet de marquer une méthode afin que son exécution soit lié à une définition d’identificateur (#define).

Certes il est toujours possible d’écrire du code que celui-ci :

#if DEBUG
void ConditionalMethod()
{ ... }
#endif

 

Mais un tel code est à la fois lourd et n’est pas sans poser de petits problèmes… Notamment parce que dans le cas ici ou DEBUG n’est pas défini la méthode “ConditionalMethod” n’existera tout simplement pas ! Ce qui implique que la compilation plantera si elle est appelée dans le reste du code…

Pas très pratique. Il y a mieux, l’attribut Conditional. Il permet à la fois une écriture plus propre (pas de #if/#endif) et plus lisible mais surtout il supprime l’exécution de la méthode même là où elle est appelée et cela sans erreur de compilation ni besoin d’intervenir !

On écrira alors plutôt :

[Conditional("DEBUG")]
static void DebugMethod()
{ ... }

 

Bien entendu n’importe quelle condition peut être testée et l’intérêt de Conditional va bien au-delà du débogue où elle est certes d’une aide précieuse (instrumentalisation d’un code qui apparait ou disparait automatiquement dès lors qu’on est dans le mode de débogue ou non). Des méthodes peuvent ainsi être appelée et exécutée par un code commun uniquement sous certaines conditions (Windows 8 ou Windows Phone par exemple) sans écrire un seul #if

Les mots clés

C# est un langage assez concis et généralement on en connait tous les mots clés, mais comme la section “syntaxe” nous l’a fait voir cette connaissance n’est pas forcément profonde… Il y a des mots clés qu’on sait exister sans jamais les avoir utilisés. Certes on aura l’impression de ne rien apprendre de nouveau, mais en réalité on ne penserait pas à les utiliser quand le bon moment se présentera…

C’est particulièrement le cas du premier mot clé de cette section, yield.

Yield

Yield fait partie de ces mots clés dont on a forcément entendu parlé mais qu’on ne saurait pas forcément utiliser correctement.

Yield sert à retourner des valeurs, une par une (ainsi que la fin de la liste) à l’intérieur de bloc de code de type itérateur.

La syntaxe est la plus simple du monde puisqu’on écrit soit yield return <expression>; soit yield break; pour stopper l’itération (ou énumération).

Ce n’est donc pas un problème de syntaxe qui m’amène à vous parler de yield, sinon il serait apparu plus haut dans la section consacrée à cet aspect du langage. C’est logique.

Avec yield le problème c’est plutôt de bien comprendre comment l’utiliser. Pourtant ce n’est pas bien compliqué. Comme je le disais yield s’utilise dans un itérateur. Mais qu’est-ce que c’est qu’un itérateur ?

Un itérateur c’est une section de code qui retourne une séquence ordonnée de valeurs du même type (définition tirée de la documentation MSDN).

Un itérateur peut être utilisé comme corps d’une méthode, d’un opérateur ou d’un accesseur Get. Il utilise yield dont je viens de parler soit pour retourner une valeur soit pour indiquer la fin de la séquence.

Une classe peut implémenter plusieurs itérateurs. La contrainte étant, ce qui tombe sous le sens, qu’ils aient des noms différents et qu’ils puissent être invoqués par un Foreach de la façon suivante : foreach (var v in Voiture.Modèles)Voiture est une instance de la classe imaginaire Véhicule et où Modèles est l’itérateur qui retourne la liste des modèles de la voiture en question. Ceci est bien entendu un exemple fictif servant à situer le morceau de code.

Le type de retour d’un itérateur doit être choisi parmi les suivants : IEnumerable, IEnumerator, IEnumerable<T> ou IEnumerator<T>.

Je n’entrerai pas dans les détails de ces types, MSDN sera comme d’habitude un ami fidèle pour ce genre de recherches (même si en général il est préférable de chercher sous Google pour accéder à ce qu’on désire dans MSDN ce qui est un comble…).

Pour mieux comprendre prenons un exemple d’itérateur implémenté au niveau de la classe. Ici nous définirons une classe DayOfTheWeek (jour de la semaine) que nous pourrons balayer avec un foreach (l’une de ses instances, pas la classe elle-même bien sûr).

void Main()
{
    // Création de l’instance
    DaysOfTheWeek week = new DaysOfTheWeek();

    // Itération
    foreach (string day in week) Console.Write(day + "; ");
}

public class DaysOfTheWeek : System.Collections.IEnumerable
{
    string[] m_Days = { "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim" };

    public System.Collections.IEnumerator GetEnumerator()
    {
        for (int i = 0; i < m_Days.Length; i++)
            yield return m_Days[i];
    }
}

Ce qui retournera à la console : Lun; Mar; Mer; Jeu; Ven; Sam; Dim;

Comme on le voit sur cet exemple la classe DayOfTheWeek implémente l’interface IEnumerable. De fait toute instance de cette classe se comporte comme un fournisseur de liste. Cela impose de déclarer GetEnumerator() qui est la méthode imposée par l’interface. Son type est IEnumerator.

Cet énumérateur se contente de faire une boucle qui retourne à un à un les éléments de la liste m_Days. Pour ce faire la méthode utilise un boucle for calée sur le nombre d’éléments de la liste, chaque itération utilisant yield return <expression> (l’expression étant ici un simple élément de la liste).

L’avantage des itérateurs est évident puisque la méthode appelante n’est pas obligée de traiter une liste entière, uniquement les éléments qu’elle reçoit et qu’elle peut effectuer un travail sur chaque élément puis demander le suivant ou non. Une liste se retourne d’un bloc, pas élément par élément.

Avec IEnumerable et l’implémentation d’une méthode retournant un IEnumerator on peut ainsi fournir des valeurs les unes après les autres. Ces valeurs sont soit issues d’une liste interne comme dans l’exemple ci-dessus soit calculées à la volée ce qui devient plus intéressant en ouvrant des horizons nouveaux…

Revenons à yield. Il s’utilise donc dans un bloc de code pour fourni un objet énumérateur ou signaler la fin d’une séquence. Comme on le remarque au passage sur notre exemple la fin de séquence est optionnelle, ici nous avons choisi de balayer une liste interne par for et nul besoin d’un signal de fin, la boucle for s’arrête d’elle-même.

On notera que yield se comporte comme une sorte de callback puisque dans un foreach chaque élément est retourné un à un, ce qui impose un aller retour entre l’intérieur de la boucle de l’énumérateur et le code appelant. Dans notre exemple cela signifie que la boucle for interne à GetEnumerator() est saucissonnée élément par élément et que le foreach appelant obtient une réponse à la fois qu’il peut traiter comme il le veut avant de réclamer la suivante. Ce mécanisme est en lui même intéressant à comprendre car il est à la base de nombreuses utilisation “inventives” qui peuvent être faites de yield…

Si nous avons vu dans l’exemple de code comment faire pour qu’une classe soit d’emblée énumérable, le code suivant montre comment implémenter un énumérateur nommé à l’intérieur d’une classe n’étant pas elle-même énumérable.

Ici la classe Calcul est une classe statique de service, elle fourni des opérations mathématiques un peu comme l’espace de noms Math du framework. Parmi les opérations disponibles on trouve Power qui élève un nombre à une puissance donnée. Toutefois ici ce n’est pas seulement le résultat du calcul qui nous intéresse mais aussi les “valeurs intermédiaires” de celui-ci. De fait la méthode Power() va être implémentée sous la forme d’un IEnumerable(type de retour) qui renverra chaque valeur intermédiaire jusqu’à la valeur finale. Power étant obtenu par des itérations successives c’est une opération qui se prête bien à l’utilisation de yield. Voici ce code :

void Main()
{
    foreach (int i in Calcul.Power(3, 9)) Console.Write("{0}; ", i);
}

public static class Calcul
{
    public static IEnumerable Power(int number, int exponent)
    {
        int counter = 0;
        int result = 1;
        while (counter++ < exponent)
        {
            result = result * number;
            yield return result;
        }
    }
}
Ce qui retournera à la console  : 3; 9; 27; 81; 243; 729; 2187; 6561; 19683;

 

Yield peut aussi être utilisé pour fabriquer des listes filtrées depuis d’autres listes… regardez le code très simple ci-dessous :

static IEnumerable<int> FilterWithYield()

{
            foreach (int i in MyList)
            {
                if (i > 10) yield return i;
            }
} 

 

Partant d’une liste (MyList) supposée contenir des entiers, la méthode FilterWithYield est définie comme un IEnumerable d’entiers et son code utilise un foreach sur la liste originale en conjonction avec un yield uniquement quand la valeur obtenue est supérieure à 10. De fait on obtient une sorte de vue vivante sur MyList filtrée selon des critères qui peuvent être complexes. La valeur retournée peut même être calculée en fonction de la valeur originale. Ce n’est qu’un exemple parmi des centaines…

Comme on le voit yield est un mot clé plein de ressources ! Il est directement connecté à la notion de bloc de code de type énumérateur et aux interfaces IEnumerable que nous avons vu à l’œuvre et IEnumerator qui n’est que la classe de base dans le framework .NET pour tous les énumérateurs non génériques.

Il n’y a rien de compliqué à comprendre dans yield lui-même mais la difficulté semble être plutôt d’y penser et d’imaginer du code qui en tire partie. Le présent rappel vous donnera peut-être envie de vous en servir un peu plus souvent dorénavant …

Default

Même si toutes les variables de types par référence peuvent se voir mettre à null sans soucis “null” n’est pas forcément “null”, notamment pour les types par valeur. C’est le cas pour les booléens (false), les entiers (0) et tous les types par valeur en général.

Lorsqu’on écrit du code générique il y a des circonstances où connaitre à l’avance la “bonne” valeur “null” à indiquer est impossible.

C’est là qu’intervient default(T). Plutôt que d’utiliser une constante comme “null” qui ne marchera pas à tous les coups, mieux vaut utiliser default(T) qui a l’avantage de fonctionner dans tous les cas…

var a = default(int); // donnera int a = 0
var b = default(bool) // donnera bool b = false

Volatile

En voilà une si vous me dîtes que vous le connaissez déjà j’aurai quelques doutes et si en plus vous m’assurez que vous l’utilisez tous les jours j’aurai vraiment du mal à vous croire sans preuves solides ! Sourire

Nous avons vu dans un récent billet l’utilisation de Interlocked qui permet d’incrémenter ou décrémenter des variables de façon fiable en multithreading évitant souvent l’utilisation de lock().

Volatile rempli un peu un rôle similaire au niveau d’un champ. S’il est déclaré volatile les lectures et écritures concurrentes sont assurées de travailler sur la dernière valeur disponible (au lieu d’une valeur en cache dans un registre du CPU par exemple). Volatile obtient des “aquire-fence” pour les read ou des release-fence pour les write.

Volatile ne signifie pas seulement “s’assurer que le compilateur et le jitter ne feront aucune réorganisation de code ou du caching dans les registres du CPU ni aucune autre optimisation”, ce mot clé signifie aussi “demander aux processeurs de faire le nécessaire pour que quoi qu’ils fassent ils s’assurent de délivrer la dernière valeur disponible, même si cela signifie d’arrêter tous les processeurs pour qu’ils synchronisent leurs caches entre eux et avec la mémoire centrale”.

Toutefois il faut se méfier un peu de cette “garantie” car le compilateur C# s’offre quelques libertés pour optimiser le code final. Par exemple il peut swapper des reads et des writes. Cela est rare puisque un read/read, read/write ou un write/write ne seront pas swappés, mais un write/read peut l’être… Et dans ce cas Volatile ne sert plus à grand chose.

Ceci expliquant cela, le multithreading étant déjà bien assez subtile comme cela et réservant suffisamment de surprises, il est probable que Volatile ne soit par trop biscornu pour être devenu populaire…

Il en va de même de Threading.MemoryBarrier() que vous ne connaissez certainement pas et qui permet pourtant en multithread d’obtenir le même type de barrière pour une portion de code.

Volatile ne peut agir sur les variables passées par référence, c’est pourquoi le framework offre VolatileRead() et VolatileWrite() qui sont construits en utilisant MemoryBarrier();

Voici un bout de code qui met en évidence le problème (à compiler en mode Release avec les optimisations pour avoir une chance de voir le blocage) :

class Test
{
    private bool _loop = true;

    public static void Main()
    {
        Test test1 = new Test();

        // _loop est mis à false dans un autre thread
        new Thread(() => { test1._loop = false;}).Start();

        // normalement la lecteur doit être ok… mais…
        while (test1._loop == true) ;

        // ...la boucle ne termine jamais…
    }
}

En ajoutant volatile à la définition de _loop le problème cesse.

Mais rassurez-vous, si tout cela ne vous dit rien c’est que vous n’êtes pas un expert en multithreading et que tant que vous savez utiliser lock() à bon escient ou des objets immuables vous pourrez tout de même écrire du code multithread tout à fait correct !

Extern Alias

Encore une possibilité du langage qui n’est pas souvent utilisée pourtant dans certains gros projets qui connaissent des améliorations dans le temps cela pourrait l’être avec plus de bonheur que de trafiquer les espaces de noms existants…

Car de quoi s’agit-il ici ? Le problème est simple, un code peut très bien vouloir accéder à deux versions différentes d’un même espace de nom. Supposons que dans la première version de notre application nous ayons créé un objet Grille qui a donné le binaire grid.dll. Supposons maintenant que nous ayons au fil du temps décidé de créer une amélioration de cette grille compilée dans le binaire grid2.dll. L’assemblage est exactement le même, les espaces de noms sont identiques, les classes aussi. C’est juste le code qui diffère.

Imaginons maintenant que notre application doit utiliser la nouvelle grille dans une page où l’ancienne est aussi utilisée et que pour des raisons variées on ne puisse pas ou ne veuille pas remplacer l’ancienne en place. Il faudra donc dans ce code accéder à deux classes Grille définies dans le même espace de noms… Et là ce n’est pas possible. Il faut que l’arborescence de noms possède une petite différence au moins sinon le compilateur ne sait pas quelle classe choisir.

La solution vue le plus souvent est le “bricolage” de l’espace de nom de la nouvelle classe ou bien le changement de nom de cette dernière. Cela n’est pas optimal. Créer une différence fictive entre deux espaces de noms qui sont en réalité identiques est la porte ouverte à toutes les confusions. Quant à attribuer un nouveau nom à une même classe rendant les mêmes services à quelques nuances près, là encore la porte s’ouvre sur les bogues à venir tout en rendant difficile l’échange là où il est possible (puisqu’il faudra changer le nom de classe utilisé dans le code existant).

Il y a mieux. Il y a Extern Alias. Cela fonctionne comme la définition d’un alias sur un espace de noms sauf qu’ici on indique le nom de l’assemblage binaire.

Sous VS, dans les références, on peut voir que par défaut les assemblages ont une propriété “Alias” qui est positionnée à Global. C’est en donnant un nom différent  ici qu’on peut utiliser ce dernier avec “extern alias” dans son code.

Si nous définissions Grille1 et Grille2 comme alias pour les références aux deux assemblages, dans le code on peut maintenant utiliser Grille1::Grille pour accéder à la classe Grille dans version 1 et Grille2::Grille pour sa version 2, le tout sans changer le code existant, sans bricoler les espaces de noms etc.

Bien entendu il ne s’agit pas ici d’une guidline, on doit éviter de se retrouver dans de telles situations, mais elles existent et alors il faut savoir utiliser ce que le langage propose pour y répondre de la façon la plus appropriée…

On notera pour terminer que “extern” est aussi utilisé pour désigner un code externe non managé, ce qui n’a rien à voir avec le présent topic…

Les autres possibilités de C#

Il existe d’autres possibilités du langage qui restent peu utilisées. En tout cas on rencontre soit des gens qui s’en servent sans problème soit d’autres qui les ignorent totalement. La situation médiane se retrouvant bizarrement assez rarement sur le terrain.

Nullable

Il en va ainsi des types “nullables”, c’est à dire ces types par valeur qui normalement ne peuvent pas être nuls, comme un booléen par exemple. A l’initialisation de la mémoire un booléen est rempli de “0” sa valeur numérique vaut zéro, et cela se traduit par “false”. Il peut être positionné à “true”, remis à “false”, etc, mais jamais il ne sera “null”.

Les types “nullables” exploitent un artifice de notation lors de la déclaration d’un champ, le point d’interrogation derrière le nom du type, pour autoriser la valeur nulle dans de tels cas.

Nous ne discuterons pas de l’utilité ni du bon usage des nullables, ils existent, c’est une fonctionnalité du langage, et cela rend service à qui sait s’en servir !

La notation est par exemple pour définir une variable “a” de type entier et nullable (et lui affecter la valeur nulle au passage) : int? a = null;

C’est d’ailleurs dans ces cas là que l’opérateur ?? que je vous ai présenté plus haut prend son sens. Car maintenant notre “a” peut très bien être nul, ce qui impose de l’utiliser en prenant en compte cette éventualité b = a ?? 0; permet d’attribuer à “b” la valeur de “a” s’il n’est pas nulle et la valeur zéro dans le cas contraire, en une instruction et sans “if”.

Les types nullables sont très utiles pour des statistiques ou des questionnaires. Ils permettent par la valeur nulle de matérialiser la “non réponse”. Une question “nombre d’enfants” si elle est stockée dans un entier (ce qui permettra ensuite des calculs comme la somme, la moyenne, la médiane…) n’est pas à même de faire la différence en “zéro enfant” comme réponse et “zéro enfant” parce que la réponse n’a pas été donnée. En utilisant un int? pour conserver la valeur on peut attribuer à zéro sa véritable signification (pas d’enfant) car en cas de non réponse la valeur sera nulle.

Il y a d’autres domaines où les nullables peuvent jouer un rôle important, mais cela dépasse le sujet traité !

Conclusion

Je vais conclure ici car ce n’est qu’un billet pas un livre sur C#… Les subtilités, grandes ou petites, sont nombreuses et il n’est pas possible de faire le tour de la question même dans un grand billet.

Déjà ici tracer une ligne claire est difficile. Certains lecteurs trouveront pour certains passages qu’il s’agit du B.A.BA du langage alors que d’autres y découvriront une possibilité qu’ils n’ont jamais utilisée, et ce même si globalement il s’agit de développeurs de même niveau, chacun n’ayant pas les mêmes “lacunes”.

Le but à peine caché de ce billet n’est donc pas d’être exhaustif mais bien d’attirer votre attention sur C# qu’on pense connaitre alors qu’il réserve bien des surprises à celui qui farfouille un peu.

Développer est un plaisir, cela doit l’être en tout cas. Un langage est un ami de tous les jours. Bien connaitre ses amis permet de passer de meilleurs moments ensembles…

Stay Tuned !

Faites des heureux, partagez l'article !
blog comments powered by Disqus