Modi eleganti per gestire if (if else) else


161

Questo è un piccolo inconveniente, ma ogni volta che devo codificare qualcosa del genere, la ripetizione mi dà fastidio, ma non sono sicuro che nessuna delle soluzioni sia peggiore.

if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
    }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}
  • C'è un nome per questo tipo di logica?
  • Sono un po 'troppo DOC?

Sono aperto ai suggerimenti di codici malvagi, anche solo per motivi di curiosità ...


8
@Emmad Kareem: due DefaultActionchiamate violano il principio DRY
Abyx,

Grazie per la tua risposta, ma penso che sia OK, tranne per non aver usato try / catch poiché potrebbero esserci errori che non restituiscono risultati e causerebbero interruzioni (a seconda del linguaggio di programmazione).
NoChance,

20
Penso che il problema principale qui sia che stai lavorando a livelli incoerenti di astrazione . Il livello di astrazione più elevato è il seguente: make sure I have valid data for DoSomething(), and then DoSomething() with it. Otherwise, take DefaultAction(). I dettagli grintosi di assicurarsi di avere i dati per DoSomething () sono a un livello di astrazione più basso, e quindi dovrebbero avere una funzione diversa. Questa funzione avrà un nome nel livello di astrazione più elevato e la sua implementazione sarà di basso livello. Le buone risposte di seguito affrontano questo problema.
Gilad Naor,

6
Si prega di specificare una lingua. Le possibili soluzioni, i modi di dire standard e le norme culturali di lunga data sono diverse per le diverse lingue e porteranno a risposte diverse al tuo Q.
Caleb

1
È possibile fare riferimento a questo libro "Refactoring: migliorare il design del codice esistente". Ci sono diverse sezioni sulla struttura if-else, pratica davvero utile.
Vacker,

Risposte:


96

Estrai per separare la funzione (metodo) e usa l' returnistruzione:

if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
        return;
    }
}

DefaultAction();

O, forse meglio, separare il contenuto e la sua elaborazione:

contents_t get_contents(name_t file)
{
    if(!FileExists(file))
        return null;

    contents = OpenFile(file);
    if(!SomeTest(contents)) // like IsContentsValid
        return null;

    return contents;
}

...

contents = get_contents(file)
contents ? DoSomething(contents) : DefaultAction();

UPD:

Perché non le eccezioni, perché OpenFilenon genera l'eccezione IO:
penso che sia una domanda davvero generica, piuttosto che una domanda sul file IO. Nomi come FileExists, OpenFilepossono essere fonte di confusione, ma se sostituirli con Foo, Barecc., Sarebbe più chiaro che DefaultActionpossano essere chiamati tutte le volte DoSomething, quindi potrebbe essere un caso non eccezionale. Péter Török ne ha scritto alla fine della sua risposta

Perché c'è un operatore condizionale ternario nella seconda variante:
se ci fosse un tag [C ++], avrei scritto una ifdichiarazione con una dichiarazione contentsnella sua parte condizione:

if(contents_t contents = get_contents(file))
    DoSomething(contents);
else
    DefaultAction();

Ma per altri linguaggi (simili a C), if(contents) ...; else ...;è esattamente lo stesso dell'istruzione expression con operatore condizionale ternario, ma più lungo. Poiché la parte principale del codice era la get_contentsfunzione, ho appena usato la versione più breve (e ho anche omesso il contentstipo). Ad ogni modo, va oltre questa domanda.


93
+1 per più ritorni - quando i metodi sono sufficientemente piccoli , questo approccio ha funzionato meglio per me
moscerino

Non è un grande fan dei rendimenti multipli, anche se lo uso occasionalmente. È abbastanza ragionevole su qualcosa di semplice, ma non si adatta bene. Il nostro standard è quello di evitarlo per tutti i metodi semplici ma folli perché i metodi tendono a crescere di dimensioni più di quanto si riducano.
Brian Knoblauch,

3
Percorsi di restituzione multipli possono avere ripercussioni negative sulle prestazioni nei programmi C ++, vanificando gli sforzi dell'ottimizzatore di impiegare RVO (anche NRVO, a meno che ogni percorso non restituisca lo stesso oggetto).
Functastic

Consiglio di invertire la logica sulla seconda soluzione: {if (il file esiste) {imposta il contenuto; if (sometest) {restituisce contenuti; }} return null; } Semplifica il flusso e riduce il numero di righe.
Wed

