Anti-for-if antipattern


9

Stavo leggendo in questo post sul blog l'anti-pattern for-if, e non sono abbastanza sicuro di capire perché sia ​​un anti-pattern.

foreach (string filename in Directory.GetFiles("."))
{
    if (filename.Equals("desktop.ini", StringComparison.OrdinalIgnoreCase))
    {
        return new StreamReader(filename);
    }
}

Domanda 1:

È a causa del return new StreamReader(filename);dentro for loop? o il fatto che non hai bisogno di un forciclo in questo caso?

Come ha sottolineato l'autore del blog, la versione meno folle di questo è:

if (File.Exists("desktop.ini"))
{
    return new StreamReader("desktop.ini");
} 

entrambi soffrono di una condizione di competizione perché se il file viene eliminato prima della creazione di StreamReader, otterrai un File­Not­Found­Exception.

Domanda 2:

Per correggere il secondo esempio, lo riscriveresti senza l'istruzione if, e invece lo circonderesti StreamReadercon un blocco try-catch, e se lancia un File­Not­Found­Exceptionlo gestisci nel catchblocco di conseguenza?



1
@amon - Grazie, ma non faccio alcun punto aggiuntivo, sto solo cercando di capire cosa dice l'articolo e come lo correggerei.

3
Non ci penserei tanto. Il poster del blog sta inventando un codice idiota che nessuno scrive, gli dà un nome e lo chiama un anti-schema. Eccone un altro: "Lo chiamo il modello di assegnazione inutile: x = x; vedo che questo è fatto tutto il tempo. Così stupido. Penso che scriverò un blog al riguardo."
Martin Maat,

4
To fix the second example, would you re-write it without the if statement, and instead surround the StreamReader with a try-catch block, and if it throws a File­Not­Found­Exception you handle it in the catch block accordingly?- Sì, è esattamente quello che vorrei fare. Risolvere le condizioni della razza è più importante di alcune nozioni di "eccezioni come flusso di controllo" e le risolve in modo elegante e pulito.
Robert Harvey,

1
Cosa fa se nessuno dei file soddisfa i criteri indicati? Ritorna null? Di solito puoi usare LINQ per ripulire il codice che assomiglia al codice del tuo esempio: return Directory.GetFiles(".").FirstOrDefault(fileName => fileName.Equals("desktop.ini", StringComparison.OrdinalIgnoreCase))?.Select(fileName => new StreamReader(filename)); nota l' ?.operatore tra le due chiamate LINQ. Inoltre, le persone potrebbero obiettare che la creazione di oggetti come questo non è l'uso più appropriato di LINQ, ma penso che qui vada bene. Questa non è una risposta alla tua domanda, ma sta divagando su una parte di essa.
Panzercrisis,

Risposte:


7

Questo è un antipasto in quanto prende la forma di:

loop over a set of values
   if current value meets a condition
       do something with value
   end
end

e può essere sostituito con

do something with value

Un classico esempio di questo è il codice come:

for (var i=0; i < 5; i++)
{
    switch (i)
        case 1:
            doSomethingWith(1);
            break;
        case 2:
            doSomethingWith(2);
            break;
        case 3:
            doSomethingWith(4);
            break;
        case 4:
            doSomethingWith(4);
            break;
    }
}

Quando il seguente funziona bene:

doSomethingWith(1);
doSomethingWith(2);
doSomethingWith(3);
doSomethingWith(4);

Se ti ritrovi a eseguire un loop e un ifo switch, quindi fermati e pensa a quello che stai facendo. Stai complicando le cose e l'intero ciclo e il test possono essere sostituiti con una semplice riga "Fallo". A volte, però, scoprirai che devi fare quel ciclo (più di un oggetto potrebbe corrispondere all'unica condizione, per esempio), nel qual caso il modello va bene.

Ecco perché è un anti-modello: prende il modello "loop and test" e lo abusa.

