Valutazione del corto circuito, è una cattiva pratica?


88

Qualcosa che conosco da un po 'ma che non ho mai considerato è che nella maggior parte delle lingue è possibile dare priorità agli operatori in un'istruzione if basata sul loro ordine. Lo uso spesso come modo per prevenire eccezioni di riferimento null, ad esempio:

if (smartphone != null && smartphone.GetSignal() > 50)
{
   // Do stuff
}

In questo caso il codice comporterà dapprima la verifica che l'oggetto non sia nullo e quindi l'utilizzo di questo oggetto sapendo che esiste. Il linguaggio è intelligente perché sa che se la prima istruzione è falsa, non ha senso valutare la seconda istruzione e quindi non viene mai generata un'eccezione di riferimento null. Questo funziona allo stesso modo per ande oroperatori.

Questo può essere utile anche in altre situazioni come la verifica che un indice sia nei limiti di un array e una tale tecnica può essere eseguita in vari linguaggi, per quanto ne so: Java, C #, C ++, Python e Matlab.

La mia domanda è: questo tipo di codice è una rappresentazione di cattiva pratica? Questa cattiva pratica deriva da alcuni problemi tecnici nascosti (ad esempio, ciò potrebbe eventualmente causare un errore) o porta a un problema di leggibilità per altri programmatori? Potrebbe essere fonte di confusione?


69
Il termine tecnico è valutazione del corto circuito . Questa è una tecnica di implementazione ben nota e dove lo standard linguistico lo garantisce, fare affidamento su di essa è una buona cosa. (In caso contrario, non è molto di una lingua, IMO.)
Kilian Foth,

3
La maggior parte delle lingue ti consente di scegliere quale comportamento desideri. In C # l'operatore ||cortocircuita, l'operatore |(singolo tubo) non funziona ( &&e si &comporta allo stesso modo). Poiché gli operatori di cortocircuito vengono utilizzati molto più frequentemente, consiglio di contrassegnare gli utilizzi di quelli non di corto circuito con un commento per spiegare perché è necessario il loro comportamento (principalmente cose come le invocazioni di metodi che devono accadere tutti).
Linac,

14
@linac ora mettere qualcosa sul lato destro &o |per assicurarsi che si verifichi un effetto collaterale è qualcosa che direi che è sicuramente una cattiva pratica: non ti costerà nulla mettere quel metodo di chiamata sulla propria linea.
Jon Hanna,

14
@linac: in C e C ++, &&è in corto circuito e &non lo è, ma sono operatori molto diversi. &&è un "e" logico; &è un bit "e". È possibile x && yessere vero mentre x & yè falso.
Keith Thompson,