1
Ciao Abyx, ho notato che hai incorporato alcuni dei feedback dei commenti qui: grazie per averlo fatto. Ho ripulito tutto ciò che è stato affrontato nella tua risposta e altre risposte.

56

Se il linguaggio di programmazione che stai utilizzando (0) confronta i binari dei cortocircuiti (ovvero se non chiama SomeTestse FileExistsrestituisce false) e (1) l'assegnazione restituisce un valore (il risultato OpenFileè assegnato a contentse quindi quel valore viene passato come argomento a SomeTest), puoi usare qualcosa di simile al seguente, ma ti consigliamo comunque di commentare il codice notando che il singolo =è intenzionale.

if( FileExists(file) && SomeTest(contents = OpenFile(file)) )
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}

A seconda di quanto sia contorto l'if, è meglio avere una variabile flag (che separa il test delle condizioni di successo / fallimento con il codice che gestisce l'errore DefaultActionin questo caso)


Ecco come lo farei.
Anthony,

13
ifSecondo me, è piuttosto volgare inserire così tanto codice in una dichiarazione.
moteutsch,

15
Al contrario, mi piace questo tipo di affermazione "se qualcosa esiste e soddisfa questa condizione". +1
Gorpik,

Lo faccio anch'io! Personalmente non mi piace il modo in cui le persone usano più ritorni, alcuni locali non sono soddisfatti. Perché non inverti questi if ed esegui il tuo codice se sono soddisfatti?
klaar,

"Se qualcosa esiste e soddisfa questa condizione" va bene. "se qualcosa esiste e fa qualcosa di tangenzialmente correlato qui e soddisfa questa condizione", OTOH, è confuso. In altre parole, non mi piacciono gli effetti collaterali in una condizione.
Piskvor,

26

Più seriamente della ripetizione della chiamata a DefaultAction è lo stesso stile perché il codice è scritto non ortogonale (vedi questa risposta per buoni motivi per scrivere ortogonalmente).

Per mostrare perché il codice non ortogonale è errato, prendi in considerazione l'esempio originale, quando viene introdotto un nuovo requisito per cui non dovremmo aprire il file se è archiviato su un disco di rete. Bene, allora potremmo semplicemente aggiornare il codice al seguente:

if(FileExists(file))
{
    if(! OnNetworkDisk(file))
    {
        contents = OpenFile(file); // <-- prevents inclusion in if
        if(SomeTest(contents))
        {
            DoSomething(contents);
        }
        else
        {
            DefaultAction();
        }
    }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}

Ma poi arriva anche la necessità di non aprire file di grandi dimensioni oltre 2 GB. Bene, ci limitiamo ad aggiornare di nuovo:

if(FileExists(file))
{
    if(LessThan2Gb(file))
    {
        if(! OnNetworkDisk(file))
        {
            contents = OpenFile(file); // <-- prevents inclusion in if
            if(SomeTest(contents))
            {
                DoSomething(contents);
            }
            else
            {
                DefaultAction();
            }
        }
        else
        {
            DefaultAction();
        }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}

Dovrebbe essere molto chiaro che tale stile di codice sarà un enorme problema di manutenzione.

Tra le risposte che sono scritte correttamente in modo ortogonale ci sono il secondo esempio di Abyx e la risposta di Jan Hudec , quindi non lo ripeterò, sottolineo solo che aggiungere i due requisiti in quelle risposte sarebbe solo

if(! LessThan2Gb(file))
    return null;

if(OnNetworkDisk(file))
    return null;

(o goto notexists;invece di return null;), senza influire su nessun altro codice oltre a quelle aggiunte . Ad esempio ortogonale.

Durante il test, la regola generale dovrebbe essere quella di testare le eccezioni, non il caso normale .


8
+1 per me. I ritorni anticipati aiutano ad evitare l'anti schema della freccia. Vedi codinghorror.com/blog/2006/01/flattening-arrow-code.html e lostechies.com/chrismissal/2009/05/27/… Prima di leggere questo schema, mi sono sempre iscritto a 1 entrata / uscita per funzione teoria a causa di ciò che mi è stato insegnato circa 15 anni fa. Credo che questo renda il codice molto più facile da leggere e, come dici, più gestibile.
Mr Moose,

3
@MrMoose: la tua menzione dell'anti-pattern a punta di freccia risponde alla domanda esplicita di Benjol: "C'è un nome per questo tipo di logica?" Pubblicalo come risposta e avrai il mio voto.
Outis

Questa è un'ottima risposta, grazie. E @MrMoose: "anti pattern a punta di freccia" probabilmente risponde al mio primo proiettile, quindi sì, pubblicalo. Non posso promettere che lo accetterò, ma merita voti!
Benjol,

@outis. Grazie. Ho aggiunto la risposta. L'anti schema a punta di freccia è certamente rilevante nel post di hlovdal e le sue clausole di guardia funzionano bene per aggirarli. Non so come si possa rispondere a questo secondo punto. Non sono qualificato per diagnosticare questo :)
Mr Moose,

4
+1 per "eccezioni di prova, non nel caso normale".
Roy Tinker,

25

Ovviamente:

Whatever(Arguments)
{
    if(!FileExists(file))
        goto notexists;
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(!SomeTest(contents))
        goto notexists;
    DoSomething(contents);
    return;
notexists:
    DefaultAction();
}

Hai detto di essere aperto anche alle soluzioni malvagie, quindi usando il male si conta, no?

In effetti, a seconda del contesto, questa soluzione potrebbe essere meno malvagia del male che compie l'azione due volte o del male extra variabile. L'ho avvolto in una funzione, perché sicuramente non sarebbe OK nel mezzo della funzione lunga (non da ultimo a causa del ritorno nel mezzo). Ma la funzione lunga non è OK, punto.

Quando hai delle eccezioni, saranno più facili da leggere, specialmente se puoi avere OpenFile e DoSomething semplicemente generare un'eccezione se le condizioni non sono soddisfatte, quindi non hai bisogno di controlli espliciti. D'altra parte in C ++, Java e C # lanciare un'eccezione è un'operazione lenta, quindi dal punto di vista delle prestazioni, il goto è ancora preferibile.


Nota su "male": la FAQ 6.15 di C ++ definisce "male" come:

Significa che tale e tale è qualcosa che dovresti evitare la maggior parte del tempo, ma non qualcosa che dovresti evitare tutto il tempo. Ad esempio, finirai per usare queste cose "malvagie" ogni volta che sono "le meno malvagie delle alternative malvagie".

E questo vale gotoin questo contesto. I costrutti strutturati di controllo del flusso sono migliori per la maggior parte del tempo, ma quando ci si trova nella situazione in cui si accumulano troppi dei loro mali, come l'assegnazione in condizioni, la nidificazione più profonda di circa 3 livelli, la duplicazione del codice o le condizioni lunghe, gotopuò semplicemente finire essere meno malvagio.


11
Il mio cursore si posiziona sul pulsante Accetta ... solo per far dispetto a tutti i puristi. Oooohh la tentazione: D
Benjol,

2
Si si! Questo è il modo assolutamente "giusto" per scrivere il codice. La struttura del codice ora è "Se errore, gestisci errore. Azione normale. Se errore, gestisci errore. Azione normale" che è esattamente come dovrebbe essere. Tutto il codice "normale" è scritto con un solo rientro di livello, mentre tutto il codice relativo all'errore ha due livelli di rientro. Quindi il codice normale E PIÙ IMPORTANTE ottiene il posto visivo più importante ed è possibile leggere in modo molto rapido e semplice il flusso in sequenza verso il basso. Accetta assolutamente questa risposta.
hlovdal

2
E un altro aspetto è che il codice scritto in questo modo è ortogonale. Ad esempio le due righe "if (! FileExists (file)) \ n \ tgoto non esiste;" sono ora SOLO correlati alla gestione di questo singolo aspetto dell'errore (KISS) e, soprattutto, non influisce su nessuna delle altre linee . Questa risposta stackoverflow.com/a/3272062/23118 elenca diversi buoni motivi per mantenere il codice ortogonale.
hlovdal

5
Parlando delle soluzioni del male: io posso avere la tua soluzione senza goto:for(;;) { if(!FileExists(file)) break; contents = OpenFile(file); if(!SomeTest(contents)) break; DoSomething(contents); return; } /* broken out */ DefaultAction();
Herby

4
@herby: La tua soluzione è più malvagia di goto, perché stai abusando breakin un modo in cui nessuno si aspetta che venga abusato, quindi le persone che leggono il codice avranno più problemi a vedere dove li porta l'interruzione che con goto che lo dice esplicitamente. Inoltre stai usando un ciclo infinito che verrà eseguito solo una volta, il che sarà piuttosto confuso. Sfortunatamente do { ... } while(0)non è nemmeno leggibile, perché vedi che è solo un blocco divertente quando arrivi alla fine e C non supporta la rottura da altri blocchi (a differenza di perl).
Jan Hudec,

12
function FileContentsExists(file) {
    return FileExists(file) ? OpenFile(file) : null;
}

...

contents = FileContentExists(file);
if(contents && SomeTest(contents))
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}