Per quanto riguarda la tua seconda domanda: sì. Un modello "prova a fare" è più robusto di un modello "prova quindi fai" in qualsiasi situazione in cui il tuo codice non è l'unico thread sull'intero dispositivo che può cambiare lo stato dell'elemento in prova.

Il problema con questo codice:

if (File.Exists("desktop.ini"))
{
    return new StreamReader("desktop.ini");
}

è che nel periodo tra il File.Existse il StreamReadertentativo di aprire il file, un altro thread o processo potrebbe eliminare il file. Quindi otterrai un'eccezione. Pertanto, l'eccezione deve essere protetta da qualcosa come:

try
{
    return new StreamReader("desktop.ini");
}
catch (File­Not­Found­Exception)
{
    return null; // or whatever
}

@Flater solleva un buon punto? Questo è di per sé un antipasto? Stiamo usando le eccezioni come flusso di controllo?

Se il codice legge qualcosa del tipo:

try
{
    if (!File.Exists("desktop.ini")
    {
        throw new IniFileMissingException();
        return new StreamReader("desktop.ini");
    }
}
catch (IniFileMissingException)
{
    return null;
}

allora userei davvero le eccezioni come un goto glorificato e sarebbe davvero un anti-schema. Ma in questo caso stiamo solo aggirando il comportamento indesiderato della creazione di un nuovo flusso, quindi questo non è un esempio di tale anti-pattern. Ma è un esempio di come aggirare quel modello.

Ovviamente, ciò che vogliamo davvero è un modo più elegante di creare il flusso. Qualcosa di simile a:

return TryCreateStream("desktop.ini", out var stream) ? stream : null;

E consiglierei di racchiudere quel try catchcodice in un metodo di utilità come questo se ti ritrovi a usare molto questo codice.


1
@Flater, tendo a concordare sul fatto che non è l'ideale (anche se contesto che sia un esempio dell'anti-pattern di cui parla la risposta). Il codice dovrebbe mostrare il suo intento, non la meccanica. Quindi preferirei qualcosa come Scala Try, ad esempio return Try(() => new StreamReader("desktop.ini")).OrElse(null);, ma C # non supporta quel costrutto (senza usare una libreria di terze parti), quindi dobbiamo lavorare con la versione goffa.
David Arno,

1
@Flater, concordo pienamente sul fatto che le eccezioni dovrebbero essere eccezionali. Ma lo sono raramente. Ad esempio, un file inesistente non è certo eccezionale, ma ne File.Opengenererà uno se non riesce a trovare il file. Poiché è già stata generata un'eccezione non eccezionale, è necessario intrappolarla come parte del flusso di controllo (a meno che non si desideri semplicemente interrompere l'app).
David Arno,

1
@Flater: il secondo esempio di codice è il modo giusto per aprire il file. Qualsiasi altra cosa introduce una condizione di razza. Anche se controlli l'esistenza dei file, tra quel controllo e quando lo apri effettivamente, qualcosa potrebbe rendere quel file non disponibile per te e la chiamata Open () esploderà. Quindi devi essere pronto per l'eccezione comunque. Quindi potresti anche non preoccuparti di controllare e provare ad aprirlo. Le eccezioni sono strumenti per aiutarci a fare cose e il loro uso non dovrebbe essere visto come dogma religioso.
whatsisname

1
@Flater: qualsiasi modo per evitare di provare / catturare le operazioni sui file introduce le condizioni di gara. Il file system in cui si desidera scrivere può diventare non disponibile nell'istante prima di chiamare File.Create, rendendo non valido qualsiasi controllo.
whatsisname

1
@Flater " Stai effettivamente sostenendo che ogni chiamata di metodo ha bisogno di un tipo di ritorno ... che sta effettivamente cercando di reinventare le eccezioni in modo diverso ". Corretta. Sebbene quella reinvenzione non sia mia. È stato usato nei linguaggi funzionali per anni. In genere evitano tutte le "eccezioni come problema del flusso di controllo" (che confusamente si afferma sia un anti-pattern in un respiro e poi si discute a favore di quello successivo) usando i tipi di unione, ad esempio in questo caso si userebbe un Maybe<Stream>tipo che ritorna nothingse il file non esiste e un flusso se lo è.
David Arno,

