Inversione di un'istruzione IF


39

Quindi sto programmando da alcuni anni e recentemente ho iniziato a utilizzare ReSharper di più. Una cosa che ReSharper mi suggerisce sempre è di "invertire" l'istruzione if per ridurre l'annidamento ".

Diciamo che ho questo codice:

foreach (someObject in someObjectList) 
{
    if(someObject != null) 
    {
        someOtherObject = someObject.SomeProperty;
    }
}

E ReSharper mi suggerirà di fare questo:

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;
}

Sembra che ReSharper suggerirà SEMPRE di invertire i parametri IF, indipendentemente dalla quantità di annidamento in corso. Questo problema è che mi piace annidare in almeno alcune situazioni. A me sembra più facile leggere e capire cosa sta succedendo in certi luoghi. Questo non è sempre il caso, ma a volte mi sento più a mio agio a nidificare.

La mia domanda è: oltre alle preferenze personali o alla leggibilità, c'è un motivo per ridurre la nidificazione? C'è una differenza di prestazione o qualcos'altro di cui potrei non essere a conoscenza?


1
Possibile duplicato del livello di rientro basso
moscerino

24
La cosa chiave qui è "Suggeriti resharper". Dai un'occhiata al suggerimento, se facilita la lettura del codice ed è coerente, usalo. Fa la stessa cosa con for / per ogni loop.
Jon Raynor,

In genere abbasso quei suggerimenti di riaffilatura, non seguo sempre, quindi non vengono più visualizzati nella barra laterale.
CodesInChaos,

1
ReSharper ha rimosso il negativo, che di solito è una buona cosa. Tuttavia, non ha suggerito un condizionale ternario che potrebbe adattarsi bene qui, se il blocco è davvero semplice come il tuo esempio.
Dal

2
ReSharper non suggerisce anche di spostare il condizionale nella query? foreach (someObject in someObjectList.Where (object => object! = null)) {...}
Roman Reiner,

Risposte:


63

Dipende. Nel tuo esempio avere l' ifistruzione non invertita non è un problema, perché il corpo dell'elemento ifè molto breve. Comunque di solito vuoi invertire le affermazioni quando i corpi crescono.

Siamo solo umani e tenere in mente più cose alla volta è difficile.

Quando il corpo di un'affermazione è grande, capovolgere l'affermazione non solo riduce l'annidamento, ma dice anche allo sviluppatore che possono dimenticare tutto quanto sopra quell'affermazione e concentrarsi solo sul resto. È molto probabile che tu usi le istruzioni invertite per sbarazzarti di valori errati il ​​prima possibile. Una volta che sai che sono fuori dal gioco, l'unica cosa che rimane sono i valori che possono essere effettivamente elaborati.


64
Adoro vedere l'andamento dell'opinione degli sviluppatori in questo modo. C'è così tanto di nidificazione nel codice semplicemente perché non di una straordinaria illusione popolare che break, continuee più returnnon sono strutturati.
JimmyJames,

6
il problema è la rottura, ecc. sono ancora i flussi di controllo che devi tenere nella testa "questo non può essere x o sarebbe uscito dal ciclo in alto" potrebbe non aver bisogno di ulteriori considerazioni se si tratta di un controllo nullo che provocherebbe comunque un'eccezione. Ma non è così bello quando è più sfumato "esegui solo metà di questo codice per questo status il giovedì"
Ewan,

2
@Ewan: Qual è la differenza tra 'questo non può essere x o sarebbe uscito dal ciclo in alto' e 'questo non può essere x o non sarebbe entrato in questo blocco'?
Collin,

nulla è la complessità e il significato della x che è importante
Ewan,

4
@Ewan: penso che break/ continue/ returnabbia un vero vantaggio rispetto a un elseblocco. Con le uscite anticipate, ci sono 2 blocchi : il blocco per uscire e il blocco per rimanere lì; con elseci sono 3 blocchi : if, else, e tutto ciò che viene dopo (che viene utilizzato qualunque sia il ramo preso). Preferisco avere meno blocchi.
Matthieu M.,

33

Non evitare i negativi. Gli umani fanno schifo mentre li analizzano anche quando la CPU li adora.

Tuttavia, rispetta i modi di dire. Noi umani siamo creature abituali, quindi preferiamo percorsi ben usurati a percorsi diretti pieni di erbacce.