o vai al maschio in più e crea un metodo FileExistsAndConditionMet (file) aggiuntivo ...
UncleZeiv

@herby SomeTestpuò avere la stessa semantica dell'esistenza del file se SomeTestcontrolla il tipo di file, ad esempio controlla che .gif sia davvero un file GIF.
Abyx,

1
Sì. Dipende. @ Benjol lo sa meglio.
citato il

3
... ovviamente intendevo "vai al massimo" ... :)
ZioZeiv

2
Questo sta portando i ravioli alle estremità anche se non ci vado (e sono estremo in questo) ... Penso che ora sia ben leggibile considerando il contents && f(contents). Due funzioni per salvarne un'altra ?!
citato il

12

Una possibilità:

boolean handled = false;

if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
        handled = true;
    }
}
if (!handled)
{
    DefaultAction();
}

Naturalmente, questo rende il codice leggermente più complesso in un modo diverso. Quindi è in gran parte una domanda di stile.

Un approccio diverso sarebbe utilizzare le eccezioni, ad esempio:

try
{
    contents = OpenFile(file); // throws IO exception if file not found
    DoSomething(contents); // calls SomeTest() and throws exception on failure
}
catch(Exception e)
{
    DefaultAction();
    // and the exception should be at least logged...
}

Sembra più semplice, tuttavia è applicabile solo se

  • sappiamo esattamente che tipo di eccezioni aspettarci e che si DefaultAction()adattano a ciascuno
  • ci aspettiamo che l'elaborazione dei file abbia esito positivo e che un file mancante o un errore SomeTest()sia chiaramente una condizione errata, quindi è opportuno gettare un'eccezione su di esso.

19
Noooo ~! Non è una variabile flag, è sicuramente un modo sbagliato, perché porta a codice complesso, difficile da capire (dove-quella-bandiera-diventa-reale) e difficile da refactoring.
Abyx,

Non se lo limiti al più possibile ambito locale. (function () { ... })()in Javascript, { flag = false; ... }in ecc C-like
erbaceo

+1 per la logica dell'eccezione, che potrebbe benissimo essere la soluzione più adatta a seconda dello scenario.
Steven Jeuris,

4
+1 Questo reciproco 'Nooooo!' è divertente. Penso che la variabile di stato e il ritorno anticipato siano entrambi ragionevoli in alcuni casi. In routine più complesse, sceglierei la variabile status perché, anziché aggiungere complessità, ciò che fa realmente è rendere esplicita la logica. Niente di sbagliato in questo.
Grossvogel,

1
Questo è il nostro formato preferito dove lavoro. Le 2 principali opzioni utilizzabili sembrano essere "rendimenti multipli" e "variabili variabili". Nessuno dei due sembra avere alcun tipo di vero vantaggio in media, ma entrambi si adattano a determinate circostanze meglio di altri. Devo andare con il tuo caso tipico. Solo un'altra guerra religiosa "Emacs" contro "Vi". :-)
Brian Knoblauch,

11

Questo è ad un livello più alto di astrazione:

if (WeCanDoSomething(file))
{
   DoSomething(contents);
}
else
{
   DefaultAction();
} 

E questo riempie i dettagli.

boolean WeCanDoSomething(file)
{
    if FileExists(file)
    {
        contents = OpenFile(file);
        return (SomeTest(contents));
    }
    else
    {
        return FALSE;
    }
}

11

Le funzioni dovrebbero fare una cosa. Dovrebbero farlo bene. Dovrebbero farlo solo.
- Robert Martin in codice pulito

Alcune persone trovano questo approccio un po 'estremo, ma è anche molto pulito. Permettetemi di illustrare in Python:

def processFile(self):
    if self.fileMeetsTest():
        self.doSomething()
    else:
        self.defaultAction()

def fileMeetsTest(self):
    return os.path.exists(self.path) and self.contentsTest()

def contentsTest(self):
    with open(self.path) as file:
        line = file.readline()
        return self.firstLineTest(line)

