Il filtro foreach loop con una condizione where vs continue clausole di guardia


24

Ho visto alcuni programmatori usare questo:

foreach (var item in items)
{
    if (item.Field != null)
        continue;

    if (item.State != ItemStates.Deleted)
        continue;

    // code
}

invece di dove userei normalmente:

foreach (var item in items.Where(i => i.Field != null && i.State != ItemStates.Deleted))
{
    // code
}

Ho anche visto una combinazione di entrambi. Mi piace molto la leggibilità con 'continue', soprattutto con condizioni più complesse. C'è persino una differenza nelle prestazioni? Con una query di database presumo ci sarebbe. E gli elenchi regolari?


3
Per le liste regolari sembra una micro ottimizzazione.
apocalisse,

2
@zgnilec: ... ma in realtà quale delle due varianti è la versione ottimizzata? Ho un'opinione a riguardo, ovviamente, ma dal solo guardare il codice questo non è intrinsecamente chiaro per tutti.
Doc Brown,

2
Il corso continuerà sarà più veloce. Utilizzando linq. Dove si crea iteratore aggiuntivo.
apocalisse

1
@zgnilec - Buona teoria. Ti interessa pubblicare una risposta che spieghi perché lo pensi? Entrambe le risposte attualmente esistenti dicono il contrario.
Bobson,

2
... quindi la linea di fondo è: le differenze di prestazioni tra i due costrutti sono trascurabili e sia la leggibilità che la debuggabilità possono essere raggiunte per entrambi. È semplicemente una questione di gusti quale preferisci.
Doc Brown,

Risposte:


64

Vorrei considerare questo come un luogo appropriato per utilizzare la separazione comando / query . Per esempio:

// query
var validItems = items.Where(i => i.Field != null && i.State != ItemStates.Deleted);
// command
foreach (var item in validItems) {
    // do stuff
}

Ciò consente anche di assegnare un buon nome di auto-documentazione al risultato della query. Ti aiuta anche a vedere le opportunità per il refactoring, perché è molto più semplice eseguire il refactoring del codice che interroga solo i dati o muta solo i dati rispetto al codice misto che tenta di fare entrambi.

Durante il debug, è possibile interrompere prima foreachper verificare rapidamente se il contenuto della validItemsrisoluzione è quello previsto. Non devi entrare nella lambda a meno che non sia necessario. Se hai bisogno di entrare nella lambda, ti suggerisco di fattorizzarlo in una funzione separata, quindi passaci attraverso.

C'è una differenza nelle prestazioni? Se la query è supportata da un database, la versione di LINQ ha il potenziale per essere eseguita più velocemente, poiché la query SQL potrebbe essere più efficiente. Se si tratta di LINQ to Objects, non vedrai alcuna reale differenza di prestazioni. Come sempre, profila il tuo codice e correggi i colli di bottiglia effettivamente segnalati, piuttosto che cercare di prevedere in anticipo le ottimizzazioni.


1
Perché un set di dati estremamente grande può fare la differenza? Solo perché il costo minuscolo degli lambda alla fine si sommerebbe?
BlueRaja - Danny Pflughoeft

1
@ BlueRaja-DannyPflughoeft: Sì, hai ragione, questo esempio non comporta alcuna complessità algoritmica aggiuntiva oltre al codice originale. Ho rimosso la frase.
Christian Hayter,

Ciò non comporta due iterazioni sulla raccolta? Naturalmente, il secondo è più corto, considerando che al suo interno sono presenti solo gli elementi validi, ma è ancora necessario farlo due volte, una volta per filtrare gli elementi, la seconda volta per lavorare con gli elementi validi.
Andy,

1
@DavidPacker No. Il driver IEnumerableè guidato solo dal foreachloop.
Benjamin Hodgson,

2
@DavidPacker: è esattamente quello che fa; la maggior parte dei metodi LINQ to Objects sono implementati usando blocchi iteratori. Il codice di esempio sopra eseguirà l'iterazione della raccolta esattamente una volta, eseguendo Wherelambda e il corpo del ciclo (se lambda restituisce true) una volta per elemento.
Christian Hayter,

7

Naturalmente c'è una differenza nelle prestazioni, il .Where()risultato è una chiamata delegata per ogni singolo elemento. Tuttavia, non mi preoccuperei affatto delle prestazioni:

  • I cicli di clock utilizzati nell'invocazione di un delegato sono trascurabili rispetto ai cicli di clock utilizzati dal resto del codice che scorre sulla raccolta e verifica le condizioni.

  • La penalità prestazionale nell'invocare un delegato è dell'ordine di alcuni cicli di clock e, fortunatamente, passiamo molto tempo ai giorni in cui dovevamo preoccuparci dei singoli cicli di clock.

Se per qualche motivo le prestazioni sono davvero importanti per te a livello di ciclo di clock, allora usa List<Item>invece di IList<Item>, in modo che il compilatore possa usare chiamate dirette (e inlinabili) invece di chiamate virtuali, e in modo che l'iteratore di List<T>, che è in realtà a struct, non deve essere inscatolato. Ma è roba davvero insignificante.

Una query di database è una situazione diversa, perché esiste (almeno in teoria) la possibilità di inviare il filtro all'RDBMS, migliorando così notevolmente le prestazioni: solo le righe corrispondenti faranno il viaggio dall'RDBMS al programma. Ma per questo penso che dovresti usare linq, non penso che questa espressione possa essere inviata al RDBMS così com'è.

Sarà davvero vedere i benefici di if(x) continue;questo momento è necessario eseguire il debug di questo codice: Single-scavalcando if()s e continues funziona bene; entrare nel delegato di filtraggio è una seccatura.


Questo è quando qualcosa non va e vuoi guardare tutti gli elementi e controllare nel debugger quali hanno Field! = Null e quali hanno State! = Null; questo potrebbe essere difficile o impossibile con foreach ... dove.
gnasher729,

Buon punto con il debug. Entrare in un dove non è poi così male in Visual Studio, ma non è possibile riscrivere le espressioni lambda durante il debug senza ricompilare e questo si evita quando si utilizzaif(x) continue; .
Paprik,

A rigor di termini, .Whereviene invocato solo una volta. Ciò che viene invocato ad ogni iterazione è il delegato filtro (e MoveNexteCurrent sulla enumeratore, quando essi non vengono ottimizzati out)
CodesInChaos

@CodesInChaos mi ci è voluto un po 'di pensiero per capire di cosa stai parlando, ma ovviamente wh00ps, hai ragione, a rigor di termini, .Whereviene invocato solo una volta. Aggiustato.
Mike Nakis,
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.