Il confronto tra uguaglianza di numeri float induce in errore gli sviluppatori junior anche se nel mio caso non si verifica un errore di arrotondamento?

30

Ad esempio, voglio mostrare un elenco di pulsanti da 0,0.5, ... 5, che salta per ogni 0.5. Uso un ciclo for per farlo e ho colori diversi sul pulsante 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';
    }
}

In questo caso non ci dovrebbero essere errori di arrotondamento poiché ogni valore è esatto in IEEE 754.Ma sto faticando se dovessi cambiarlo per evitare il confronto dell'uguaglianza in virgola mobile:

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';
    }
}

Da un lato, il codice originale è più semplice e diretto a me. Ma c'è una cosa che sto considerando: i == STANDARD_LINE induce in errore i compagni di squadra junior? Nasconde il fatto che i numeri in virgola mobile possono avere errori di arrotondamento? Dopo aver letto i commenti di questo post:

link

sembra che ci siano molti sviluppatori che non sanno che alcuni numeri float sono esatti. Dovrei evitare confronti tra uguaglianza di numeri float anche se è valido nel mio caso? O sto pensando troppo a questo?

    
posta mmmaaa 31.01.2018 - 07:40
fonte

8 risposte

117

Eviterei sempre le successive operazioni in virgola mobile a meno che il modello che sto calcolando non le richieda. L'aritmetica a virgola mobile non è intuitiva per la maggior parte e una fonte importante di errori. E dire i casi in cui provoca errori da quelli in cui non è una distinzione ancora più sottile!

Pertanto, l'utilizzo di float come contatori di loop è un difetto in attesa di accadere e richiederebbe almeno un grosso commento in background che spieghi perché è corretto utilizzare 0.5 qui e che ciò dipende dallo specifico valore numerico. A quel punto, riscrivere il codice per evitare i contatori mobili sarà probabilmente l'opzione più leggibile. E la leggibilità è prossima alla correttezza nella gerarchia dei requisiti professionali.

    
risposta data 31.01.2018 - 08:04
fonte
39

Come regola generale, i loop dovrebbero essere scritti in modo tale da pensare di fare qualcosa n volte. Se stai usando indici in virgola mobile, non è più una questione di fare qualcosa n volte, ma piuttosto di essere eseguito fino a quando una condizione non viene soddisfatta. Se questa condizione sembra essere molto simile al i<n che molti programmatori si aspettano, allora il codice sembra fare una cosa quando ne sta facendo un altro che può essere facilmente interpretato erroneamente dai programmatori che sfiorano il codice.

È un po 'soggettivo, ma a mio modesto parere, se riesci a riscrivere un ciclo per utilizzare un indice intero per eseguire il ciclo di un numero fisso di volte, dovresti farlo. Quindi considera la seguente alternativa:

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';
    }
}

Il ciclo funziona in termini di numeri interi. In questo caso i è un numero intero e STANDARD_LINE è forzato a un intero. Ovviamente questo cambierebbe la posizione della tua linea standard se ci fosse un roundoff e allo stesso modo per MAX , quindi dovresti comunque cercare di impedire il roundoff per un rendering accurato. Tuttavia, hai ancora il vantaggio di modificare i parametri in termini di pixel e non di numeri interi senza doversi preoccupare del confronto dei punti mobili.

    
risposta data 31.01.2018 - 08:36
fonte
19

Sono d'accordo con tutte le altre risposte che l'uso di una variabile di ciclo non intero è generalmente di cattivo stile anche in casi come questo in cui funzionerà correttamente. Ma mi sembra che ci sia un altro motivo per cui è brutto stile qui.

Il tuo codice "sa" che le larghezze di linea disponibili sono precisamente i multipli di 0,5 da 0 fino a 5.0. Dovrebbe? Sembra che si tratti di una decisione dell'interfaccia utente che potrebbe facilmente cambiare (ad esempio, forse vuoi che gli spazi tra le larghezze disponibili diventino più grandi come fanno le larghezze. 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0 o qualcosa di simile).

Il tuo codice "sa" che le larghezze di linea disponibili hanno tutte rappresentazioni "belle" sia come numeri a virgola mobile sia come decimali. Sembra anche qualcosa che potrebbe cambiare. (Potresti volere 0.1, 0.2, 0.3, ... ad un certo punto.)

Il tuo codice "sa" che il testo da mettere sui pulsanti è semplicemente ciò che Javascript trasforma in quei valori a virgola mobile. Sembra anche qualcosa che potrebbe cambiare. Ad esempio, forse un giorno vorrai larghezze come 1/3, che probabilmente non vorresti mostrare come 0.33333333333333 o qualsiasi altra cosa, o forse vuoi vedere "1.0" invece di "1" per coerenza con "1.5" .)