Quando dice che le funzioni dovrebbero fare una cosa, intende una cosa. processFile()sceglie un'azione in base al risultato di un test e questo è tutto. fileMeetsTest()combina tutte le condizioni del test e questo è tutto. contentsTest()trasferisce la prima riga su firstLineTest(), ed è tutto ciò che fa.

Sembra un sacco di funzioni, ma sembra praticamente un semplice inglese:

Per elaborare il file, verificare se soddisfa il test. In tal caso, fai qualcosa. Altrimenti, eseguire l'azione predefinita. Il file soddisfa il test se esiste e supera il test dei contenuti. Per testare il contenuto, aprire il file e testare la prima riga. Il test per la prima linea ...

Certo, è un po 'prolisso, ma nota che se un manutentore non si preoccupa dei dettagli, può smettere di leggere dopo solo le 4 righe di codice processFile()e avrà comunque una buona conoscenza di alto livello di ciò che fa la funzione.


5
+1 È un buon consiglio, ma ciò che costituisce "una cosa" dipende dall'attuale livello di astrazione. processFile () è "una cosa", ma due cose: fileMeetsTest () e doSomething () o defaultAction (). Temo che l'aspetto "una cosa" possa confondere i principianti che non capiscono a priori il concetto.
Caleb,

1
È un buon obiettivo ... Questo è tutto ciò che ho da dire a riguardo ... ;-)
Brian Knoblauch,

1
Non mi piace implicitamente passare argomenti come variabili di istanza del genere. Ti riempi di variabili di istanza "inutili" e ci sono molti modi per rovinare il tuo stato e spezzare gli invarianti.
hugomg,

@Caleb, ProcessFile () sta effettivamente facendo una cosa. Come afferma Karl nel suo post, sta usando un test per decidere quale azione intraprendere e rinviare la reale attuazione delle possibilità di azione ad altri metodi. Se si dovessero aggiungere molte altre azioni alternative, i criteri a scopo singolo per il metodo verrebbero comunque rispettati fintanto che nel metodo immediato non si verifica l'annidamento della logica.
S.Robins,

6

Per quanto riguarda ciò che viene chiamato, può facilmente svilupparsi nel modello anti punta di freccia man mano che il codice cresce per gestire più requisiti (come mostrato dalla risposta fornita su https://softwareengineering.stackexchange.com/a/122625/33922 ) e quindi cade nella trappola di avere enormi sezioni di codici con istruzioni condizionali nidificate che assomigliano a una freccia.

Vedi collegamenti come;

http://codinghorror.com/blog/2006/01/flattening-arrow-code.html

http://lostechies.com/chrismissal/2009/05/27/anti-patterns-and-worst-practices-the-arrowhead-anti-pattern/

C'è molto di più su questo e altri schemi anti da trovare su Google.

Alcuni ottimi consigli che Jeff fornisce sul suo blog a riguardo sono;

1) Sostituire le condizioni con clausole di protezione.

2) Decomporre i blocchi condizionali in funzioni separate.

3) Convertire i controlli negativi in ​​controlli positivi

4) Ritorna sempre opportunisticamente dalla funzione.

Vedi alcuni dei commenti sul blog di Jeff riguardo ai suggerimenti di Steve McConnells anche sui primi ritorni;

"Usa un ritorno quando migliora la leggibilità: in alcune routine, una volta che conosci la risposta, vuoi restituirla immediatamente alla routine di chiamata. Se la routine è definita in modo tale da non richiedere ulteriore pulizia una volta rileva un errore, non tornare immediatamente significa che devi scrivere più codice. "

...

"Riduci al minimo il numero di resi in ogni routine: è più difficile capire una routine quando, leggendola in fondo, non sei consapevole della possibilità che sia tornata da qualche parte. Per questo motivo, usa i rendimenti con giudizio - solo quando migliorano leggibilità ".

Mi sono sempre iscritto alla teoria 1 entrata / uscita per funzione a causa di ciò che mi è stato insegnato circa 15 anni fa. Credo che questo renda il codice molto più facile da leggere e, come dici, più gestibile


6

Questo è conforme alle regole DRY, no-goto e no-multiple-return, secondo me è scalabile e leggibile:

success = FileExists(file);
if (success)
{
    contents = OpenFile(file);
    success = SomeTest(contents);
}
if (success)
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}

1
Tuttavia, la conformità agli standard non equivale necessariamente a un buon codice. Sono attualmente indeciso su questo frammento di codice.
Brian Knoblauch,

