2012-02-13 11 views
5

Qual è la procedura migliore per verificare se una raccolta contiene elementi?Come verificare correttamente IEnumerable per i risultati esistenti

Ecco un esempio di ciò che ho:

var terminalsToSync = TerminalAction.GetAllTerminals(); 

if(terminalsToSync.Any()) 
    SyncTerminals(terminalsToSync); 
else 
    GatewayLogAction.WriteLogInfo(Messages.NoTerminalsForSync); 

Procedimento GetAllTerminals() eseguirà una procedura memorizzata e, se ci ritorno conseguenza, (Any() è true), SyncTerminals() volontà scorrere gli elementi; quindi enumerandolo nuovamente ed eseguendo la stored procedure per la seconda volta.

Qual è il modo migliore per evitare questo?

Mi piacerebbe una buona soluzione che possa essere utilizzata anche in altri casi; possibilmente senza convertirlo in List.

Grazie in anticipo.

+0

perché è necessario eseguire la procedura di acquisto la seconda volta? Terminali ToSync contiene già il risultato SP vero? –

+0

Non ho bisogno, né voglio eseguirlo per la seconda volta. Quello che ho detto nella mia domanda è stato che poiché GetAllTerminals() restituisce IEnumerable entrambi Any() e foreach lo chiamerà e la procedura terminerà chiamata due volte. Mi piacerebbe evitare questo comportamento ed essere ancora in grado di verificare se ci sono delle righe. –

+0

Vedi Marc Gravell @ http://stackoverflow.com/questions/5047349/how-to-check-if-ienumerable-is-null-or-empty ?? – SpaceBison

risposta

3

Come su questo, che rinvia ancora l'esecuzione, ma tampona una volta eseguito:

var terminalsToSync = TerminalAction.GetAllTerminals().Lazily(); 

con:

public static class LazyEnumerable { 
    public static IEnumerable<T> Lazily<T>(this IEnumerable<T> source) { 
     if (source is LazyWrapper<T>) return source; 
     return new LazyWrapper<T>(source); 
    } 
    class LazyWrapper<T> : IEnumerable<T> { 
     private IEnumerable<T> source; 
     private bool executed; 
     public LazyWrapper(IEnumerable<T> source) { 
      if (source == null) throw new ArgumentNullException("source"); 
      this.source = source; 
     } 
     IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } 
     public IEnumerator<T> GetEnumerator() { 
      if (!executed) { 
       executed = true; 
       source = source.ToList(); 
      } 
      return source.GetEnumerator(); 
     } 
    } 
} 
+0

+1 È bello; e naturalmente generico, che il mio non era. –

+3

Questo non è affatto diverso dal chiamare ToList() però! – MattDavey

+1

@MattDavey l'unica differenza è ** quando ** viene invocato. In realtà, personalmente mi piace *** sapere quando avviene il mio accesso ai dati, quindi sì: sarei felice con un 'ToList()' –

5

Probabilmente userei una chiamata ToArray e quindi controllare Length; hai intenzione di enumerare comunque tutti i risultati, quindi perché non farlo in anticipo? Tuttavia, dal momento che hai detto che vuoi evitare di realizzazione in tempi rapidi della enumerabile ...

Sto indovinando che SyncTerminals ha un foreach, nel qual caso si può scrivere qualcosa di simile a questo:

bool any = false; 
foreach(var terminal in terminalsToSync) 
{ 
    if(!any)any = true; 
    //.... 
} 

if(!any) 
    GatewayLogAction.WriteLogInfo(Messages.NoTerminalsForSync); 

Ok, c'è un if ridondante dopo il primo ciclo, ma suppongo che il costo di un numero limitato di cicli della CPU non abbia molta importanza.

Allo stesso modo, è possibile eseguire l'iterazione alla vecchia maniera e utilizzare un ciclo do...while e GetEnumerator; prendendo la prima iterazione dal ciclo; in questo modo ci sono letteralmente nessuna operazione sprecati:

var enumerator = terminalsToSync.GetEnumerator(); 
if(enumerator.MoveNext()) 
{ 
    do 
    { 
    //sync enumerator.Current 
    } while(enumerator.MoveNext()) 
} 
else 
    GatewayLogAction.WriteLogInfo(Messages.NoTerminalsForSync); 
+0

@MattDavey - Certo che lo è; la soluzione migliore è usare 'ToArray()', che altre risposte hanno detto, non c'è davvero alcun argomento legittimo contro di esso. Ma sto fornendo una risposta entro le restrizioni fornite dalla domanda. –

+0

+1 per la prima frase - Non riesco a capire perché l'OP si oppone così a questo approccio. – MattDavey

+1

@MattDavey - sì; a meno che non supponga che enumerabile abbia un sacco e un sacco di oggetti di grandi dimensioni. Ma anche così è tenue. Forse è un Windows Mobile o qualcosa del genere. –

1

.Length o .Count è più veloce in quanto non ha bisogno di passare attraverso il GetEnumerator()/MoveNext()/Dispose() richieste da eventuali()

+0

