Suite à la rédaction d’un “Guide du développeur” pour ma boite, j’ai eu l’idée de rédiger un article référençant les bonnes pratiques en C#/.NET. L’idée n’est pas de faire une énième liste comme il en existe des dizaines sur le net, mais d’essayer d’apporter des astuces pertinentes et argumentées par rapport à mon expérience. Et surtout, ce que je souhaiterais, c’est d’avoir des retours sur VOS astuces, VOUS lecteurs de mon blog, basées sur VOS expériences. Cela permettrait d’enrichir cet article dans le but d’en faire une référence dans le monde francophone du C# ! (il faut savoir rêver un peu…). N’hésitez donc surtout pas à me contacter, je mettrais l’article à jour ;)

Utilisation des vars

Le mot-clé « var » existe en C# depuis la version 3 ! Il permet de laisser au compilateur le soin de déterminer le type d’une variable. Son utilisation est sujette à de nombreux débats (et moi-même, j’ai longtemps refusé de m’en servir…), mais je pense qu’en 2018, il y a plus d’avantages que d’inconvénients à s’en servir. En voici quelques-uns :

  • Induit un meilleur nommage des variables : en effet, le type n’étant pas visible directement, cela force le développeur à choisir un nom adapté et permettant de lever toutes ambiguïtés (sur son type ET sur son rôle).
  • Force l’initialisation des variables : on ne peut pas déclarer une « var » sans l’initialiser. Cela peut améliorer la robustesse de l’application.
  • Réduit le bruit au sein du code : les déclarations de variable sont plus courtes et donc le code est allégé et plus clair.
// C'est vraiment trop long à écrire...
MyBestClassWithWithRidiculouslyLongClassName myInstance1 =
  new MyBestClassWithWithRidiculouslyLongClassName();

// Plus pratique non ? :)
var myInstance2 = new MyBestClassWithWithRidiculouslyLongClassName();
  • **Augmente la rapidité d’écriture du code **😊

Attention, le mot-clé « var » ne conviendra pas forcément à l’ensemble des cas. Certains scénarios nécessitent encore le recours au typage explicite :

var userId = GetUserId(); // Guid ou int ?
var people = GetPeople(); // Est-ce une liste de string, d'entité People ou de ViewModel ?

Chaines de caractères

Pour comparer des chaînes de caractères, il est conseillé d’utiliser la méthode string.Compare(). C’est recommandé par Microsoft pour pallier certains problèmes de performances (avec StringComparison.Ordinal ou StringComparison.OrdinalIgnoreCase) et de localisation (dans ce cas StringComparison.CurrentCulture).

Pour tester si une chaine de caractères est vide, il faut utiliser la méthode string.IsNullOrEmpty().

Lors de concaténation de chaine « en masse » (à partir de 3/4 concaténations ou dans une boucle), il est nécessaire de passer par la classe StringBuilder pour éviter des problèmes de performance. L’opérateur « + » suffira pour les cas simples.

Constantes

Autant que faire se peut, il faut éviter l’utilisation de constante et privilégier un fichier de configuration. Néanmoins, il arrive que l’on soit obligé de s’en servir. Dans ce cas, il est mieux de les regrouper dans un fichier « Constants.cs ». Pour le cas des constantes numériques, on privilégiera autant que possible l’utilisation d’énumération.

Switch

Le traitement des autres cas (cas par défaut) d’une instruction de choix multiple est obligatoire. Il permet de se prémunir contre les oublis et de traiter le cas par défaut. Si aucun traitement n’est prévu, il faudra produire un message ou ajouter un commentaire approprié. Ce principe doit donc être appliqué même si, a priori, il n’y a pas de traitement particulier à réaliser dans le cas par défaut.

Les régions

Les balises #region/#endregion sont très pratiques pour améliorer la lisibilité du code. Elles permettent de regrouper les éléments d’une classe par domaine fonctionnel ou technique (constructeurs, propriétés, évènements de l’IHM…).

En revanche, elles ne doivent pas servir à regrouper des traitements au sein d’une même méthode. Lorsque l’on est tenté de le faire, il faut plutôt envisager de sortir ces traitements dans une méthode dédiée. Les régions ne se substituent pas à un bon découpage du code.

Quelques rares cas peuvent justifier leur utilisation à l’intérieur d’une méthode de classe comme les méthodes permettant de générer un contenu (une édition, un export, etc.).

Les titres des régions doivent être de nature à aider à naviguer dans le fichier. Inutile donc de les utiliser si cela y nuit.

Asynchronisme

Une méthode exécutée sur le thread principal bloque ce dernier. Celui-ci étant utilisé par l’OS pour gérer l’interface, une méthode longue va donc bloquer l’UI (ce qui va donner une impression de lenteur et empêchera à l’application d’être réactive et fluide). C’est le cas lors d’un accès à une ressource extérieure (web, réseau, GPS…). Il est bien évidemment possible de créer un thread séparé pour ce genre de cas, mais cela complexifie le code et il est toujours délicat de gérer correctement les cycles de vie de plusieurs threads. Du coup, il a été inventé le formidable combo async/await !

  • **Await **: Permet d’attendre la fin d’une tâche longue
  • Async: Indique qu’une méthode fait usage du mot-clé await