questo sostituisce solo 2 defaultAction (); con 2 condizioni if ​​identiche e aggiunge una variabile flag che è molto peggio.
Ryathal,

3
Il vantaggio dell'uso di un costrutto come questo è che all'aumentare del numero di test il codice non inizia a nidificare più ifs all'interno di altri ifs. Inoltre, il codice per gestire il caso non riuscito ( DefaultAction()) si trova solo in un punto e per scopi di debug il codice non salta attorno alle funzioni di supporto e l'aggiunta di punti di interruzione alle linee in cui successviene cambiata la variabile può mostrare rapidamente quali test sono passati (sopra il trigger punto di interruzione) e quali non sono stati testati (sotto).
frozenkoi,

1
Sì, mi piace un po ', ma penso che rinominerei successin ok_so_far:)
Benjol il

Questo è molto simile a quello che faccio quando (1) il processo è molto lineare quando tutto va bene e (2) altrimenti avresti la freccia anti-pattern. Tuttavia, cerco di evitare di aggiungere una variabile aggiuntiva, che di solito è facile se si pensa in termini di prerequisiti per il passaggio successivo (che è leggermente diverso dal chiedere se un passaggio precedente non è riuscito). Se il file esiste, aprire il file. Se il file è aperto, leggi il contenuto. Se ho contenuti, elaborali, altrimenti esegui l'azione predefinita.
Adrian McCarthy,

3

Lo estrarrei in un metodo separato e quindi:

if(!FileExists(file))
{
    DefaultAction();
    return;
}

contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return;
}

DoSomething(contents);

che consente anche

if(!FileExists(file))
{
    DefaultAction();
    return Result.FileNotFound;
}

contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return Result.TestFailed;
}

DoSomething(contents);
return Result.Success;            

quindi possibilmente potresti rimuovere le DefaultActionchiamate e lasciare l'esecuzione DefaultActionper il chiamante:

Result OurMethod(file)
{
    if(!FileExists(file))
    {
        return Result.FileNotFound;
    }

    contents = OpenFile(file);
    if(!SomeTest(contents))
    {
        return Result.TestFailed;
    }

    DoSomething(contents);
    return Result.Success;            
}

void Caller()
{
    // something, something...

    var result = OurMethod(file);
    // if (result == Result.FileNotFound || result == Result.TestFailed), or just
    if (result != Result.Success)        
    {
        DefaultAction();
    }
}

Mi piace anche l'approccio di Jeanne Pindar .


3

Per questo caso particolare, la risposta è abbastanza semplice ...

C'è una condizione di competizione tra FileExistse OpenFile: cosa succede se il file viene rimosso?

L'unico modo sano di affrontare questo caso particolare è saltare FileExists:

contents = OpenFile(file);
if (!contents) // open failed
    DefaultAction();
else (SomeTest(contents))
    DoSomething(contents);

Questo risolve perfettamente questo problema e rende il codice più pulito.

In generale: prova a ripensare il problema e escogita un'altra soluzione che eviti completamente il problema.


2

Un'altra possibilità, se non ti piace vedere troppi l'altro è far cadere l'uso di altro del tutto e gettare in una dichiarazione extra rendimento. Altrimenti è in qualche modo superfluo a meno che non sia necessaria una logica più complessa per determinare se ci sono più di due sole possibilità di azione.

Quindi il tuo esempio potrebbe diventare:

void DoABunchOfStuff()
{
    if(FileExists(file))
    {
        DoSomethingWithFileContent(file);
        return;
    }

    DefaultAction();
}

void DoSomethingWithFileContent(file)
{        
    var contents = GetFileContents(file)

    if(SomeTest(contents))
    {
        DoSomething(contents);
        return;
    }

    DefaultAction();
}

AReturnType GetFileContents(file)
{
    return OpenFile(file);
}

Personalmente non mi dispiace usare la clausola else in quanto afferma esplicitamente come dovrebbe funzionare la logica e quindi migliora la leggibilità del codice. Alcuni strumenti di abbellimento del codice preferiscono tuttavia semplificare una singola istruzione if per scoraggiare la logica di annidamento.


2

Il caso mostrato nel codice di esempio può in genere essere ridotto a una singola ifistruzione. Su molti sistemi, la funzione di apertura del file restituirà un valore non valido se il file non esiste già. A volte questo è il comportamento predefinito; altre volte, deve essere specificato tramite un argomento. Ciò significa che il FileExiststest può essere eliminato, il che può anche aiutare con le condizioni di gara risultanti dalla cancellazione del file tra il test di esistenza e l'apertura del file.

