Devo seguire il percorso normale o fallire presto?


73

Dal libro Code Complete arriva la seguente citazione:

"Metti il ​​caso normale dopo il ifpiuttosto che dopo il else"

Ciò significa che nel elsecaso dovrebbero essere previste eccezioni / deviazioni dal percorso standard .

Ma The Pragmatic Programmer ci insegna a "schiantarci presto" (p. 120).

Quale regola dovrei seguire?


15
@gnat non duplicato
BЈовић

16
i due non si escludono a vicenda, non c'è ambiguità.
jwenting

6
Non sono così sicuro che la citazione completa del codice sia un ottimo consiglio. Presumibilmente è un tentativo di migliorare la leggibilità, ma ci sono certamente situazioni in cui vorresti che i casi non comuni fossero prima testuali. Nota che questa non è una risposta; le risposte esistenti spiegano già la confusione da cui deriva la tua domanda.
Eamon Nerbonne,

14
Arresto anticipato, arresto anomalo! Se uno dei tuoi iframi ritorna, usalo prima. Ed evita il elseresto del codice, sei già tornato se le pre-condizioni fallivano. Il codice è più facile da leggere, meno rientro ...
CodeAngry

5
Sono solo io a pensare che questo non abbia nulla a che fare con la leggibilità, ma probabilmente è stato solo un tentativo fuorviante di ottimizzazione per la previsione del ramo statico?
Mehrdad,

Risposte:


189

"Arresto anomalo" non riguarda la linea di codice che precede testualmente. Ti dice di rilevare errori nella prima fase possibile dell'elaborazione , in modo da non prendere inavvertitamente decisioni e calcoli basati su uno stato già difettoso.

In un costrutto if/ else, viene eseguito solo uno dei blocchi, quindi nessuno dei due può essere considerato un passo "precedente" o "successivo". Il modo in cui ordinarli è quindi una questione di leggibilità e "fallire presto" non entra nella decisione.


2
Il tuo punto generale è fantastico ma vedo un problema. Se hai (if / else if / else), l'espressione valutata in "else if" viene valutata dopo l'espressione nell'istruzione "if". La parte importante, come lei sottolinea, è la leggibilità rispetto all'unità di elaborazione concettuale. Viene eseguito un solo blocco di codice ma è possibile valutare più condizioni.
Encaitar,

7
@Encaitar, quel livello di granularità è molto più piccolo di quello che generalmente si intende quando viene usata la frase "fail early".
riwalk

2
@Encaitar Questa è l'arte della programmazione. Di fronte a più controlli, l'idea è quella di provare prima quello che più probabilmente è vero. Tuttavia, tali informazioni potrebbero non essere note in fase di progettazione ma in fase di ottimizzazione, ma attenzione all'ottimizzazione prematura.
BP:

Commenti onesti. Questa è stata una buona risposta e volevo solo aiutare a renderlo migliore per riferimento futuro
Encaitar,

I linguaggi di scripting come JavaScript, Python, perl, PHP, bash, ecc. Sono eccezioni perché interpretati in modo lineare. Nei piccoli if/elsecostrutti probabilmente non importa. Ma quelli chiamati in un ciclo o con molte istruzioni in ciascun blocco potrebbero essere eseguiti più velocemente con la condizione più comune per prima.
DocSalvager,

116

Se la tua elsedichiarazione contiene solo un codice di errore, molto probabilmente non dovrebbe essere lì.

Invece di fare questo:

if file.exists() :
  if validate(file) :
    # do stuff with file...
  else :
    throw foodAtMummy
else :
  throw toysOutOfPram

Fai questo

if not file.exists() :
  throw toysOutOfPram

if not validate(file) :
  throw foodAtMummy

# do stuff with file...

Non vuoi annidare profondamente il tuo codice semplicemente per includere il controllo degli errori.

E, come hanno già affermato tutti gli altri, i due consigli non sono contraddittori. Uno riguarda l'ordine di esecuzione , l'altro riguarda l' ordine del codice .