3
Il tuo esempio specifico può essere una cattiva pratica per un altro motivo: cosa succede se è nullo? È ragionevole che sia nullo qui? Perché è nullo? Il resto della funzione al di fuori di questo blocco dovrebbe essere eseguito se è nullo, il tutto non dovrebbe fare nulla o il tuo programma dovrebbe fermarsi? Annotare un "if null" su ogni accesso (forse a causa di un'eccezione di riferimento null di cui hai avuto fretta di sbarazzarti) significa che puoi andare piuttosto lontano nel resto del programma, senza mai notare che l'inizializzazione dell'oggetto del telefono è stata avvitata su. E alla fine, quando finalmente
arriverai

Risposte:


125

No, questa non è una cattiva pratica. Affidarsi al corto circuito dei condizionali è una tecnica ampiamente accettata e utile, purché si utilizzi un linguaggio che garantisca questo comportamento (che include la stragrande maggioranza dei linguaggi moderni).

Il tuo esempio di codice è abbastanza chiaro e, in effetti, è spesso il modo migliore per scriverlo. Le alternative (come le ifaffermazioni nidificate ) sarebbero più disordinate, più complicate e quindi più difficili da comprendere.

Un paio di avvertenze:

Usa il buon senso per quanto riguarda complessità e leggibilità

Quanto segue non è una buona idea:

if ((text_string != null && replace(text_string,"$")) || (text_string = get_new_string()) != null)

Come molti modi "intelligenti" per ridurre le righe del codice, l'eccessiva dipendenza da questa tecnica può risultare in un codice difficile da comprendere e soggetto a errori.

Se devi gestire gli errori, c'è spesso un modo migliore

Il tuo esempio va bene fintanto che è uno stato normale del programma per smartphone essere nullo. Se, tuttavia, si tratta di un errore, è necessario includere una gestione degli errori più intelligente.

Evitare ripetuti controlli delle stesse condizioni

Come sottolinea Neil, molti controlli ripetuti della stessa variabile in diversi condizionali sono probabilmente prove di un difetto di progettazione. Se nel tuo codice sono presenti dieci istruzioni diverse che non devono essere eseguite se lo smartphoneè null, considera se esiste un modo migliore per gestirlo che controllare la variabile ogni volta.

Non si tratta in particolare del corto circuito; è un problema più generale di codice ripetitivo. Tuttavia, vale la pena menzionare, perché è abbastanza comune vedere il codice che ha molte istruzioni ripetute come la tua istruzione di esempio.


12
Va notato che i blocchi ripetuti se controllano se un'istanza è nulla in una valutazione di cortocircuito dovrebbero probabilmente essere riscritti per eseguire quel controllo solo una volta.
Neil,

1
@Neil, buon punto; Ho aggiornato la risposta.

2
L'unica cosa che posso pensare di aggiungere è annotare un altro modo opzionale di gestirlo, che è davvero preferito se stai per verificare qualcosa in più condizioni diverse: controlla per vedere se qualcosa è nullo in un if (), quindi ritorno / lancio - altrimenti continua ad andare avanti. Questo elimina l'annidamento e spesso può rendere il codice più esplicito e conciso, come "questo non dovrebbe essere nullo e se lo è allora sono fuori di qui" - inserito nel codice il più prudentemente possibile. Ottimo ogni volta che si codifica qualcosa che controlla una condizione specifica più che in un punto in un metodo.
BrianH,

1
@ dan1111 Vorrei generalizzare l'esempio che hai dato come "Una condizione non dovrebbe contenere un effetto collaterale (come l'assegnazione delle variabili sopra). La valutazione del corto circuito non cambia questo e non dovrebbe essere usata come scorciatoia per un lato- effetto che avrebbe potuto essere facilmente inserito nel corpo del if per rappresentare meglio la relazione if / then. "
AaronLS,

@AaronLS, sono fortemente in disaccordo con l'affermazione secondo cui "una condizione non dovrebbe contenere un effetto collaterale". Una condizione non dovrebbe essere inutilmente confusa, ma fintanto che è un codice chiaro, sto bene con un effetto collaterale (o anche più di un effetto collaterale) in una condizione.

26

Supponiamo che tu stia utilizzando un linguaggio in stile C senza &&e sia necessario eseguire il codice equivalente come nella tua domanda.

Il tuo codice sarebbe:

if(smartphone != null)
{
  if(smartphone.GetSignal() > 50)
  {
    // Do stuff
  }
}

Questo schema si presenterebbe molto.

Ora immagina la versione 2.0 del nostro linguaggio ipotetico introduce &&. Pensa a quanto pensi che sia bello!

&&è il modo riconosciuto e idiomatico di fare ciò che fa il mio esempio sopra. Non è una cattiva pratica usarlo, anzi è una cattiva pratica non usarlo in casi come sopra: un programmatore esperto in un linguaggio del genere si domanderebbe dove elsefosse o altro motivo per non fare le cose nel modo normale.

Il linguaggio è intelligente perché sa che se la prima istruzione è falsa, non ha senso valutare la seconda istruzione e quindi non viene mai generata un'eccezione di riferimento null.

No, sei intelligente, perché sapevi che se la prima affermazione era falsa, non ha senso nemmeno valutare la seconda affermazione. La lingua è stupida come una scatola di pietre e ha fatto quello che gli hai detto di fare. (John McCarthy era ancora più intelligente e si rese conto che la valutazione del corto circuito sarebbe stata utile per le lingue).