file = OpenFile(path);
if(isValidFileHandle(file) && SomeTest(file)) {
    DoSomething(file);
} else {
    DefaultAction();
}

Questo non affronta direttamente il problema di miscelazione a livello di astrazione poiché elude completamente il problema di test multipli e non concatenabili, sebbene eliminare il test di esistenza dei file non sia incompatibile con la separazione dei livelli di astrazione. Supponendo che gli handle di file non validi equivalgano a "false" e che gli handle di file si chiudano quando escono dall'ambito:

OpenFileIfSomething(path:String) : FileHandle {
    file = OpenFile(path);
    if (file && SomeTest(file)) {
        return file;
    }
    return null;
}

...

if ((file = OpenFileIfSomething(path))) {
    DoSomething(file);
} else {
    DefaultAction();
}

2

Sono d'accordo con frozenkoi, tuttavia, per C # comunque, ho pensato che avrebbe aiutato a seguire la sintassi dei metodi TryParse.

if(FileExists(file) && TryOpenFile(file, out contents))
    DoSomething(contents);
else
    DefaultAction();
bool TryOpenFile(object file, out object contents)
{
    try{
        contents = OpenFile(file);
    }
    catch{
        //something bad happened, computer probably exploded
        return false;
    }
    return true;
}

1

Il tuo codice è brutto perché stai facendo troppo in una singola funzione. Si desidera elaborare il file o eseguire l'azione predefinita, quindi iniziare dicendo che:

if (!ProcessFile(file)) { 
  DefaultAction(); 
}

I programmatori Perl e Ruby scrivono processFile(file) || defaultAction()

Ora vai a scrivere ProcessFile:

if (FileExists(file)) { 
  contents = OpenFile(file);
  if (SomeTest(contents)) {
    processContents(contents);
    return true;
  }
}
return false;

1

Ovviamente puoi andare così lontano solo in scenari come questi, ma ecco un modo per andare:

interface File<T> {
    function isOK():Bool;
    function getData():T;
}

var appleFile:File<Apple> = appleStorage.get(fileURI);
if (appleFile.isOK())
    eat(file.getData());
else
    cry();

Potresti desiderare filtri aggiuntivi. Quindi fai questo:

var appleFile = appleStorage.get(fileURI, isEdible);
//isEdible is of type Apple->Bool and will be used internally to answer to the isOK call
if (appleFile.isOK())
    eat(file.getData());
else
    cry();

Anche se questo potrebbe avere senso anche:

function eat(apple:Apple) {
     if (isEdible(apple)) 
         digest(apple);
     else
         die();
}
var appleFile = appleStorage.get(fileURI);
if (appleFile.isOK())
    eat(appleFile.getData());
else
    cry();

Qual'è il migliore? Dipende dal problema del mondo reale che stai affrontando.
Ma la cosa da togliere è: puoi fare molto con la composizione e il polimorfismo.


1

Cosa c'è che non va nell'ovvio

if(!FileExists(file)) {
    DefaultAction();
    return;
}
contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return;
}        
DoSomething(contents);

Mi sembra abbastanza standard? Per quel tipo di grande procedura in cui devono accadere molte piccole cose, il fallimento di una qualsiasi delle quali impedirebbe quest'ultima. Le eccezioni lo rendono un po 'più pulito se questa è un'opzione.


0

Credo che questa sia una vecchia domanda, ma ho notato uno schema che non è stato menzionato; principalmente, impostando una variabile per determinare in seguito il / i metodo / i che si desidera chiamare (al di fuori di if ... else ...).

Questo è solo un altro aspetto da guardare per rendere il codice più facile da lavorare. Consente inoltre quando si desidera aggiungere un altro metodo da chiamare o modificare il metodo appropriato che deve essere chiamato in determinate situazioni.

Piuttosto che dover sostituire tutte le menzioni del metodo (e forse mancare alcuni scenari), sono tutti elencati alla fine del blocco if ... else ... e sono più semplici da leggere e modificare. Tendo a usarlo quando, ad esempio, possono essere chiamati diversi metodi, ma all'interno del nidificato se ... altrimenti ... un metodo può essere chiamato in più corrispondenze.

Se imposti una variabile che definisce lo stato, potresti avere molte opzioni profondamente annidate e aggiornare lo stato quando qualcosa deve essere (o non essere) eseguito.