+1 persone dicono spesso che Any() è più veloce perché cortocircuito, ma questo non è sempre il caso in quanto Count() può talvolta utilizzare la proprietà IList.Count se la raccolta è stata stabilizzata. La lunghezza non è disponibile su IEnumerable. – MattDavey

+0

afaik non è possibile chiamare Count (proprietà) su IEnumerable è possibile chiamare Count() metodo di estensione ma sarà più lento di Any() perché deve scorrere l'intera raccolta e contare il risultato quindi è sufficiente confrontarlo a 0 mentre Any() restituirà true dopo aver colpito il primo elemento. –

+0

Questo è vero solo se i risultati vengono prima copiati in un 'List' o in un' Array', naturalmente. @MattDavey: questo è il modo sbagliato di guardarlo. Se è necessario verificare se una raccolta ha ** qualsiasi ** elementi, è necessario utilizzare 'Qualsiasi'. Per 'IEnumerable',' Count() 'è un disastro completo, mentre per un' List', 'Any' controllerà semplicemente il suo conteggio e ritornerà immediatamente. Se il tuo codice è disaccoppiato, allora il tuo metodo che accetta un 'IEnumerable' non può sapere quale implementazione riceverà. – Groo

3

Personalmente non ne userei uno qui, foreach semplicemente non passerà attraverso gli oggetti se la collezione è vuota, quindi lo farei semplicemente in questo modo. Tuttavia, ti consiglio di verificare la presenza di null.

Se si vuole pre-indica l'utilizzazione set .ToArray() ad esempio, sarà enumerare solo una volta:

var terminalsToSync = TerminalAction.GetAllTerminals().ToArray(); 

if(terminalsToSync.Any()) 
    SyncTerminals(terminalsToSync); 
+1

+1 per ToArray(). Quando si lavora con gli enumerabili è importante sapere (e prendere il controllo) quando la raccolta è stabilizzata. – MattDavey

2
var terminalsToSync = TerminalAction.GetAllTerminals().ToList(); 

if(terminalsToSync.Any()) 
    SyncTerminals(terminalsToSync); 
else 
    GatewayLogAction.WriteLogInfo(Messages.NoTerminalsForSync); 
+1

Grazie per la risposta, ma ho menzionato nella domanda che vorrei evitare di convertirlo in lista perché sto cercando una soluzione generica. –

+0

@FedorHajdu cosa intendi esattamente con questo? In che modo la conversione in un elenco non è una soluzione generica? – MattDavey

+0

Per scoprire se terminaliToSync contiene elementi, è necessario prima enumerare l'enumerabile. – Chris

0

Se stai vedendo due procedura richiede la valutazione di qualsiasi GetAllTerminals() rendimenti , questo significa che il risultato della procedura non viene memorizzato nella cache. Senza sapere quale strategia di accesso ai dati si sta utilizzando, questo è abbastanza difficile da risolvere in generale.

La soluzione più semplice, come hai accennato, è copiare il risultato della chiamata prima di eseguire qualsiasi altra operazione. Se si voleva, si poteva ben avvolgere questo comportamento in un IEnumerable<T> che esegue la chiamata enumerabile interno solo una volta:

public class CachedEnumerable<T> : IEnumerable<T> 
{ 
    public CachedEnumerable<T>(IEnumerable<T> enumerable) 
    { 
     result = new Lazy<List<T>>(() => enumerable.ToList()); 
    } 

    private Lazy<List<T>> result; 

    public IEnumerator<T> GetEnumerator() 
    { 
     return this.result.Value.GetEnumerator(); 
    } 

    System.Collections.IEnumerable GetEnumerator() 
    { 
     return this.GetEnumerator(); 
    } 
} 

Avvolgere il risultato in un caso di questo tipo e non valuterà le interne enumerabili più volte .

1

Ecco un altro modo di affrontare questo problema:

int count = SyncTerminals(terminalsToSync); 
if(count == 0) GatewayLogAction.WriteLogInfo(Messages.NoTerminalsForSync); 

dove si modifica SyncTerminals da fare:

int count = 0; 
foreach(var obj in terminalsToSync) { 
    count++; 
    // some code 
} 
return count; 

Bello e semplice.

1

Tutte le soluzioni di memorizzazione nella cache qui memorizzano nella cache tutti gli articoli quando viene recuperato il primo elemento. È davvero pigro se si memorizza nella cache ogni singolo articolo mentre gli elementi dell'elenco sono iterati.

La differenza può essere visto in questo esempio:

public class LazyListTest 
{ 
    private int _count = 0; 

    public void Test() 
    { 
     var numbers = Enumerable.Range(1, 40); 
     var numbersQuery = numbers.Select(GetElement).ToLazyList(); // Cache lazy 
     var total = numbersQuery.Take(3) 
      .Concat(numbersQuery.Take(10)) 
      .Concat(numbersQuery.Take(3)) 
      .Sum(); 
     Console.WriteLine(_count); 
    } 

    private int GetElement(int value) 
    { 
     _count++; 
     // Some slow stuff here... 
     return value * 100; 
    } 
} 

Se si esegue il metodo Test(), il _coun t è solo 10. Senza caching sarebbe 16 e con .ToList() sarebbe 40 !

Un esempio di implementation of LazyList can be found here.

Problemi correlati