La differenza tra l'essere intelligente e il linguaggio intelligente è importante, perché le buone e le cattive pratiche spesso dipendono dal fatto che sei intelligente quanto necessario, ma non più intelligente.

Tener conto di:

if(smartphone != null && smartphone.GetSignal() > ((range = (range != null ? range : GetRange()) != null && range.Valid ? range.MinSignal : 50)

Questo estende il codice per verificare a range. Se il valore rangeè null, prova a impostarlo tramite una chiamata, GetRange()anche se ciò potrebbe non riuscire, quindi rangepotrebbe essere nullo. Se intervallo non è nullo dopo questo, ed è Validquindi MinSignalviene utilizzata la sua proprietà, altrimenti viene utilizzato un valore predefinito di 50.

Anche questo dipende &&, ma metterlo in una riga è probabilmente troppo intelligente (non sono sicuro al 100% di averlo bene, e non ricontrollerò perché questo fatto dimostra il mio punto).

Non &&è questo il problema qui, ma la capacità di usarlo per mettere molto in una espressione (una cosa buona) aumenta la nostra capacità di scrivere inutilmente espressioni difficili da capire (una cosa cattiva).

Anche:

if(smartphone != null && (smartphone.GetSignal() == 2 || smartphone.GetSignal() == 5 || smartphone.GetSignal() == 8 || smartPhone.GetSignal() == 34))
{
  // do something
}

Qui sto combinando l'uso di &&con un controllo per determinati valori. Questo non è realistico nel caso dei segnali telefonici, ma si presenta in altri casi. Ecco un esempio del mio non essere abbastanza intelligente. Se avessi fatto quanto segue:

if(smartphone != null)
{
  switch (smartphone.GetSignal())
  {
    case 2: case 5: case 8: case 34:
      // Do something
      break;
  }
}

Avrei guadagnato sia in leggibilità che probabilmente anche in termini di prestazioni (le chiamate multiple a GetSignal()probabilmente non sarebbero state ottimizzate via).

Anche in questo caso il problema non è tanto &&quanto prendere quel particolare martello e vedere tutto il resto come un chiodo; non usarlo mi permetta di fare qualcosa di meglio che usarlo.

Un caso finale che si allontana dalle migliori pratiche è:

if(a && b)
{
  //do something
}

Rispetto a:

if(a & b)
{
  //do something
}

L'argomentazione classica sul perché potremmo favorire quest'ultimo è che vi è qualche effetto collaterale nel valutare bche vogliamo che asia vero o no. Non sono d'accordo su questo: se quell'effetto collaterale è così importante, fallo accadere in una riga di codice separata.

Tuttavia, in termini di efficienza, è probabile che uno dei due sia migliore. Il primo eseguirà ovviamente meno codice (non valutando baffatto in un percorso di codice) che può farci risparmiare tutto il tempo necessario per valutare b.

Il primo però ha anche un altro ramo. Considera se lo riscriviamo nel nostro ipotetico linguaggio in stile C senza &&:

if(a)
{
  if(b)
  {
    // Do something
  }
}

Questo extra ifè nascosto nel nostro uso di &&, ma è ancora lì. E come tale è un caso in cui ci sono previsioni di succursali in corso e potenzialmente quindi false previsioni.

Per questo motivo il if(a & b)codice può essere più efficiente in alcuni casi.

Qui direi che if(a && b)è ancora l'approccio delle migliori pratiche con cui iniziare: Più comune, l'unico praticabile in alcuni casi (dove bsbaglierà se aè falso) e più veloce il più delle volte. Vale la pena notare che if(a & b)in alcuni casi è spesso un'utile ottimizzazione.


1
Se si hanno trymetodi che ritornano truein caso di successo, cosa ne pensi if (TryThis || TryThat) { success } else { failure }? È un linguaggio meno comune, ma non so come realizzare la stessa cosa senza duplicare il codice o aggiungere un flag. Mentre la memoria richiesta per un flag sarebbe un problema, dal punto di vista dell'analisi, l'aggiunta di un flag divide efficacemente il codice in due universi paralleli - uno contenente istruzioni che sono raggiungibili quando il flag è truee l'altro raggiungibile quando è false. A volte buono dal punto di vista SECCO, ma icky.
supercat,

5
Non vedo nulla di sbagliato in if(TryThis || TryThat).
Jon Hanna,

3
Dannazione eccellente, signore.
vescovo,

FWIW, programmatori eccellenti tra cui Kernighan, Plauger e Pike dei Bell Labs raccomandano per motivi di chiarezza di usare strutture di controllo superficiali come if {} else if {} else if {} else {}invece di strutture di controllo nidificate come if { if {} else {} } else {}, anche se ciò significa valutare la stessa espressione più di una volta. Linus Torvalds ha detto qualcosa di simile: "se hai bisogno di più di 3 livelli di rientro, sei comunque fregato". Prendiamolo come voto per gli operatori di corto circuito.
Sam Watkins,

if (a && b) statement; è ragionevolmente facile da sostituire. if (a && b && c && d) statement1; else statement2; è un vero dolore.
gnasher729,

10

Puoi andare oltre, e in alcune lingue è il modo idiomatico di fare le cose: puoi usare la valutazione del corto circuito anche al di fuori delle dichiarazioni condizionali, che diventano esse stesse una forma di dichiarazione condizionale. Ad esempio in Perl, è idiomatico che le funzioni restituiscano qualcosa di falso in caso di fallimento (e qualcosa di sincero in caso di successo), e qualcosa di simile

open($handle, "<", "filename.txt") or die "Couldn't open file!";

è idiomatico. openrestituisce qualcosa di diverso da zero in caso di successo (in modo che la dieparte dopo ornon accada mai), ma indefinita in caso di fallimento, quindi la chiamata a dieavvenire (questo è il modo di Perl di sollevare un'eccezione).

Va bene nelle lingue in cui lo fanno tutti.


17
Il maggior contributo di Perl alla progettazione del linguaggio è quello di fornire molteplici esempi di come non farlo.
Jack Aidley,

@JackAidley: mi mancano Perl unlesse i condizionali postfix ( send_mail($address) if defined $address).
RemcoGerlich,

6
@JackAidley potresti fornire un puntatore al perché 'or die' è male? È solo un modo ingenuo di gestire le eccezioni?
bigstones,

È possibile fare molte cose terribili in Perl, ma non riesco a vedere il problema con questo esempio. È semplice, breve e chiaro, esattamente ciò che si dovrebbe desiderare dalla gestione degli errori in un linguaggio di scripting.

1
@RemcoGerlich Ruby ha ereditato un po 'di quello stile:send_mail(address) if address
bonh,

6

Anche se in genere concordo con la risposta di dan1111 , esiste un caso particolarmente importante che non copre: il caso in cui smartphoneè usato contemporaneamente. In quello scenario, questo tipo di modello è una fonte ben nota di bug difficili da trovare.

Il problema è che la valutazione del corto circuito non è atomica. Il tuo thread può verificare che smartphonesia null, un altro thread può entrare e annullarlo, e quindi il tuo thread tenta prontamente di farlo GetSignal()esplodere.

Ma, naturalmente, lo fa solo che, quando le stelle si allineano e la tempistica vostre discussioni è solo giusto.

Quindi, sebbene questo tipo di codice sia molto comune e utile, è importante conoscere questo avvertimento in modo da poter prevenire accuratamente questo brutto bug.


3

Ci sono molte situazioni in cui voglio verificare prima una condizione e voglio solo verificare una seconda condizione se la prima condizione ha avuto successo. A volte per pura efficienza (perché non ha senso controllare la seconda condizione se la prima ha già fallito), a volte perché altrimenti il ​​mio programma andrebbe in crash (il tuo controllo per NULL prima), a volte perché altrimenti i risultati sarebbero terribili. (SE (Voglio stampare un documento) E (stampa di un documento non riuscita) ALLORA visualizza un messaggio di errore: non si desidera stampare il documento e verificare se la stampa non è riuscita se non si desidera stampare un documento nella primo posto!

Quindi c'era un ENORME bisogno di una valutazione garantita del corto circuito delle espressioni logiche. Non era presente in Pascal (che aveva una valutazione del corto circuito non garantita) che era un grande dolore; L'ho visto per la prima volta in Ada e C.

Se pensi davvero che questa sia una "cattiva pratica", prendi un po 'di codice moderatamente complesso, riscrivilo in modo che funzioni bene senza valutazione del corto circuito, e torna indietro e dicci come ti è piaciuto. È come se si dicesse che respirare produce anidride carbonica e chiedere se respirare è una cattiva pratica. Prova senza di essa per alcuni minuti.


"Riscriverlo in modo che funzioni bene senza la valutazione del corto circuito, e torna indietro e dicci come ti è piaciuto." - Sì, questo riporta i ricordi di Pascal. E no, non mi è piaciuto.
Thomas Padron-McCarthy,

2

Una considerazione, non menzionata in altre risposte: a volte avere questi controlli può suggerire un possibile refactoring al modello di progettazione di oggetti nulli . Per esempio:

if (currentUser && currentUser.isAdministrator()) 
  doSomething();

Potrebbe essere semplificato per essere solo:

if (currentUser.isAdministrator())
  doSomething ();

Se currentUserè impostato automaticamente su un "utente anonimo" o "utente null" con un'implementazione di fallback se l'utente non ha effettuato l'accesso.

Non sempre un odore di codice, ma qualcosa da considerare.


-4

Sarò impopolare e dirò di sì, è una cattiva pratica. SE la funzionalità del codice si basa su di esso per implementare la logica condizionale.

vale a dire

 if(quickLogicA && slowLogicB) {doSomething();}

è buono, ma

if(logicA && doSomething()) {andAlsoDoSomethingElse();}

è male, e sicuramente nessuno sarebbe d'accordo ?!

if(doSomething() || somethingElseIfItFailed() && anotherThingIfEitherWorked() & andAlwaysThisThing())
{/* do nothing */}

Ragionamento:

Il tuo errore di codice se vengono valutati entrambi i lati anziché semplicemente non eseguire la logica. È stato sostenuto che questo non è un problema, l'operatore 'and' è definito in c # quindi il significato è chiaro. Tuttavia, sostengo che questo non è vero.

Motivo 1. Storico

Fondamentalmente stai facendo affidamento su (ciò che originariamente era inteso come) un'ottimizzazione del compilatore per fornire funzionalità nel tuo codice.

Sebbene in c # le specifiche del linguaggio garantiscano il booleano 'e' l'operatore fa corto circuito. Questo non è vero per tutte le lingue e compilatori.

wikipeda elenca varie lingue e la loro opinione sul corto circuito http://en.wikipedia.org/wiki/Short-circuit_evaluation, come puoi, ci sono molti approcci. E un paio di problemi extra in cui non vado qui.

In particolare, FORTRAN, Pascal e Delphi hanno flag di compilazione che attivano questo comportamento.

Pertanto: è possibile che il codice funzioni quando viene compilato per il rilascio, ma non per il debug o con un compilatore alternativo ecc.

Motivo 2. Differenze linguistiche

Come puoi vedere da Wikipedia, non tutte le lingue adottano lo stesso approccio al corto circuito, anche quando specificano come gli operatori booleani dovrebbero gestirlo.

In particolare l'operatore 'e' di VB.Net non mette in corto circuito e TSQL ha alcune peculiarità.

Dato che è probabile che una moderna "soluzione" includa un numero di lingue, come javascript, regex, xpath ecc., È necessario tenerne conto quando si chiarisce la funzionalità del codice.

Motivo 3. Differenze all'interno di una lingua

Ad esempio VB inline if si comporta diversamente dal suo if. Ancora una volta nelle applicazioni moderne il tuo codice può spostarsi tra, ad esempio code behind e un modulo web incorporato, devi essere consapevole delle differenze di significato.

Motivo 4. L'esempio specifico di null che controlla il parametro di una funzione

Questo è un molto buon esempio. In questo è qualcosa che fanno tutti, ma in realtà è una cattiva pratica quando ci pensi.

È molto comune vedere questo tipo di controllo in quanto riduce notevolmente le righe di codice. Dubito che qualcuno lo tirerebbe fuori in una recensione di codice. (e ovviamente dai commenti e dalla valutazione puoi vedere che è popolare!)

Tuttavia, non riesce le migliori pratiche per più di un motivo!

  1. È molto chiaro da numerose fonti, che la migliore pratica è controllare la validità dei parametri prima di utilizzarli e generare un'eccezione se non sono validi. Lo snippet di codice non mostra il codice circostante, ma probabilmente dovresti fare qualcosa del tipo:

    if (smartphone == null)
    {
        //throw an exception
    }
    // perform functionality
  2. Stai chiamando una funzione dall'interno dell'istruzione condizionale. Anche se il nome della tua funzione implica che calcola semplicemente un valore, può nascondere effetti collaterali. per esempio

    Smartphone.GetSignal() { this.signal++; return this.signal; }
    
    else if (smartphone.GetSignal() < 1)
    {
        // Do stuff 
    }
    else if (smartphone.GetSignal() < 2)
    {
        // Do stuff 
    }

    le migliori pratiche suggeriscono:

    var s = smartphone.GetSignal();
    if(s < 50)
    {
        //do something
    }

Se applichi queste migliori pratiche al tuo codice, puoi vedere che la tua opportunità di ottenere un codice più breve attraverso l'uso del condizionale nascosto scompare. La migliore pratica è fare qualcosa , gestire il caso in cui lo smartphone è nullo.

Penso che scoprirai che questo vale anche per altri usi comuni. Devi ricordare che abbiamo anche:

condizionali in linea

var s = smartphone!=null ? smartphone.GetSignal() : 0;

e null coalescenza

var s = smartphoneConnectionStatus ?? "No Connection";

che forniscono funzionalità simili di lunghezza del codice funzione con maggiore chiarezza

numerosi link per supportare tutti i miei punti:

https://stackoverflow.com/questions/1158614/what-is-the-best-practice-in-case-one-argument-is-null

Convalida dei parametri del costruttore in C # - Best practice

Si dovrebbe verificare la null se non si aspetta null?

https://msdn.microsoft.com/en-us/library/seyhszts%28v=vs.110%29.aspx

https://stackoverflow.com/questions/1445867/why-would-a-language-not-use-short-circuit-evaluation

http://orcharddojo.net/orchard-resources/Library/DevelopmentGuidelines/BestPractices/CSharp

esempi di flag del compilatore che influiscono sul corto circuito:

Fortran: "sce | nosce Per impostazione predefinita, il compilatore esegue la valutazione del corto circuito in espressioni logiche selezionate utilizzando le regole XL Fortran. La specifica di sce consente al compilatore di utilizzare regole non XL Fortran. Il compilatore eseguirà la valutazione del corto circuito se le regole correnti lo consentono Il valore predefinito è nosce. "

Pascal: "B - Una direttiva flag che controlla la valutazione del corto circuito. Vedere Ordine di valutazione degli operandi di operatori diadici per ulteriori informazioni sulla valutazione del corto circuito. Vedere anche l'opzione del compilatore -sc per il compilatore della riga di comando per ulteriori informazioni ( documentato nel Manuale dell'utente). "

Delphi: "Delphi supporta la valutazione del corto circuito per impostazione predefinita. Può essere disattivato usando la direttiva del compilatore {$ BOOLEVAL OFF}."


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Ingegnere mondiale,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.