foo != nullpurtroppo è diventato un linguaggio. Riesco a malapena a notare le parti separate. Lo guardo e penso solo, "oh, è sicuro di lasciar perdere fooadesso".

Se vuoi ribaltarlo (oh, un giorno, per favore) hai bisogno di un caso davvero forte. Hai bisogno di qualcosa che mi lasci sfuggire gli occhi senza sforzo e lo capisca in un istante.

Tuttavia, ciò che ci hai dato è stato questo:

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;
}

Che non è nemmeno strutturalmente lo stesso!

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;

    if(someGlobalObject == null) continue;
    someSideEffectObject = someGlobalObject.SomeProperty;
}

Questo non sta facendo ciò che il mio cervello pigro si aspettava che facesse. Pensavo di poter continuare ad aggiungere codice indipendente ma non posso. Questo mi fa pensare a cose a cui non voglio pensare ora. Hai rotto il mio linguaggio ma non me ne hai dato uno migliore. Tutto perché volevi proteggermi dall'unico aspetto negativo che ha bruciato il mio piccolo sé cattivo nella mia anima.

Mi dispiace, forse ReSharper ha ragione a suggerire questo 90% delle volte, ma con controlli nulli penso che tu abbia trovato un caso d'angolo in cui è meglio ignorarlo. Gli strumenti semplicemente non sono bravi a insegnarti cos'è il codice leggibile. Chiedi a un essere umano. Fai una revisione tra pari. Ma non seguire ciecamente lo strumento.


1
Bene, se hai più codice nel ciclo, il modo in cui funziona dipende da come tutto si riunisce. Non è un codice indipendente. Il codice refactored nella domanda era corretto per l'esempio, ma potrebbe non essere corretto quando non in isolamento - era questo il punto che stavi cercando? (+1 per il non evitare i negativi però)
Baldrickk,

@Baldrickk if (foo != null){ foo.something() }mi permette di continuare ad aggiungere codice senza preoccuparmi se foofosse nullo. Questo no. Anche se non ho bisogno di farlo ora, non mi sento motivato a rovinare il futuro dicendo "beh, possono semplicemente rifattorizzarlo se ne hanno bisogno". Feh. Il software è morbido solo se è facile da cambiare.
candied_orange,

4
"continua ad aggiungere il codice senza preoccuparti" O il codice dovrebbe essere correlato (altrimenti, probabilmente dovrebbe essere separato) o ci dovrebbe essere cura di capire la funzionalità della funzione / blocco che stai modificando poiché l'aggiunta di qualsiasi codice ha il potenziale per cambiare comportamento oltre quello previsto. Per casi semplici come questo, non credo sia importante in entrambi i casi. Per i casi più complessi, mi aspetterei che la comprensione del codice abbia la precedenza, quindi sceglierei quello che ritengo più chiaro, che potrebbe essere uno dei due stili.
Baldrickk,

correlati e intrecciati non sono la stessa cosa.
candied_orange,

1
Non evitare i negativi: D
VitalyB,

20

È il vecchio argomento sul fatto che una pausa sia peggio di nidificazione.

Le persone sembrano accettare il ritorno anticipato per i controlli dei parametri, ma è disapprovato nella maggior parte di un metodo.

Personalmente evito di continuare, rompere, andare ecc. Anche se ciò significa più nidificazione. Ma nella maggior parte dei casi puoi evitare entrambi.

foreach (someObject in someObjectList.where(i=>i != null))
{
    someOtherObject = someObject.SomeProperty;
}

Ovviamente la nidificazione rende le cose difficili da seguire quando si hanno molti livelli

foreach (someObject in someObjectList) 
{
    if(someObject != null) 
    {
        someOtherObject = someObject.SomeProperty;
        if(someObject.x > 5) 
        {
            x ++;
            if(someObject.y > 5) 
            {
                x ++;
            }
        }
    }
}

Il peggio è quando hai entrambi

foreach (someObject in someObjectList) 
{
    if(someObject != null) 
    {
        someOtherObject = someObject.SomeProperty;
        if(someObject.x > 5) 
        {
            x ++;
            if(someObject.y > 5) 
            {
                x ++;
                return;
            }
            continue;
        }
        someObject.z --;
        if(myFunctionWithSideEffects(someObject) > 6) break;
    }
}

11
Adoro questa foreach (subObject in someObjectList.where(i=>i != null))frase : non so perché non ho mai pensato a qualcosa del genere.
Barry Franklin,

