Pièges en C# – Episode 2

Parce qu’un article ne peut être nommé « Episode 1 » que s’il y a un épisode 2, voici le successeur de son prédécesseur. Comme tout épisode 2 qui se respecte, on reprend la recette du 1, mais avec des pièges encore plus inattendus et encore plus mortels.

Double (ou float) et arrondi

Le problème

Bien évidemment, 1.000001 - 0.000001, ça fait 1. Pourtant, result est false

La raison

En C#, la valeur d’un float ou un double se calcule ainsi :

Le calcul se fait donc en base 2. Les valeurs décimale de d1 et d2 n’ont pas de valeurs binaires exactes. Ces deux arrondis aboutissent à ce qui semble être une erreur de calcul.

Cet arrondi en valeurs binaires est similaire à celui de 1/3 en valeur décimale.

La solution

Il est possible d’accepter une marge d’erreur :

Il est également possible d’utiliser decimal à la place de double. Ce type se base sur des valeurs décimales. On y stocke donc de manière exacte toute valeur décimale. En revanche, un decimal est deux fois plus gros en mémoire, et les calculs sont plus lents. Vraiment plus lents. Genre entre 20 et 100 fois plus lents (selon les opérations).

Si on veut faire des calculs monétaires, on ne peut pas se permettre un arrondi. On utilise donc forcément du decimal. Sinon, double convient tout-à-fait dans la plupart des cas.

Surcharge² – override et overload

Le problème

Child.MyMethod(int semble évidemment être le meilleur candidat. Pourtant, c’est Child.MyMethod(object) qui est appelé.

La raison

La surcharge (override) d’une méthode remplace le comportement de la méthode du parent, mais ne déclare pas une méthode dans la classe enfant. La classe Child elle-même ne déclare qu’une seule méthode : MyMethod(object). Pour trouver le meilleur candidat lors d’un appel, la surcharge (overload) va commencer par chercher les méthodes déclarées sur la classe. Si aucune ne correspond, il recherche sur la classe parente, et ainsi de suite jusqu’à rechercher sur System.Object.

La solution

Le plus simple est de ne pas mixer les deux (override et overload). Pour cela, soit on nomme différemment Child.MyMethod(object), soit on remplace override sur Child.MyMethod(int) par new (attention, cela masque la méthode du parent, mais ne modifie pas son comportement).

L’autre solution est d’appeler MyMethod(integerValue) sur une référence de type Parent. Ainsi, la seule méthode disponible est Parent.MyMethod(int), surchargée (donc dont le comportement est remplacé) par Child.MyMethod(int).

Variables externes dans une lambda

Le problème

Et là, plutôt que d’obtenir toutes les valeurs de 0 à 9, j’obtiens 10 fois la valeur 10.

La raison

Dans une lambda (ou de manière plus générale, une méthode anonyme), lorsqu’on utilise une variable externe, c’est la variable elle-même qui est capturée, et non sa valeur. Lors de l’exécution des actions, la variable i a la valeur 10 puisqu’il s’agit de la condition de sortie de la boucle et qu’on se trouve après celle-ci.

« Mais si la lambda capture la variable et non la valeur, puisque la variable i est déclarée dans le bloc for, elle ne devrait donc plus exister après ! »

Très bonne remarque ! On peut remercier le compilateur C# qui est très intelligent et remarque que la variable i est utilisée dans une lambda. Et là, il va bosser ! Il va créer une classe cachée contenant une méthode correspondant à la lambda, va créer dans cette classe un champ public de type int pour sauvegarder i, va remplacer la déclaration de i par la création d’un objet de la classe en question et enfin va remplacer chaque occurrence de i dans le code par un accès au champ de l’objet qu’il vient de créer. i ayant été remplacé par le champ d’une classe, elle se trouve dans la heap et sa désallocation sera faite normalement par le garbage collector quand plus aucune référence à l’objet en question n’existera.

La solution

Il suffit de ne pas utiliser la même variable dans chaque lambda, mais d’utiliser une variable différente, initialisée à la valeur courante de i à l’intérieur de la boucle. Pour cela, un simple int val = i; entre les lignes 4 et 5, et un remplacement de i par val dans le Console.WriteLine(i); et le tour est joué. A chaque itération, val correspond à une nouvelle allocation, donc chaque action a sa val.

++ et threads

Le problème

Et là, j’affiche bien un mil… ah non, 453622… Entre les double et les incrémentations, on dirait que le C# ne sait pas compter.

La raison

L’incrémentation, même si c’est un simple opérateur, englobe 3 actions :

  • On récupère la valeur de la variable
  • On ajoute 1
  • On sauvegarde la nouvelle valeur

Celles-ci ne sont pas atomiques, c’est-à-dire que plusieurs threads concurrents peuvent entremêler ces actions. Prenons un cas extrême : le calcul est réparti sur un million de threads. Chacun demande la valeur de la variable : 0. Ensuite, chacun ajoute 1 : 0+1 = 1. Enfin, chacun sauvegarde sa nouvelle valeur : 1.

La solution

La solution la plus générique consiste à rendre l’opération atomique via un lock :

Cependant, pour certaines opérations simples (incrémentation, décrémentation, somme…), il existe la classe statique Interlocked permettant d’effectuer ces opérations atomiquement bien plus rapidement qu’avec un lock :

Appels synchrones sur une méthode asynchrone

Le problème

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.