Musée des horreurs – Thématiques de base

Quelques raccourcis :

Il est possible d'écrire des horreurs même en s'en tenant aux bases de la programmation...

Entrée 00 – Le traitement compliqué d'un booléen

La situation : supposons qu'on veuille écrire une fonction est_pair() qui prenne en paramètre un entier et qui retourne la valeur booléenne vrai (true) si cet entier est pair, et faux (false) s'il est impair.

L'horreur : les cas considérés ici ne sont pas des horreurs au sens strict du terme. Il s'agit dans chaque cas de solutions opérationnelles mais déficientes en forme ou en style (qui ne sont pas non plus des canons d'élégance). Voyons une première version :

Horreur 00.0
bool est_pair(int nb) {
   if (nb % 2 == 0) { // si nb est pair
      return true;
   } else {
      return false;
   }
}

Cette fonction est opérationnelle. Elle répond aux exigences de l'énoncé du problème, mais ne répond pas aux critères de bonne programmation en vigueur au département. Il s'y trouve beaucoup de redondance, à laquelle nous reviendrons sous peu, mais elle offre surtout deux points de sortie, ce qui rend impossible la tâche d'en réaliser un morphogramme.

Ne pas pouvoir présenter un morphogramme pour un algorithme de base empêche de l'enboîter structurellement dans un schéma plus complexe et complique une éventuelle démonstration de bon fonctionnement pour un programme se servant de cette fonction.

Voyons maintenant une deuxième version, qui corrige cette faute structurelle et utilise une variable temporaire pour éliminer la dualité des points de sortie :

Horreur 00.1
bool est_pair(int nb) {
   bool resultat;
   if (nb % 2 == 0) { // si nb est pair
      resultat = true;
   } else {
      resultat = false;
   }
   return resultat;
}

Cette version est structurellement correcte, mais est lente et complexe pour le problème qu'elle a à résoudre. En effet, si on la lit en langage naturel, on a :

Ce qui est équivalent, soit dit en passant, à :

La forme alternative ici entraîne une redondance qui devient peut-être visible à l'oeil étant donné la mise en relief qui précède. Nous exploiterons l'information dans cette deuxième forme pour déduire une solution optimale au problème, un peu plus loin.

Pour tenter de simplifier cette solution, on serait tenté d'exploiter une version de la fonction qui initialise la variable resultat avec une valeur par défaut, comme par exemple :

Horreur 00.2
bool est_pair(int nb) {
   bool resultat = false;
   if (nb % 2 == 0) { // si nb est pair
      resultat = true;
   }
   return resultat;
}

Cette version est plus compacte que la précédente, mais elle est aussi au moins aussi lente, étant même plus lente dans le cas où nb est effectivement pair. La raison :

La forme plus compacte dans ce cas-ci n'est pas celle qui fait le moins de choses.

Le bon usage : reconnaître que le résultat de l'évaluation de la condition est un booléen, et qu'on peut l'utiliser de la même manière qu'on utiliserait un littéral booléen. Ainsi, constatant que si la condition est vraie, alors on veut retourner vrai, et que si la condition est fausse, alors on veut retourner faux, on réalisera que dans le fond, notre but est de retourner la valeur résultant de l'évaluation de la condition :

Entrée 00 – Usage en bonne et due forme
bool est_pair(int nb) {
   return nb % 2 == 0;
}

Selon l'ordre de priorité des opérateurs, la calcul du reste de la division de nb par 2 se fera d'abord, résultant en un entier compris inclusivement entre 0 et 1, et la comparaison entre cet entier et le littéral entier 0 se fera ensuite. Le résultat de la comparaison de deux entiers avec l'opérateur == sera booléen, et aura la valeur attendue (vrai si nb est pair, faux sinon).

À tirte de complément, notez que l'écriture correcte de est_impair() serait :

bool est_impair(int nb) {
   return !est_pair(nb);
}

En exprimant est_impair() à partir de est_pair(), on limite les points de mise à jour de notre programme et on assure la cohérence mutuelle de ces deux opérations.

En 2018, mon illustre collègue Vincent Echelard a soumis l'extrait de code C# qui suit à mon attention :

// ...
if((Input.GetAxis("LT") == 0 ? false : true)
// ...

... ce qui, dans le même ordre d'idées, se simplifie pour donner :

// ...
if(Input.GetAxis("LT") != 0)
// ...

... qui est tellement plus simple et plus direct. Pourquoi se compliquer la vie?

Entrée 01 – Les opérateurs = et ==

La situation : on veut que la variable bOk reçoive la valeur true si les variables bDonneesRecues, bFinReception et bOk ont aussi la valeur true.

Avant d'aller plus loin, relevons qu'il y a une faute de logique de base dans l'énoncé ci-dessus, sur laquelle nous reviendrons un peu plus bas.

L'horreur : mêler les opérateurs == et =, ce qui est commun quand on est distrait (ou quand on navigue entre plusieurs langages, ce qui se produit quand on passe par exemple de VB à C++ ou de Pascal à C#) :

Horreur 01.0
if ( bDonneesRecues = true &&
     bFinReception = true &&
     bOk = true) {
   bOk = true;
}

Le bon usage (volet syntaxique): utiliser == pour les comparaisons logiques (plutôt que = qui est l'opérateur d'affectation de C, C++, C# et Java). Cela nous donne :

Entrée 01.0a – Usage en bonne et due forme (correctif syntaxique)
if ( bDonneesRecues == true &&
     bFinReception == true &&
     bOk == true) {
   bOk = true;
}

Le bon usage (première simplification logique) : quand on veut vérifier si une variable booléenne est vraie, pas besoin de la comparer avec la valeur vraie (car b==true vaudra true si b est true et false si b est false... aussi bien examiner la valeur de b directement!).

Entrée 01.0b – Usage en bonne et due forme (simplification logique)
if (bDonneesRecues && bFinReception && bOk) {
   bOk = true;
}

Le bon usage (deuxième simplification logique) : on vérifie ci-dessus une expression logique, puis on donne la valeur true à bOk si et seulement la condition est vraie. Si la condition est fausse, alors bOk gardera sa valeur antérieure (true si bOk valait true, et false si bOk valait false).

On voit aussi que la condition ne peut être vraie que si au moins bOk est vrai (car si bOk est faux, alors la condition est nécessairement fause). On notera que si bOk était faux avant l'alternative, alors bOk restera faux après l'alternative aussi (le code devant être exécuté si la condition s'avère vrai n'étant jamais atteint).

Il est éclairant d'examiner une nouvelle écriture du même énoncé, faite cette fois à l'aide d'alternatives imbriquées.

Réécriture d'Entrée 01.0b par alternatives imbriquées
if (bOk) {
   // ici, bOk est déjà true!
   if ( bDonneesRecues && bFinReception ) {
      bOk = true;
   }
} else { // ici, bOk est déjà false!
   bOk = false;
}

Constat : si bOk valait vrai avant la condition, alors il vaudra encore vrai après la condition. De même, si bOk valait faux avant la condition, alors il vaudra encore faux après la condition. Ce bloc d'instructions est donc absolument inutile, sinon pour consommer – et gaspiller – des cycles d'exécution du processeur. Le bon usage serait de l'éliminer complètement du programme!

Remarque : quand on en arrive à un bloc d'instructions sans la moindre utilité (surtout si, à l'origine, ce bloc entraînait des erreurs à l'exécution!) il vaut la peine de s'arrêter et de se demander comment on en est arrivé(e) là.

Un truc : commenter systématiquement son code. Si on avait commenté de manière utile ce bloc (donc pas en indiquant « si bDonneesRecues vaut true et si bFinReception vaut true et si bOk vaut true, alors j'affecte la valeur true à bOk », ce qui ne nous dit rien sur ce que fait, vraiment, ce bloc de code, ce à quoi il sert dans le programme), alors on aurait eu de la difficulté à dire ce à quoi ce bloc de code sert. Cela nous aurait donné un fort indice quant au problème de fond ici: étant incapables de dire ce que fait le code, il aurait alors fallu se pencher sur sa raison d'être...


Valid XHTML 1.0 Transitional

CSS Valide !