@BarryFranklin ReSharper dovrebbe avere una sottolineatura tratteggiata verde sulla foreach con "Converti in espressione LINQ" come suggerimento che lo farebbe per te. A seconda dell'operazione, potrebbe eseguire anche altri metodi linq come Sum o Max.
Kevin Fee,

@BarryFranklin È probabilmente un suggerimento che vuoi impostare a un livello basso e usare solo discrezionalmente. Mentre funziona bene per casi banali come questo esempio, trovo in un codice più complesso il modo in cui raccoglie parti condizionali più complesse in bit che sono Linqifiable e il corpo rimanente tende a creare cose che sono molto più difficili da comprendere rispetto all'originale. In genere proverò almeno il suggerimento perché quando può convertire un intero ciclo in Linq i risultati sono spesso ragionevoli; ma metà e metà dei risultati tendono a diventare brutti e veloci IMO.
Dan Neely,

Ironia della sorte, la modifica di resharper ha esattamente lo stesso livello di annidamento, perché ha anche un if seguito da una riga.
Peter,

heh, nessuna parentesi però! che è apparentemente permesso: /
Ewan,

6

Il problema continueè che se aggiungi più righe al codice la logica diventa complicata e probabilmente contiene bug. per esempio

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;

    ...  imagine that there is some code here ...

    // added later by a Junior Developer
    doSomeOtherFunction(someObject);
}

Vuoi chiamare doSomeOtherFunction()per tutti i someObject, o solo se non nullo? Con il codice originale hai l'opzione ed è più chiaro. Con quello continuesi potrebbe dimenticare "oh, sì, laggiù abbiamo saltato i null" e non hai nemmeno la possibilità di chiamarlo per tutti alcuni oggetti, a meno che tu non metta quella chiamata all'inizio del metodo che potrebbe non essere possibile o corretta per altri motivi.

Sì, un sacco di nidificazione è brutto e probabilmente confuso, ma in questo caso userei lo stile tradizionale.

Domanda bonus: perché all'inizio ci sono dei null in quell'elenco? Potrebbe essere meglio non aggiungere mai un null in primo luogo, nel qual caso la logica di elaborazione è molto più semplice.


12
Non dovrebbe esserci abbastanza codice all'interno del ciclo da non poter vedere il controllo null nella parte superiore senza scorrere.
Bryan Boettcher,

@Bryan Boettcher ha concordato, ma non tutto il codice è scritto così bene. E anche diverse righe di codice complesso potrebbero far dimenticare (o sbagliare) il proseguimento. In pratica sto bene con returns causa che formano un "taglio netto", ma IMO breake continueportare a confusione e, nella maggior parte dei casi, è meglio evitare.
user949300,

4
@ user949300 Dopo dieci anni di sviluppo, non ho mai trovato interruzioni o continui a creare confusione. Li ho coinvolti nella confusione ma non sono mai stati la causa. Di solito, le decisioni sbagliate sullo sviluppatore erano la causa e avrebbero causato confusione, indipendentemente dall'arma che usavano.
corsiKa

1
@corsiKa Il bug SSL di Apple è in realtà un bug goto, ma potrebbe facilmente essere stato un errore di interruzione o continuare. Tuttavia il codice è orribile ...
user949300

3
@BryanBoettcher il problema mi sembra che gli stessi sviluppatori che pensano che continuino o che i rendimenti multipli siano ok anche che seppellirli in corpi di metodo di grandi dimensioni sia anche ok. Quindi ogni esperienza che ho con continui / multipli ritorni è stata in enormi pasticci di codice.
Andy,

3

L'idea è il ritorno anticipato. All'inizio returndi un ciclo diventa presto continue.

Molte buone risposte a questa domanda: dovrei tornare presto da una funzione o usare un'istruzione if?

Si noti che mentre la domanda è chiusa in base all'opinione, 430+ voti sono a favore del ritorno anticipato, ~ 50 voti sono "dipende" e 8 sono contrari al ritorno anticipato. Quindi c'è un consenso eccezionalmente forte (o quello, o sono cattivo nel contare, il che è plausibile).

Personalmente, se il corpo del loop sarebbe soggetto a continui precoci e abbastanza a lungo da importare (3-4 righe), tendo ad estrarre il corpo del loop in una funzione in cui uso il ritorno anticipato invece di continuare anticipatamente.


2