1

Domanda 1: (questo for-loop è un antipattern)

Sì, perché non è necessario eseguire la propria ricerca di elementi archiviati in un sottosistema interrogabile.

In astratto, il file system, come un database, è in grado di rispondere alle query. Quando interagiamo con un sottosistema interrogabile, abbiamo una scelta fondamentale se vogliamo che un tale sottosistema ci enumeri il suo contenuto in modo da eseguire la corrispondenza all'esterno del sottosistema o utilizzare le funzionalità di query native del sottosistema.

Facciamo finta per un momento che stai cercando un record in un database anziché un file in una directory in un file system. Preferiresti vedere

SELECT * FROM SomeTable;

e quindi in loop (ad es. in C #) sopra il cursore restituito cercando ID = 100 o lasciando che il sottosistema interrogabile faccia il possibile per trovare esattamente quello che stai cercando?

SELECT * FROM SomeTable WHERE ID = 100;

Dovrei pensare che la maggior parte di noi sceglierebbe giustamente di consentire al sottosistema di eseguire l'esatta query di interesse. L'alternativa prevede potenzialmente numerosi round trip con il sottosistema, test di uguaglianza inefficienti e rinunciare all'utilizzo di qualsiasi indice o altro acceleratore di ricerca, fornito sia da database che da file system.


Domanda 2: Per correggere il secondo esempio, lo riscriveresti senza l'istruzione if e invece circonderesti StreamReader con un blocco try-catch e se genera un FileNotFoundException lo gestirai nel blocco catch?

Sì, perché è così che funziona quella particolare API: non è davvero la nostra scelta poiché si tratta di una funzione di libreria. Il controllo if, prima della chiamata, non offre alcun valore aggiuntivo: dobbiamo comunque utilizzare try / catch, poiché (1) possono verificarsi altri errori oltre FileNotFound e (2) le condizioni della competizione.


0

Domanda 1:

È a causa della restituzione di nuovo StreamReader (nome file); all'interno del ciclo for? o il fatto che non hai bisogno di un ciclo for in questo caso?

Lo streamreader non ha nulla a che fare con esso. L'anti-pattern emerge a causa di un chiaro conflitto di intenzioni tra il foreache il if:

Qual è lo scopo di foreach?

Presumo che la tua risposta sarà qualcosa del tipo: "Voglio eseguire ripetutamente un particolare pezzo di codice"

Quanti file ti aspetti di elaborare?

Poiché è possibile avere un solo nome file specifico (inclusa l'estensione) in una determinata cartella, ciò dimostra che il codice è destinato a trovare un file applicabile.

Ciò è confermato anche dal fatto che stai restituendo immediatamente un valore. In realtà non ti importa di una seconda partita, anche se dovesse esistere.


Ci sono situazioni in cui questo non è un anti-schema.

  • Se si guarda anche nelle sottodirectory ( Directory.GetFiles(".", SearchOption.AllDirectories)), è possibile trovare più di un file con lo stesso nome file (inclusa l'estensione)
  • Se cerchi corrispondenze di nome file parziali (ad es. Ogni file il cui nome inizia con "Test_", o ogni "*.zip"file.

Nota che entrambi questi casi richiederebbero di elaborare effettivamente più corrispondenze e quindi di non restituire immediatamente un valore.


Domanda 2:

Per correggere il secondo esempio, lo riscriveresti senza l'istruzione if e invece circonderesti StreamReader con un blocco try-catch e se genera un FileNotFoundException lo gestirai nel blocco catch di conseguenza?

Le eccezioni sono costose. Non dovrebbero mai essere usati al posto della corretta logica di flusso. Le eccezioni sono, poiché il loro nome suggerisce circostanze eccezionali .

Per questo motivo, non dovresti rimuovere il file if.

Secondo questa risposta su SoftwareEngineering.SE :

Generalmente, l'uso delle eccezioni per il flusso di controllo è un modello anti-pattern, con notevoli tosse con eccezioni specifiche per la situazione e la lingua.

Come una breve sintesi del perché, generalmente, è un anti-pattern:

  • Le eccezioni sono, in sostanza, sofisticate dichiarazioni GOTO
  • La programmazione con eccezioni, quindi, rende più difficile leggere e comprendere il codice
  • La maggior parte delle lingue ha strutture di controllo esistenti progettate per risolvere i tuoi problemi senza l'uso di eccezioni
  • Gli argomenti per l'efficienza tendono ad essere discutibili per i compilatori moderni, che tendono ad ottimizzare ipotizzando che le eccezioni non vengano utilizzate per il flusso di controllo.

Leggi la discussione sul wiki di Ward per informazioni molto più approfondite.

Se hai bisogno di avvolgerlo in un tentativo / cattura, dipende fortemente dalla tua situazione:

  • Quanto è probabile che si verifichi una condizione di gara?
  • Sei effettivamente in grado di gestire questa situazione o vuoi che questo problema si risolva in una bolla perché non sai come gestirla.

Nulla è mai questione di "usarlo sempre". Per dimostrare il mio punto:

Studi separati hanno dimostrato che è meno probabile che ti faccia male quando indossi un casco di sicurezza, occhiali di sicurezza e giubbotti antiproiettile.

Quindi perché non indossiamo sempre questa attrezzatura di sicurezza?

La semplice risposta è perché ci sono degli svantaggi nell'indossarli:

  • Costa denaro
  • Rende il tuo movimento più ingombrante
  • Può essere abbastanza caldo da indossare.

Ora stiamo arrivando da qualche parte: ci sono pro e contro . In altre parole, ha senso indossare questa attrezzatura solo nei casi in cui i professionisti superano i contro.

  • Gli operai edili hanno molte più probabilità di subire un infortunio durante il loro lavoro. Beneficiano di un casco di sicurezza.
  • D'altro canto, gli impiegati hanno una probabilità molto più bassa di ferirsi. I caschi di sicurezza non ne valgono la pena.
  • Un membro del team SWAT ha molte più probabilità di essere colpito, rispetto a un impiegato.

Dovresti concludere la chiamata in un tentativo / cattura? Ciò dipende molto dal fatto che i vantaggi di farlo superino i costi di attuazione.

Nota che altri potrebbero obiettare che bastano pochi tasti per avvolgerlo, quindi ovviamente dovrebbe essere fatto. Ma questo non è l'intero argomento:

  • Devi decidere cosa fare una volta rilevata l'eccezione.
  • Se ci sono molte chiamate diverse a file diversi in tutta la base di codice, decidere di racchiuderne uno in un tentativo / cattura significa generalmente che è necessario racchiudere tutti questi casi. Questo può avere un effetto drammatico sulla quantità di sforzo richiesto per implementarlo.
  • È perfettamente possibile che tu non voglia gestire un'eccezione intenzionalmente .
    • Si noti che l'applicazione dovrà gestire l'eccezione ad un certo punto, ma ciò non è necessariamente immediatamente dopo che l'eccezione è stata sollevata.

Quindi la scelta è tua. C'è un vantaggio nel farlo? Pensi che migliora l'applicazione, più che un dispendio di costi per implementarla?

Aggiornamento - Da un commento che ho scritto sull'altra risposta, poiché penso che sia una considerazione rilevante anche per te:

Dipende molto dal contesto circostante.

  • Se l'apertura dello streamreader è preceduta da if(!File.Exists) File.Create(), l'assenza del file all'apertura dello streamreader è davvero eccezionale .
  • Se il nome file è stato selezionato da un elenco di file esistenti, la sua improvvisa assenza è di nuovo eccezionale .
  • Se stai lavorando con una stringa che non hai ancora testato sulla directory; quindi l'assenza del file è un risultato perfettamente logico e quindi non eccezionale .
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.