Oubliés.NET

Cet articles présentes différentes fonctionnalités du Framework .NET qui sont présentes depuis parfois très longtemps mais qui sont rarement utilisées par les développeurs et sont donc passées dans l’oubli (ou n’ont jamais été découvertes). De plus, avec l’arrivée de .NET Core, certaines de ces fonctionnalités vont revenir au goût du jour et hanter les mesures de contournement que nous avons développés à l’époque.

Fonctionnalités oubliées/méconnues

Je vais d’abord vous présenter une liste de fonctionnalités qui sont peu ou pas utilisées alors qu’elles sont très pratiques… et souvent refaites lors d’un développement.
Après, je vais présenter des fonctionnalités (pour certaines oubliées aussi) qui vont permettre de mieux fonctionner avec .NET Core.

DebuggerDisplayAttribute

“Tout bon développeur n’a pas besoin de déboguer son code car il est parfait !”
”Dans le temps, on réfléchissait avant d’écrire du code et on n’avait pas besoin de déboguer.”
”Je trouve que c’est plus facile d’écrire du code sans bug, ça va plus vite.”

Si une de ces trois phrases vous décrit, alors le DebuggerDisplayAttribute ne vous servira à rien ! Si ces phrases ne vous concernent pas, alors vous allez l’apprécier.

DebuggerDisplayAttribute permet d’améliorer le rendu visuel d’une classe dans la section “Espion/Watch” de Visual Studio.
Ce rendu permet de rapidement analyser les données et il est généré par une chaîne de caractère selon votre propre format.

[DebuggerDisplay("{_identifiant}: {Nom}, {Prenom}")]
class Utilisateur
{
public string Nom { get; set; }
public string Prenom { get; set; }
private string _identifiant;

public Utilisateur(string identifiant)
{
this._identifiant = identifiant;
}
}

Le code [DebuggerDisplay(« {_identifiant}: {Nom}, {Prenom} »)] permet donc d’associer l’attribut à notre classe et d’indiquer à la fenêtre espion qu’elle doit l’afficher sous la forme d’une chaîne composée des éléments _identifiant, Nom et Prenom. Que ces éléments soient des propriétés ou des membres, publics ou privés, la fenêtre espion va les afficher.

image

Le seul inconvénient est que si le nom d’un élément (propriété/membre) change, il sera affiché comme une erreur dans la fenêtre espion.

Console.WriteLine / Console.Out.WriteLine / Console.Error.WriteLine

Tous les “Hello World” en C# font un Console.WriteLine, mais ce faisant, il cachent une des fonctionnalités de base de la console qui est bien connue des spécialistes Linux et les fans du C :

Il existe plusieurs consoles dans une console ! En C, nous pouvons utiliser stdout et stderr pour afficher des messages à l’écran. stdout cible la console standard se sortie et stderr cible la console des erreurs. Tout cela remonte aux racines  même du langage et de la technologie qui permet d’envoyer les messages standards dans un fichier mais d’afficher uniquement les messages d’erreurs à l’écran. (ou l’inverse, vous avez compris !)

En .NET, il est possible de faire la même chose avec Console.Out et Console.Error.

Cette fonctionnalité est très utile lorsque vous créez des application console de traitement différé et que vous voulez réserver un traitement différent aux erreurs.

Souvent, un lot va créer un fichier de log et l’envoyer par courriel à un opérateur qui doit l’analyser. Ce lot contiendra une section reprenant le “tout ce qui va bien” et une section reprenant le “tout ce qui va mal”. C’est alors au développeur qui réalise le lot de gérer ces deux sections dans l’envoi de courriel.
Il est facile de simplifier le développement de d’indiquer au développeur que ce qui va bien est pour Console.Out et ce qui va mal est pour Console.Error. Après cela, il suffit d’implémenter une redirection du “stderr” vers le fichier qui sera envoyé à l’opérateur. De cette manière, l’opérateur n’aura plus qu’à s’occuper des erreurs.

Console.WriteLine est juste un raccourci vers Console.Out.WriteLine :

image

 

Pour paraphraser un de mes collègues, nous allons maintenant faire du crunchy et aborder la gestion du parallélisme!

Interlocked

Interlocked est une classe qui permet de faire des opérations de base de manière atomique.
Chaque développeur a dû, à un moment de sa vie, réaliser le code suivant :

temporaire = a;
a = b;
b = a;

Non seulement ceci est du gaspillage de lignes, ce code n’est pas thread-safe si la variable a ou b peuvent être modifiées par une autre portion de code.
Si c’est le cas, et que vous en êtes conscient, vous pouvez entourer cette section de code par un verrou logique :