Tutti mi sembrano manifestazioni di una singola debolezza, che è una sorta di mescolanza di strati. Questi numeri in virgola mobile fanno parte della logica interna del software. Il testo mostrato sui pulsanti è parte dell'interfaccia utente. Dovrebbero essere più separati di quanto non siano nel codice qui. Nozioni come "quale di queste è l'impostazione predefinita che dovrebbe essere evidenziata?" sono importanti per l'interfaccia utente e probabilmente non dovrebbero essere legati a quei valori in virgola mobile. E il tuo loop qui è davvero (o almeno dovrebbe essere) un ciclo su pulsanti , non su larghezze . Scritto in questo modo, la tentazione di utilizzare una variabile di ciclo che prende valori non interi scompare: si utilizzerebbero interi successivi o un per ... in / for ... di loop.

Il mio sentimento è che i più casi in cui si potrebbe essere tentati di eseguire il ciclo su numeri non interi sono come questo: ci sono altre ragioni, del tutto estranee ai problemi numerici, perché il codice dovrebbe essere organizzato diversamente . (Non tutti i casi tutti ; posso immaginare che alcuni algoritmi matematici possano essere espressi in modo più preciso in termini di un ciclo su valori non interi.)

    
risposta data 31.01.2018 - 17:19
fonte
8

Un odore di codice sta usando float in loop come quello.

Il loop può essere fatto in molti modi, ma nel 99,9% dei casi dovresti limitarti ad un incremento di 1 o ci sarà sicuramente confusione, non solo da parte degli sviluppatori junior.

    
risposta data 31.01.2018 - 09:04
fonte
3

Sì, lo vuoi evitare.

I numeri in virgola mobile sono una delle più grandi trappole per il programmatore ignaro (il che significa, nella mia esperienza, quasi tutti). Dipendere dai test di uguaglianza in virgola mobile, rappresentare il denaro come punto mobile, è tutto un grande pantano. Aggiungere un flottante all'altro è uno dei più grandi delinquenti. Ci sono interi volumi di letteratura scientifica su cose come questa.

Usa i numeri in virgola mobile esattamente nei punti in cui sono appropriati, ad esempio quando esegui calcoli matematici reali dove ne hai bisogno (come trigonometria, grafici di funzioni di tracciatura, ecc.) e stai molto attento quando fai operazioni seriali. L'uguaglianza è giusta. La conoscenza di quale particolare gruppo di numeri è esatto dagli standard IEEE è molto arcana e non ci farei mai affidamento.

Nel tuo caso, sarà , da Murphys Law, arriva il punto in cui la gestione vuole che tu non abbia 0.0, 0.5, 1.0 ... ma 0.0, 0.4, 0.8 ... o qualsiasi altra cosa ; sarai immediatamente borked e il tuo programmatore junior (o te stesso) eseguirà il debugging a lungo e duramente finché non troverai il problema.

Nel tuo particolare codice, avrei davvero una variabile del ciclo intero. Rappresenta il pulsante i th, non il numero corrente.

E probabilmente, per maggiore chiarezza, non scriverò i/2 ma i*0.5 che rende abbondantemente chiaro cosa sta succedendo.

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';
    }
}

Nota: come indicato nei commenti, JavaScript non ha in realtà un tipo separato per i numeri interi. Ma gli interi fino a 15 cifre sono garantiti per essere precisi / sicuri (vedi link ), quindi per argomenti come questo ("è più confuso / incline agli errori di lavorare con numeri interi o non interi") questo è appropriatamente vicino ad avere un tipo separato "nello spirito"; nell'uso quotidiano (loop, coordinate dello schermo, indici di array ecc.) non ci saranno sorprese con i numeri interi rappresentati come Number come JavaScript.

    
risposta data 31.01.2018 - 19:16
fonte
1

Non penso che i tuoi suggerimenti siano buoni. Invece, vorrei introdurre una variabile per il numero di pulsanti in base al valore massimo e alla spaziatura. Quindi, è abbastanza semplice scorrere gli indici del pulsante stesso.

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;
    }
}

Potrebbe essere più codice, ma è anche più leggibile e più robusto.

    
risposta data 31.01.2018 - 22:14
fonte
0

Puoi evitare il tutto calcolando il valore che stai visualizzando piuttosto che usare il contatore del ciclo come valore:

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';
    }
}
    
risposta data 01.02.2018 - 11:42
fonte
-1

L'aritmetica in virgola mobile è lenta e l'aritmetica integer è veloce, quindi quando uso il punto mobile, non la userei inutilmente dove si possono usare gli interi. È utile pensare sempre a numeri in virgola mobile, anche costanti, come approssimativi, con qualche piccolo errore. È molto utile durante il debug per sostituire i numeri in virgola mobile nativi con più / meno oggetti in virgola mobile in cui si considera ciascun numero come intervallo anziché come punto. In questo modo scopri le inesattezze crescenti progressive dopo ogni operazione aritmetica. Quindi "1.5" dovrebbe essere pensato come "un numero compreso tra 1,45 e 1,55" e "1,50" dovrebbe essere considerato come "un numero compreso tra 1.495 e 1.505".

    
risposta data 31.01.2018 - 17:45
fonte

Leggi altre domande sui tag