Attention :

  • Une méthode async ne peut retourner que « **void **» ou « Task/Task »
  • La méthode Main ne peut pas être async…
  • … et ne peut donc pas utiliser await
  • Par contre, elle peut utiliser directement la classe Task pour lancer des méthodes async si elle en a besoin

Il est préférable d’éviter les méthodes « async void », sauf dans le cas des évènements (pas le choix, on ne peut pas modifier le type de retour de ces méthodes).

Si une méthode est marquée comme async mais qu’elle ne contient pas le mot-clé await, elle sera exécutée de manière synchrone. Dans ce cas, il est bon de se poser la question de l’utilité du mot-clé async… 😊

Dans la pratique, il est de bon ton de nommer les méthodes pouvant faire l’objet d’un await par le suffixe async : MyMethodAsync(…).

Attention aussi à ne pas confondre asynchronisme et multithreading.

Gestion des erreurs

Dans un programme, les erreurs peuvent être gérées de deux façons :

  • En renvoyant un code d’erreur :
if (param == null)
{
    return false;
}
  • En levant une exception :
if (param == null)
{
    throw new ArgumentNullException("param");
}

Il est préférable de privilégier la première méthode, la levée d’exception étant réservée aux cas où :

  • L’erreur est peu courante
  • L’erreur est difficile à communiquer à l’appelant
  • L’erreur est suffisamment problématique pour nécessiter une attention et donc un traitement particulier

Les blocs de code « sensibles » doivent être « surveillés » via un bloc try/catch (suivi si besoin d’un finally).

Le bloc catch NE DOIT PAS être vide, sinon l’exception sera absorbée. Il existe tout de même certains cas où ce comportement est nécessaire, notamment dans le gestionnaire d’erreur (de plus haut niveau) ou encore lors d’une écriture de log.

Il permet de gérer les erreurs que l’on ne peut pas ou difficilement prévoir. Autant que possible, il faut placer ces blocs en dehors des boucles (pour éviter les problèmes de performances). Autant que possible, il faut essayer d’attraper les exceptions au plus près de l’endroit où elles sont censées se produire pour éviter d’alourdir la StackTrace (c’est couteux en terme de performance).

“Boolean Trap”

Le principe du « boolean trap » est simple : il s’agit de l’utilisation d’un paramètre de type booléen dans la signature d’une méthode. Éclaircissons tout cela avec un exemple :

var myEntity = new Entity();
myEntity.Initialize(source, additionnalData, true);

En relisant ce code, une question vient forcément à l’esprit : à quoi sert le « true » à la fin de la méthode ?

Pour les autres paramètres, pas de soucis, en se basant sur leur nom ou en relisant les quelques lignes précédentes, on comprend dans les grandes lignes à quoi ils servent. Par contre, pour ce « true » qui sort de nulle part et dont l’utilité n’est pas explicite, son rôle est nettement moins clair (en dehors des heures qui suivent l’écriture du code… si c’est bien toi qui l’a écrit !).

Dans mon exemple, il permet d’indiquer si c’est une entité interne ou externe. Il existe deux solutions pour rendre plus explicite sa présence lors de l’appel de la méthode :

  • Le nommer lors de l’appel :
var myEntity = new Entity();
myEntity.Initialize(source, additionnalData, estInterne: true);
  • Implémenter une énumération contenant les différentes valeurs possibles :
var myEntity = new Entity();
myEntity.Initialize(source, additionnalData, EntityType.Interne);

Cela permet dont de voir d’un seul coup d’œil son rôle et de faciliter la lecture du code sans être obligé de rentrer dans l’implémentation de la méthode.

Logs

Une application doit posséder un mécanisme de logs permettant de surveiller son bon fonctionnement et de faciliter la correction des bugs. Il existe des frameworks dédiés comme Log4net ou NLog et il est bien évidemment possible de développer son propre mécanisme.

Les erreurs bloquantes doivent impérativement être tracées.

Autant que possible, il faut éviter d’afficher à l’utilisateur des pages d’erreurs standards ou des messages d’erreur « bruts ».

Métriques

Autant que possible, il faut éviter les méthodes trop longues et pour cela :

  • On s’assurera que chaque méthode réalise bien une tâche unitaire
  • On factorisera les doublons de code
  • On limitera la complexité cyclomatique des méthodes (~25 pour une classe et ~10 pour une méthode) -> les méthodes pour lesquelles cet indicateur est levé devront être décomposées

Organisation du code

Pour ce point, je préfère rediriger vers cet article de blog qui donne de très bonnes pistes pour bien organisation sa solution Visual Studio.