object _objetVerrou = new object();
private void Inverser()
{
lock (_objetVerrou)
{
temporaire = a;
a = b;
b = a;
}
}

C’est là qu’intervient la classe Interlocked qui propose des opérations atomiques telles que Exchange, Add ou CompareExchange pour ce genre de variables.
Pour refaire notre exemple, Exchange permet d’échanger deux valeurs :

int a = 0;
int b = 1;
a = Interlocked.Exchange(ref b, a);
//a vaut 1 et b vaut 0

Par contre, la distance sémantique peut être grande ! La fonction se lit :  “ la fonction retourne la valeur de b et la valeur de a vers être envoyée dans b ”.

Thread.Yield / Thread.Sleep(1) / Thread.Sleep(0) / Thread.SpinWait(0)

Question difficile : Quelle est la différence entre Thread.Sleep(0) et Thread.Sleep(1) ?
Celui qui répond 1 ms a partiellement raison…

Thread.Sleep(0) indique au système d’exploitation qu’un autre thread de priorité identique ou supérieure peut récupérer sa tranche de temps.
Thread.Sleep(1) indique au système d’exploitation que n’importe quel autre thread peut récupérer sa tranche de temps.
Thread.Yield indique au système d’exploitation qu’un autre thread du processus actuel peut récupérer sa tranche de temps.

Après la théorie, la pratique !
Dans mon cas, j’ai deux thread, le premier qui produit des données et un autre qui traite les données. Le consommateur est en attente du producteur car il a déjà consommé toutes les données.
Doit-il utiliser Thread.Sleep(0), Thread.Sleep(1) ou Thread.Yield ?

Théoriquement, il devrait utiliser Thread.SpinWait(x) avec x qui est le nombre de fois que vous voulez attendre, pas le temps.
En effet, il serait étrange d’indiquer au système d’exploitation que vous voulez attendre 1 ms alors que ce que vous faites, c’est attendre que quelqu’un d’autre ait fini son travail.

Quand Thread.Sleep est indiqué dans du code, il doit être documenté pour justifier sa présence alors que SpinWait possède une représentation sémantique beaucoup plus grande.

while (!donneesAConsommer)
{
Thread.Sleep(1); // = Pourquoi sleep 1 ? Veut-on attendre 1 ms ou veut-on donner la main à un autre thread ?
}

while (!donneesAConsommer)
{
Thread.Sleep(0); //= Pourquoi sleep 0 ? A quel autre thread de priorité identique ou supérieure voulons-nous donner la main ?
}

while (!donneesAConsommer)
{
Thread.Yield(); // = A quel autre thread voulons-nous donner la main ??
}

while (!donneesAConsommer)
{
Thread.SpinWait(1); // = j'attends parce que je n'ai rien d'autre à faire'
}

Lazy<T> / Lazy<T>(true)

Beaucoup de développeur ont déjà développé le patron d’initialisation à la demande de la manière suivante :

StringBuilder _chaineConcatenation;
StringBuilder ChaineConcatenation
{
get
{
if (_chaineConcatenation == null)
{
_chaineConcatenation = new StringBuilder();
}
return _chaineConcatenation;

}
}

Cette solution permet de n’initialiser l’object _chaineConcatenation qu’au moment où il est vraiment requis.
Cela permet de simplifier la gestion du cycle de vie bien qu’allant à l’encontre du principe de responsabilité unique. En effet, dans ce cas la méthode get couvre deux responsabilités, initialiser un objet et retourner le membre associé à une propriété.

Pour simplifier la réalisation de ce genre de code tout en gardant la fonctionnalité de création au moment où c’est requis, il est possible d’utiliser l’objet générique Lazy<T> de la manière suivante :

Lazy<StringBuilder> ChaineConcatenationLazy { get;  } = new Lazy<StringBuilder>();

Il est intéressant de remarquer qu’au lieu de créer un objet à la demande, je me retrouve à systématiquement créer un objet Lazy<StringBuilder> mais que maintenant mon code est rendu thread-safe.
En quoi est-il thread-safe ?

Imaginons deux thread, tA et tB qui utilisent la variable x et qui font le code suivant :

x =0;
Console.WriteLine(x);
x++;
if(x==0){
    x=-1;
}

Si les deux threads sont démarrés au même moment, pouvez-vous me dire que vaut x à la fin de l’exécution des deux threads et ce qui sera affiché par le Console.WriteLine ?

Non. Ce n’est pas possible sans mécanisme de synchronisation.
Si nous reprenons notre exemple de la propriété ChaineConcatenation, et que deux thread essaient d’utiliser la variable au même moment, les deux threads peuvent valider la condition if(_chaineConcatenation==null) au même moment et tous les deux exécuter la même instruction _chaineConcatenation = new StringBuilder().