Questo potrebbe essere usato come nell'esempio posto nella domanda in cui stiamo verificando se si è verificato "DoSomething" e, in caso contrario, eseguiamo l'azione predefinita. Oppure potresti avere lo stato per ogni metodo che potresti voler chiamare, impostare quando applicabile, quindi chiamare il metodo applicabile al di fuori di if ... else ...

Alla fine delle istruzioni nidificate if ... else ..., controlli lo stato e agisci di conseguenza. Ciò significa che è necessaria una sola menzione di un metodo invece di tutte le posizioni in cui dovrebbe essere applicato.

bool ActionDone = false;

if (Method_1(object_A)) // Test 1
{
    result_A = Method_2(object_A); // Result 1

    if (Method_3(result_A)) // Test 2
    {
        Method_4(result_A); // Action 1
        ActionDone = true;
    }
}

if (!ActionDone)
{
    Method_5(); // Default Action
}

0

Per ridurre l'IF nidificato:

1 / rientro anticipato;

2 / espressione composta (a corto circuito)

Quindi, il tuo esempio potrebbe essere refactored in questo modo:

if( FileExists(file) && SomeTest(contents = OpenFile(file)) )
{
    DoSomething(contents);
    return;
}
DefaultAction();

0

Ho visto molti esempi con "return" che uso anch'io, ma a volte voglio evitare di creare nuove funzioni e utilizzare invece un loop:

while (1) {
    if (FileExists(file)) {
        contents = OpenFile(file);
        if (SomeTest(contents)) {
           DoSomething(contents);
           break;
        } 
    }
    DefaultAction();
    break;
}

Se vuoi scrivere meno righe o odi loop infiniti come me, puoi cambiare il tipo di loop in "do ... while (0)" ed evitare l'ultima "interruzione".


0

Che ne dici di questa soluzione:

content = NULL; //I presume OpenFile returns a pointer 
if(FileExists(file))
    contents = OpenFile(file);
if(content != NULL && SomeTest(contents))
    DoSomething(contents);
else
    DefaultAction();

Ho assunto il presupposto che OpenFile restituisca un puntatore, ma questo potrebbe funzionare anche con il ritorno del tipo di valore specificando un valore predefinito non restituibile (codici di errore o qualcosa del genere).

Ovviamente non mi aspetto alcuna azione possibile tramite il metodo SomeTest sul puntatore NULL (ma non lo sapete mai), quindi questo potrebbe anche essere visto come un controllo extra per il puntatore NULL per la chiamata SomeTest (indice).


0

Chiaramente, la soluzione più elegante e concisa è usare una macro di preprocessore.

#define DOUBLE_ELSE(CODE) else { CODE } } else { CODE }

Che ti permette di scrivere un bellissimo codice come questo:

if(FileExists(file))
{
    contents = OpenFile(file);
    if(SomeTest(contents))
    {
        DoSomething(contents);
    }
    DOUBLE_ELSE(DefaultAction();)

Potrebbe essere difficile fare affidamento sulla formattazione automatica se si utilizza questa tecnica spesso e alcuni IDE potrebbero gridarti un po 'su ciò che erroneamente suppone sia malformato. E come dice il proverbio, tutto è un compromesso, ma suppongo che non sia un prezzo cattivo da pagare per evitare i mali del codice ripetuto.


Per alcune persone, e in alcune lingue, le macro del preprocessore sono codice malvagio :)
Benjol,

@ Benjol Hai detto di essere aperto a suggerimenti malvagi, no? ;)
Peter Olson l'

sì, assolutamente, è stato appena scritto il tuo "evitare i mali" :)
Benjol,

4
È così orribile, ho dovuto solo
votarlo

Shirley, non sei serio !!!!!!
Jim In Texas,

-1

Dal momento che hai chiesto per curiosità e la tua domanda non è taggata con una lingua specifica (anche se è chiaro che avevi in ​​mente lingue imperative), potrebbe valere la pena aggiungere che le lingue che supportano la valutazione lenta consentono un approccio completamente diverso. In quelle lingue, le espressioni vengono valutate solo quando necessario, quindi è possibile definire "variabili" e usarle solo quando ha senso farlo. Ad esempio, in un linguaggio immaginario con pigri let/ instrutture dimentichi il controllo del flusso e scrivi:

let
  contents = ReadFile(file)
in
  if FileExists(file) && SomeTest(contents) 
    DoSomething(contents)
  else 
    DefaultAction()
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.