4
Vale la pena essere esplicito che il consiglio di inserire un flusso normale nel blocco dopo ife un flusso eccezionale nel blocco dopo elsenon si applica se non si dispone di un else! Dichiarazioni di guardia come questa sono la forma preferita per gestire le condizioni di errore nella maggior parte degli stili di codifica.
Jules,

+1 questo è un buon punto e in realtà dà una sorta di risposta alla vera domanda su come ordinare cose con condizioni di errore.
ashes999,

Sicuramente molto più chiaro e più facile da mantenere. È così che mi piace farlo.
Giovanni

27

Dovresti seguirli entrambi.

Il consiglio "Arresto anticipato" / errore iniziale indica che è necessario testare gli input per possibili errori il più presto possibile.
Ad esempio, se il tuo metodo accetta una dimensione o un conteggio che si suppone sia positivo (> 0), allora la consulenza anticipata non riuscita significa che si verifica questa condizione all'inizio del metodo anziché attendere che l'algoritmo produca assurdità risultati.

Il consiglio di mettere al primo posto il caso normale significa che se si verifica una condizione, il percorso più probabile dovrebbe venire prima. Questo aiuta nelle prestazioni (poiché la previsione del ramo del processore sarà più frequente) e nella leggibilità, perché non è necessario saltare blocchi di codice quando si cerca di capire cosa sta facendo la funzione nel caso normale.
Questo consiglio non si applica realmente quando si verifica una condizione preliminare e si salva immediatamente (usando assert o if (!precondition) throwcostrutti), perché non vi è alcun errore nella gestione da saltare durante la lettura del codice.


1
Puoi approfondire la parte di previsione del ramo? Non mi aspetterei che il codice che è più probabile che vada al caso if venga eseguito più velocemente del codice che è più probabile che vada al caso altro. Voglio dire, questo è l'intero punto di previsione del ramo, non è vero?
Roman Reiner,

@ user136712: nei moderni processori (veloci), le istruzioni vengono recuperate prima che l'istruzione precedente abbia terminato l'elaborazione. La previsione del ramo viene utilizzata per aumentare la probabilità che le istruzioni recuperate durante l'esecuzione di un ramo condizionale siano anche le istruzioni giuste da eseguire.
Bart van Ingen Schenau,

2
So cos'è la previsione del ramo. Se leggo correttamente il tuo post, dici che if(cond){/*more likely code*/}else{/*less likely code*/}corre più veloce che a if(!cond){/*less likely code*/}else{/*more likely code*/}causa della previsione del ramo. Penserei che la previsione del ramo non sia distorta dall'istruzione ifo elsee che tenga conto solo della storia. Quindi, se elseè più probabile che accada, dovrebbe essere in grado di prevederlo altrettanto bene. Questo presupposto è falso?
Roman Reiner,

18

Penso che @JackAidley abbia già detto l'essenza , ma lasciami formularlo in questo modo:

senza eccezioni (ad es. C)

Nel flusso di codice regolare, hai:

if (condition) {
    statement;
} else if (less_likely_condition) {
    less_likely_statement;
} else {
    least_likely_statement;
}
more_statements;

Nel caso "errore in anticipo", il tuo codice legge improvvisamente:

/* demonstration example, do NOT code like this */
if (condition) {
    statement;
} else {
    error_handling;
    return;
}

Se noti questo modello - a returnin un else(o anche if) blocco, rielaboralo immediatamente in modo che il codice in questione non abbia un elseblocco:

/* only code like this at University, to please structured programming professors */
function foo {
    if (condition) {
        lots_of_statements;
    }
    return;
}

Nel mondo reale…

/* code like this instead */
if (!condition) {
    error_handling;
    return;
}
lots_of_statements;

Questo evita l'annidamento troppo profondo e soddisfa il caso "break out early" (aiuta a mantenere la mente - e il flusso del codice - pulito) e non viola il "mettere la cosa più probabile nella ifparte" perché semplicemente non c'è elseparte .