Bien sûr, il est toujours possible d’entourer la création de la chaîne par un lock() mais il est beaucoup plus facile d’utiliser Lazy<T>… soyons un peu fainéant !

Sauf que pour que Lazy<T> soit thread safe, il faut utiliser la construction new Lazy<T>(true) !

Lazy<T>() n’est pas thread safe par défaut ! (Donc mon code ci-dessus n’était pas vraiment thread-safe…)

 

.NET Core ?

Comment est-il possible d’oublier une fonctionnalité de .NET Core alors que c’est tout nouveau ?
.NET Core permet maintenant de facilement réaliser des applications .NET utilisables sur d’autres plateformes que Windows. Les autres plateformes que Windows sont régies par d’autres règles qui peuvent grandement influencer la portabilité de votre code. (Certes, MONO le permettait déjà mais il n’était pas courant pour des développeurs dans le monde Windows de développer sur MONO, c’était principalement réservé au monde Linux.)

Environment.NewLine

Un retour à la ligne dans un fichier sous Windows est indiqué par les caractères ASCII 13 et ASCII 11, plus simplement représentés sous la forme “\r\n”. Dans le monde UNIX (et aussi LINUX), le retour à la ligne est uniquement représenté par ASCII 11 (“\n”). Ceci a comme impact que si vous modifiez un fichier d’un environnement avec les règles de l’autre, votre fichier pourrait être inutilisable !
Dans cette situation, il faut utiliser la variable Environment.NewLine qui permet de toujours avoir la bonne séquence à utiliser.

Bien que cela paraisse comme une nouveauté, voire surprenant, que Microsoft pousse pour l’utilisation de .NET sur d’autres plateformes, Environment.Newline existe depuis la version 1.0 du Framework .NET.  (C’est le genre de coïncidences troublantes qui font croire à une éventuelle théorie du complot…)

image

Path

La classe Path contient plusieurs fonctions qui permettent de travailler avec des noms de fichiers et de répertoires sans devoir gérer certains aspects liés au système d’exploitation.
La fonction que je trouve la plus utile dans Path et que je vois rarement utilisée est Path.Combine qui permet de cumuler plusieurs nom de répertoire afin de générer un chemin complet.

Ex :  Path.Combine(“Répertoire1”,”Répertoire2”,”Répertoire3”,”NomDeFichier.Txt”) va générer la chaîne “Répertoire1\Répertoire2\Répertoire3\NomDeFichier.Txt” sous Windows.

De la même manière, Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())) permet de générer un nom de fichier aléatoire dans le répertoire %Temp% de l’utilisateur courant… fonction qui existe déjà : Path.GetTempFileName().

Il est aussi intéressant de noter que Path.Combine utilise sa propriété Path.InvalidPathChars pour vérifier que le nom de fichier qu’il va générer est accepté sur le système d’exploitation sur lequel il est lancé… et que c’est une des seules manières simples de faire cette validation. En utilisant ILSpy pour la méthode Path.Combine, on peut voir ceci :

image

et en sélectionnant la méthode “CheckInvalidPathChars”, on découvre que c’est une méthode internal, donc inaccessible à du votre/notre code :

image

 

IsolatedStorage

IsolatedStorage permet d’utiliser une zone de stockage (répertoire et fichiers) que vous créez dynamiquement avec votre application. En dehors d’un environnement hautement sécurisé, vous êtes pratiquement certain d’avoir les accès en écriture et lecture sur les éléments que vous allez créer.

Tous les fichiers que vous allez créer seront placés dans le répertoire C:\Users\[utilisateur]\AppData\Local\IsolatedStorage\[chaine représentant l’assembly/l’application]\[nom du fichier]

Ceci est principalement utilisé dans les applications de type client-riche qui ne nécessitent pas de programme d’installation. Avec cette technique, il est possible de déployer les applications sur les postes des utilisateurs en mode “copier-coller” sans que soit requis une installation et que lors de l’exécution, l’application télécharge les composants/fichiers requis.

Il est aussi typiquement utilisé pour enregistrer les préférences d’un utilisateur.

Attention que l’espace disponible dans cet emplacement est régi par les quotas définis sur le réseau.

IsolatedStorageFile isi = IsolatedStorageFile.GetUserStoreForAssembly();
var fichier = isi.CreateFile("nomdefichier.txt");
StreamWriter writer = new StreamWriter(fichier);

Conclusion

Certaines fonctionnalités du framework .NET sont anciennes, très anciennes, et méconnues et celui qui ne connaît pas l’histoire sera amené à la reproduire… idem pour les technologies !
Celui qui ne sais pas qu’une fonctionnalité existe sera amené à la reproduire (et la déboguer) !