Questa tecnica è utile per certificare le condizioni preliminari quando si verifica la maggior parte del metodo quando tali condizioni sono soddisfatte.

Spesso invertiamo il ifsmio lavoro e assumiamo un altro fantasma. Era abbastanza frequente che durante la revisione di un PR potessi sottolineare come il mio collega potesse rimuovere tre livelli di annidamento! Succede meno frequentemente dal momento che lanciamo i nostri if.

Ecco un esempio giocattolo di qualcosa che può essere lanciato

function foobar(x, y, z) {
    if (x > 0) {
        if (z != null && isGamma(y)) {
            // ~10 lines of code
            // return something
        }
        else {
           return y
        }
    }
    else {
      return y + z
    }
}

Codice come questo, dove c'è UN caso interessante (dove il resto sono semplicemente ritorni o piccoli blocchi di istruzioni) sono comuni. È banale riscrivere quanto sopra come

function foobar(x, y, z) {
    if (x <=0) {
        return y + z
    }
    if (z == null || !isGamma(y) {
       return y
    }
    // ~ 10 lines of code
    // return something
}

Qui, il nostro codice significativo, le 10 righe non incluse, non ha un triplo rientro nella funzione. Se hai il primo stile, o sei tentato di trasformare il blocco lungo in un metodo o tollerare il rientro. È qui che entrano in gioco i risparmi. In un certo senso si tratta di un banale risparmio, ma attraverso molti metodi in molte classi, si risparmia la necessità di generare una funzione aggiuntiva per rendere il codice più pulito e non si ha un livello di orrore a più livelli rientro .


In risposta all'esempio di codice di OP, concordo sul fatto che si tratta di un collasso eccessivo. Soprattutto dal momento che in 1 TB, lo stile che uso, l'inversione fa aumentare le dimensioni del codice.


0

I suggerimenti di Resharpers sono spesso utili ma devono sempre essere valutati in modo critico. Cerca alcuni schemi di codice predefiniti e suggerisce trasformazioni, ma non può prendere in considerazione tutti i fattori che rendono il codice più o meno leggibile per l'uomo.

A parità di altre condizioni, è preferibile un numero minore di annidamenti, motivo per cui ReSharper suggerirà una trasformazione che riduca l'annidamento. Ma tutte le altre cose non sono uguali: in questo caso la trasformazione richiede l'introduzione di a continue, il che significa che il codice risultante è effettivamente più difficile da seguire.

In alcuni casi, lo scambio di un livello di annidamento con a continue potrebbe effettivamente essere un miglioramento, ma ciò dipende dalla semantica del codice in questione, che va oltre la comprensione delle regole meccaniche di ReSharpers.

Nel tuo caso il suggerimento ReSharper peggiorerebbe il codice. Quindi ignoralo. O meglio: non usare la trasformazione suggerita, ma rifletti su come migliorare il codice. Si noti che è possibile assegnare più volte la stessa variabile, ma solo l'ultima assegnazione avrà effetto, poiché la precedente verrebbe semplicemente sovrascritta. Questo fa apparire come un insetto. Il suggerimento di ReSharpers non risolve il bug, ma rende un po 'più ovvio che sta succedendo qualche logica dubbia.


0

Penso che il punto più grande qui sia che lo scopo del loop impone il modo migliore di organizzare il flusso all'interno del loop. Se stai filtrando alcuni elementi prima di passare in rassegna i rimanenti, allora continueha senso uscire presto. Tuttavia, se stai eseguendo diversi tipi di elaborazione all'interno di un ciclo, potrebbe avere più senso renderlo ifdavvero ovvio, come ad esempio:

for(obj in objectList) {
    if(obj == null) continue;
    if(obj.getColour().equals(Color.BLUE)) {
        // Handle blue objects
    } else {
        // Handle non-blue objects
    }
}

Si noti che in Java Streams o altri modi di dire funzionali, è possibile separare il filtro dal looping utilizzando:

items.stream()
    .filter(i -> (i != null))
    .filter(i -> i.getColour().equals(Color.BLUE))
    .forEach(i -> {
        // Process blue objects
    });

Per inciso, questa è una delle cose che amo di Perl's invertito se ( unless) applicato a singole istruzioni, che consente il seguente tipo di codice, che trovo molto facile da leggere:

foreach my $item (@items) {
    next unless defined $item;
    carp "Only blue items are supported! Failed on item $item" 
       unless ($item->get_colour() eq 'blue');
    ...
}
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.