C e pulizia

Ispirato da una risposta a una domanda simile (che ha sbagliato), ecco come eseguire la pulizia con C. Qui puoi usare uno o due punti di uscita, eccone uno per due punti di uscita:

struct foo *
alloc_and_init(size_t arg1, int arg2)
{
    struct foo *res;

    if (!(res = calloc(sizeof(struct foo), 1)))
        return (NULL);

    if (foo_init1(res, arg1))
        goto err;
    res.arg1_inited = true;
    if (foo_init2(&(res->blah), arg2))
        goto err;
    foo_init_complete(res);
    return (res);

 err:
    /* safe because we use calloc and false == 0 */
    if (res.arg1_inited)
        foo_dispose1(res);
    free(res);
    return (NULL);
}

Puoi comprimerli in un punto di uscita se c'è meno pulizia da fare:

char *
NULL_safe_strdup(const char *arg)
{
    char *res = NULL;

    if (arg == NULL)
        goto out;

    /* imagine more lines here */
    res = strdup(arg);

 out:
    return (res);
}

Questo uso di gotova benissimo, se puoi affrontarlo; il consiglio di smettere di usare gotoè rivolto a persone che non possono ancora decidere da sole se un uso è buono, accettabile, cattivo, codice spaghetti o qualcos'altro.

eccezioni

Quanto sopra parla di lingue senza eccezioni, che preferisco di gran lunga me stesso (posso usare la gestione degli errori espliciti molto meglio e con molta meno sorpresa). Per citare igli:

<igli> exceptions: a truly awful implementation of quite a nice idea.
<igli> just about the worst way you could do something like that, afaic.
<igli> it's like anti-design.
<mirabilos> that too… may I quote you on that?
<igli> sure, tho i doubt anyone will listen ;)

Ma ecco un suggerimento su come farlo bene in una lingua con eccezioni e quando vuoi usarli bene:

errore restituito a fronte di eccezioni

È possibile sostituire la maggior parte dei primi returncon l'eccezione. Tuttavia , il normale flusso del programma, ovvero qualsiasi flusso di codice in cui il programma non ha riscontrato, beh, un'eccezione ... una condizione di errore o qualcosa del genere, non deve sollevare eccezioni.

Ciò significa che…

# this page is only available to logged-in users
if not isLoggedIn():
    # this is Python 2.5 style; insert your favourite raise/throw here
    raise "eh?"

... va bene, ma ...

/* do not code like this! */
try {
    openFile(xyz, "rw");
} catch (LockedException e) {
    return "file is locked";
}
closeFile(xyz);
return "file is not locked";

… non è. Fondamentalmente, un'eccezione non è un elemento di flusso di controllo . Questo rende anche Operations strano per te ("quei programmatori Java ™ ci dicono sempre che queste eccezioni sono normali") e può ostacolare il debug (es. Dire all'IDE di interrompere qualsiasi eccezione). Le eccezioni spesso richiedono che l'ambiente di runtime rilasci lo stack per produrre traceback, ecc. Probabilmente ci sono più ragioni per evitarlo.

Questo si riduce a: in un linguaggio che supporta le eccezioni, usa tutto ciò che corrisponde alla logica e allo stile esistenti e ti sembra naturale. Se scrivi qualcosa da zero, concorda presto. Se scrivi una libreria da zero, pensa ai tuoi consumatori. (Non usare mai, abort()neanche in una biblioteca ...) Ma qualunque cosa tu faccia, non fai, come regola generale, un'eccezione se l'operazione continua (più o meno) normalmente dopo di essa.

consiglio generale eccezioni

Cerca di ottenere prima tutto l'utilizzo in programma delle eccezioni concordate da tutto il team di sviluppo. Fondamentalmente, pianificali. Non usarli in abbondanza. A volte, anche in C ++, Java ™, Python, un ritorno dell'errore è migliore. A volte non lo è; usali con il pensiero.


