web-dev-qa-db-fra.com

La comparaison de l'égalité des nombres flottants trompe-t-elle les développeurs juniors même si aucune erreur d'arrondi ne se produit dans mon cas?

Par exemple, je veux afficher une liste de boutons de 0,0,5, ... 5, qui saute pour chaque 0,5. J'utilise une boucle for pour cela, et j'ai une couleur différente au bouton STANDARD_LINE:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0;i<=MAX;i=i+DIFF){
    button.text=i+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

Dans ce cas, il ne devrait pas y avoir d'erreurs d'arrondi car chaque valeur est exacte dans IEEE 754. Mais je me bats si je dois la changer pour éviter la comparaison d'égalité en virgule flottante:

var MAX=10;
var STANDARD_LINE=3;

for(var i=0;i<=MAX;i++){
    button.text=i/2.0+'';
    if(i==STANDARD_LINE/2.0){
      button.color='red';
    }
}

D'une part, le code d'origine est plus simple et me parvient. Mais il y a une chose que j'envisage: est-ce que i == STANDARD_LINE induit en erreur les coéquipiers juniors? Couvre-t-il le fait que les nombres à virgule flottante peuvent avoir des erreurs d'arrondi? Après avoir lu les commentaires de ce post:

https://stackoverflow.com/questions/33646148/is-hardcode-float-precise-if-it-can-be-represented-by-binary-format-in-ieee-754

il semble que de nombreux développeurs ne savent pas que certains nombres flottants sont exacts. Dois-je éviter les comparaisons d'égalité de nombres flottants même si elles sont valables dans mon cas? Ou est-ce que j'y pense trop?

31
ocomfd

J'éviterais toujours les opérations successives en virgule flottante à moins que le modèle que je calcule ne les nécessite. L'arithmétique en virgule flottante n'est pas intuitive pour la plupart et constitue une source majeure d'erreurs. Et distinguer les cas dans lesquels il provoque des erreurs de ceux où il ne l'est pas est une distinction encore plus subtile!

Par conséquent, l'utilisation de flotteurs comme compteurs de boucles est un défaut qui attend de se produire et nécessiterait au moins un commentaire de fond gras expliquant pourquoi il est correct d'utiliser 0,5 ici, et que cela dépend de la valeur numérique spécifique. À ce stade, la réécriture du code pour éviter les compteurs flottants sera probablement l'option la plus lisible. Et la lisibilité est à côté de l'exactitude dans la hiérarchie des exigences professionnelles.

116
Kilian Foth

En règle générale, les boucles doivent être écrites de manière à penser à faire quelque chose n fois. Si vous utilisez des indices à virgule flottante, il ne s'agit plus de faire quelque chose n fois mais plutôt de courir jusqu'à ce qu'une condition soit remplie. Si cette condition est très similaire à la i<n que tant de programmeurs attendent, alors le code semble faire une chose alors qu'il en fait une autre qui peut être facilement mal interprétée par les programmeurs en écrémant le code.

C'est quelque peu subjectif, mais à mon humble avis, si vous pouvez réécrire une boucle pour utiliser un index entier pour boucler un nombre fixe de fois, vous devriez le faire. Considérez donc l'alternative suivante:

var DIFF=0.5;                           // pixel increment
var MAX=Math.floor(5.0/DIFF);           // 5.0 is max pixel width
var STANDARD_LINE=Math.floor(1.5/DIFF); // 1.5 is pixel width

for(var i=0;i<=MAX;i++){
    button.text=(i*DIFF)+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

La boucle fonctionne en termes de nombres entiers. Dans ce cas, i est un entier et STANDARD_LINE est également contraint à un entier. Bien sûr, cela changerait la position de votre ligne standard s'il y avait un arrondi et de même pour MAX, vous devriez donc vous efforcer d'empêcher l'arrondi pour un rendu précis. Cependant, vous avez toujours l'avantage de changer les paramètres en termes de pixels et non de nombres entiers sans avoir à vous soucier de la comparaison des virgules flottantes.

40
Neil

Je suis d'accord avec toutes les autres réponses selon lesquelles l'utilisation d'une variable de boucle non entière est généralement un mauvais style même dans des cas comme celui-ci où cela fonctionnera correctement. Mais il me semble qu'il y a une autre raison pour laquelle c'est un mauvais style ici.

Votre code "sait" que les largeurs de ligne disponibles sont précisément les multiples de 0,5 de 0 à 5,0. Devrait-il? Il semble que ce soit une décision d'interface utilisateur qui pourrait facilement changer (par exemple, vous voulez peut-être que les écarts entre les largeurs disponibles deviennent plus grands comme le font les largeurs. 0,25, 0,5, 0,75, 1,0, 1,5, 2,0, 2,5, 3,0, 4,0, 5,0 ou quelque chose).

Votre code "sait" que les largeurs de ligne disponibles ont toutes des représentations "Nice" à la fois en nombres à virgule flottante et en décimales. Cela semble également quelque chose qui pourrait changer. (Vous voudrez peut-être 0,1, 0,2, 0,3, ... à un moment donné.)

Votre code "sait" que le texte à mettre sur les boutons est simplement ce en quoi Javascript transforme ces valeurs à virgule flottante. Cela semble également quelque chose qui pourrait changer. (Par exemple, peut-être qu'un jour vous voudrez des largeurs comme 1/3, que vous ne voudriez probablement pas afficher comme 0.33333333333333 ou autre. Ou peut-être que vous voulez voir "1.0" au lieu de "1" pour la cohérence avec "1.5" .)

Tout cela me semble être les manifestations d'une seule faiblesse, qui est une sorte de mélange de couches. Ces nombres à virgule flottante font partie de la logique interne du logiciel. Le texte affiché sur les boutons fait partie de l'interface utilisateur. Ils devraient être plus séparés que dans le code ici. Des notions telles que "laquelle est la valeur par défaut à mettre en évidence?" sont des questions d'interface utilisateur, et elles ne devraient probablement pas être liées à ces valeurs à virgule flottante. Et votre boucle ici est vraiment (ou du moins devrait être) une boucle sur boutons, pas sur largeurs de ligne. Écrit de cette façon, la tentation d'utiliser une variable de boucle prenant des valeurs non entières disparaît: vous utiliseriez simplement des entiers successifs ou une boucle for ... in/for ... of.

Mon sentiment est que la plupart cas où l'on pourrait être tenté de boucler sur des nombres non entiers sont comme ceci: il y a d'autres raisons, sans aucun rapport avec les problèmes numériques, pour lesquelles le code devrait être organisé différemment. (Pas tous cas; je peux imaginer que certains algorithmes mathématiques pourraient être exprimés de la manière la plus nette en termes de boucle sur des valeurs non entières.)

20
Gareth McCaughan

Une odeur de code utilise des flotteurs en boucle comme ça.

Le bouclage peut se faire de nombreuses façons, mais dans 99,9% des cas, vous devriez vous en tenir à un incrément de 1 ou il y aura certainement de la confusion, non seulement par les développeurs juniors.

8
Pieter B

Oui, vous voulez éviter cela.

Les nombres à virgule flottante sont l'un des plus gros pièges pour le programmeur sans méfiance (ce qui signifie, selon mon expérience, presque tout le monde). De dépendre des tests d'égalité en virgule flottante à représenter l'argent en virgule flottante, tout est un gros bourbier. Additionner un flotteur sur l'autre est l'un des plus grands contrevenants. Il y a des volumes entiers de littérature scientifique sur des choses comme ça.

Utilisez des nombres à virgule flottante exactement aux endroits où ils sont appropriés, par exemple lorsque vous effectuez des calculs mathématiques réels là où vous en avez besoin (comme la trigonométrie, les graphiques de fonction de traçage, etc.) et soyez très prudent lorsque vous effectuez des opérations en série. L'égalité est au rendez-vous. La connaissance de quel ensemble particulier de nombres est exact selon les normes IEEE est très mystérieuse et je n'en dépendrais jamais.

Dans votre cas, il y aura , par Murphys Law, le moment où la direction voudra que vous n'ayez pas 0,0, 0,5, 1,0 ... mais 0,0, 0,4, 0,8 ... ou autre; vous vous serez immédiatement borked, et votre programmeur junior (ou vous-même) déboguera longtemps et dur jusqu'à ce que vous trouviez le problème.

Dans votre code particulier, j'aurais en effet une variable de boucle entière. Il représente le bouton ith, pas le numéro courant.

Et je voudrais probablement, pour plus de clarté, ne pas écrire i/2 mais i*0.5 ce qui montre clairement ce qui se passe.

var BUTTONS=11;
var STANDARD_LINE=3;

for(var i=0; i<BUTTONS; i++) {
    button.text = (i*0.5)+'';
    if (i==STANDARD_LINE) {
      button.color='red';
    }
}

Remarque: comme indiqué dans les commentaires, JavaScript n'a pas de type distinct pour les entiers. Mais les entiers jusqu'à 15 chiffres sont garantis pour être précis/sûr (voir https://www.ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer ), donc pour des arguments comme celui-ci ("est-il plus déroutant/sujet aux erreurs de travailler avec des entiers ou des non-entiers"), il est tout à fait approprié d'avoir un type séparé "dans l'esprit"; dans l'utilisation quotidienne (boucles, coordonnées d'écran, indices de tableau, etc.), il n'y aura pas de surprise avec des nombres entiers représentés comme Number comme JavaScript.

3
AnoE

Je ne pense pas que vos suggestions soient bonnes. Au lieu de cela, j'introduirais une variable pour le nombre de boutons en fonction de la valeur maximale et de l'espacement. Ensuite, il est assez simple de parcourir les index du bouton eux-mêmes.

function precisionRound(number, precision) {
  let factor = Math.pow(10, precision);
  return Math.round(number * factor) / factor;
}

var maxButtonValue = 5.0;
var buttonSpacing = 0.5;

let countEstimate = precisionRound(maxButtonValue / buttonSpacing, 5);
var buttonCount = Math.floor(countEstimate) + 1;

var highlightPosition = 3;
var highlightColor = 'red';

for (let i=0; i < buttonCount; i++) {
    let buttonValue = i / buttonSpacing;
    button.text = buttonValue.toString();
    if (i == highlightPosition) {
        button.color = highlightColor;
    }
}

Il peut s'agir de plus de code, mais il est également plus lisible et plus robuste.

1
Jared Goguen

Vous pouvez éviter tout cela en calculant la valeur que vous affichez plutôt qu'en utilisant le compteur de boucle comme valeur:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0; (i*DIFF) < MAX ; i=i+1){
    var val = i * DIFF

    button.text=val+'';

    if(val==STANDARD_LINE){
      button.color='red';
    }
}
0
Arnab Datta