In generale, vedo i ritorni iniziali come odore di codice. Darei invece un'eccezione, se il seguente codice fallisse perché non era soddisfatta una condizione preliminare. Solo dicendo
Danman

1
@DanMan: il mio articolo è stato scritto pensando a C ... Normalmente non uso Eccezioni. Ma ho esteso l'articolo con un suggerimento (oops, è diventato piuttosto lungo). eccezioni; per inciso, ieri abbiamo avuto la stessa domanda sulla mailing list di sviluppo interna dell'azienda ...
mirabilos,

Inoltre, usa le parentesi graffe anche su if e fors a una riga. Non ne vuoi un altro goto fail;nascosto nell'identificazione.
Bruno Kim,

1
@BrunoKim: questo dipende completamente dalla convenzione di stile / codifica del progetto con cui lavori. Lavoro con BSD, dove questo è disapprovato (più disordine ottico e perdita di spazio verticale); a $ dayjob tuttavia li inserisco come concordato (meno difficile per i neofiti, meno possibilità di errori, ecc. come hai detto).
mirabilos

3

A mio avviso, "Condizioni di guardia" è uno dei modi migliori e più semplici per rendere leggibile il codice. Odio davvero quando vedo ifl'inizio del metodo e non vedo il elsecodice perché è fuori dallo schermo. Devo scorrere verso il basso solo per vedere throw new Exception.

Metti i controlli all'inizio in modo che la persona che legge il codice non debba saltare dappertutto il metodo per leggerlo ma invece scansiona sempre dall'alto verso il basso.


2

(La risposta di @mirabilos è eccellente, ma ecco come penso alla domanda per arrivare alla stessa conclusione :)

Sto pensando a me stesso (o a qualcun altro) a leggere il codice della mia funzione in seguito. Quando leggo la prima riga, non posso fare alcuna ipotesi sul mio input (tranne quelli che non verificherò comunque). Quindi il mio pensiero è "Ok, so che farò delle cose con i miei argomenti. Ma prima" puliamoli ", cioè uccidiamo i percorsi di controllo in cui non sono di mio gradimento." Ma allo stesso tempo , Non vedo il caso normale come qualcosa che è condizionato; voglio sottolineare che è normale.

int foo(int* bar, int baz) {

   if (bar == NULL) /* I don't like you, leave me alone */;
   if (baz < 0) /* go away */;

   /* there, now I can do the work I came into this function to do,
      and I can safely forget about those if's above and make all 
      the assumptions I like. */

   /* etc. */
}

-3

Questo tipo di ordinamento delle condizioni dipende dalla criticità della sezione di codice in questione e dall'esistenza di valori predefiniti utilizzabili.

In altre parole:

A. sezione critica e nessuna impostazione predefinita => Fail Early

B. sezione non critica e valori predefiniti => Usa valori predefiniti nella parte altrimenti

C. tra i casi => decide per caso secondo necessità


è solo una tua opinione o puoi spiegarla / sostenerla in qualche modo?
moscerino del

come esattamente questo non è il backup, come ogni opzione spiega (senza molte parole davvero) perché viene utilizzata?
Nikos M.,

non vorrei dirlo, ma i voti negativi in ​​(mia) questa risposta sono fuori contesto :). questa è la domanda posta dall'OP, se hai una risposta alternativa è un'altra questione
Nikos M.,

Onestamente non riesco a vedere la spiegazione qui. Ad esempio, se qualcuno scrive un'altra opinione, come "sezione critica e nessuna impostazione predefinita => non fallire presto" , in che modo questa risposta aiuterebbe il lettore a scegliere due opinioni opposte? Valuta di modificarlo in una forma migliore, per adattarlo alle linee guida su Come rispondere .
moscerino del

ok, vedo, questa potrebbe essere un'altra spiegazione, ma almeno capisci la parola "sezione critica" e "nessuna impostazione predefinita" che può implicare una strategia per fallire presto e questa è davvero una espiazione seppur minimalista
Nikos M.
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.