2013-03-14 34 views
48

Sto sviluppando un programma C# che ha un "IEnumerable users" che memorizza gli ID di 4 milioni di utenti. Ho bisogno di scorrere l'Ienummerable ed estrarre un batch 1000 id ogni volta per eseguire alcune operazioni in un altro metodo.Come passare da IEnumerable a lotti

Come estrarre 1000 id alla volta dall'inizio di Ienumerable ... fare qualcos'altro quindi recuperare il successivo lotto di 1000 e così via?

È possibile?

+1

Eventuali duplicati di [creare batch in LINQ] (https://stackoverflow.com/ domande/13731796/create-batches-in-linq) –

risposta

30

Sembra che tu debba utilizzare i metodi Skip e Take del tuo oggetto. Esempio:

users.Skip(1000).Take(1000) 

questo sarebbe saltare la prima 1000 e prendere il prossimo 1000. Si aveva solo bisogno di aumentare la quantità saltato con ogni chiamata

Si potrebbe utilizzare una variabile intera con il parametro per Skip e puoi regolare quanto viene saltato. Puoi quindi chiamarlo in un metodo.

public IEnumerable<user> GetBatch(int pageNumber) 
{ 
    return users.Skip(pageNumber * 1000).Take(1000); 
} 
+0

Thanks bill. Non ho capito esattamente quale sia il numero di pagina * (numero di pagina * 1000) che ho scelto per gli utenti di undertoood.Skip (1000) .Take (1000). An d Avrei solo bisogno di incrementare saltare 1000 ... 2000 ... 3000 e così via – user1526912

+0

Il parametro pageNumber sarebbe incrementato ogni volta che è necessario recuperare un nuovo gruppo di utenti. Ad esempio, quando si desidera ottenere il batch da 3001 a 4000, si chiama il metodo: 'var usersBatch = GetBatch (3);' – Bill

+11

Non è molto efficiente. Ricomincia sempre dall'inizio e cammina attraverso i risultati precedenti. Più efficiente sarebbe mantenere l'IEnumerable da dove si era interrotto. –

25

Il modo più semplice per fare questo è probabilmente solo per utilizzare il metodo GroupBy in LINQ:

var batches = myEnumerable 
    .Select((x, i) => new { x, i }) 
    .GroupBy(p => (p.i/1000), (p, i) => p.x); 

Ma per una soluzione più sofisticata, vedono questo blog post su come creare il proprio metodo di estensione per fare questo. Duplicato qui per i posteri:

public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> collection, int batchSize) 
{ 
    List<T> nextbatch = new List<T>(batchSize); 
    foreach (T item in collection) 
    { 
     nextbatch.Add(item); 
     if (nextbatch.Count == batchSize) 
     { 
      yield return nextbatch; 
      nextbatch = new List<T>(); 
      // or nextbatch.Clear(); but see Servy's comment below 
     } 
    } 

    if (nextbatch.Count > 0) 
     yield return nextbatch; 
} 
+1

Non creerebbe 4 milioni di oggetti anonimi (non il mio downvote)? Anche internamente creerà la ricerca di tutti quegli oggetti. –

+1

@lazyberezovsky sì, suppongo che sarebbe (ho detto che era il modo più semplice, non necessariamente il migliore). Ho aggiunto anche una soluzione alternativa. –

+0

Sì, buona modifica. Vi consiglio anche di dare un'occhiata all'implementazione di morelinq con array come bucket. Penso che sarà un po 'più veloce –

5

È possibile ottenere tale risultato utilizzando il metodo di estensione Take and Skip Enumerable. Per ulteriori informazioni sull'utilizzo cassa linq 101

+1

Sì, ma potrebbe non essere molto efficiente ... –

+0

Capisco che Take può essere usato ma quando prendo 1000 record come faccio a prendere un altro 1000 dall'ultimo punto – user1526912

+1

@ user1526912 - 'Salta (i * 1000) .Take (1000) 'con i = 0,1,2, ... –

90

È possibile utilizzare MoreLINQ's Batch operator (disponibile da NuGet):

foreach(IEnumerable<User> batch in users.Batch(1000)) 
    // use batch 

Se l'utilizzo semplice della libreria non è un'opzione, è possibile riutilizzare implementazione:

public static IEnumerable<IEnumerable<T>> Batch<T>(
     this IEnumerable<T> source, int size) 
{ 
    T[] bucket = null; 
    var count = 0; 

    foreach (var item in source) 
    { 
     if (bucket == null) 
      bucket = new T[size]; 

     bucket[count++] = item; 

     if (count != size)     
      continue; 

     yield return bucket.Select(x => x); 

     bucket = null; 
     count = 0; 
    } 

    // Return the last bucket with all remaining elements 
    if (bucket != null && count > 0)    
     yield return bucket.Take(count);    
} 

BTW per prestazioni si può semplicemente restituire il secchio senza chiamare Select(x => x). Select è ottimizzato per gli array, ma il delegato del selettore verrà comunque richiamato su ciascun elemento. Quindi, nel tuo caso è meglio usare

yield return bucket; 
+1

Stavo per rispondere a questo. Non reinventare la ruota, usa il metodo Batch di MoreLinq ;-) –

+0

@ Meta-Knight sì, io uso il loro MaxBy, DistinctBy, Batch e altri metodi. È meglio inventare una nuova macchina, piuttosto che reinventare le ruote :) –

+6

Vale la pena notare che il 'Select' è specificamente in grado di oscurare l'array sottostante dal chiamante (non che possano fare molto con esso a quel punto). La maggior parte delle persone non ne ha bisogno, ma per un metodo di libreria aiuta a evitare di rompere le modifiche se cambiano l'implementazione in futuro. – Servy

7

provare a utilizzare questo:

public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
     this IEnumerable<TSource> source, 
     int batchSize) 
    { 
     var batch = new List<TSource>(); 
     foreach (var item in source) 
     { 
      batch.Add(item); 
      if (batch.Count == batchSize) 
      { 
       yield return batch; 
       batch = new List<TSource>(); 
      } 
     } 

     if (batch.Any()) yield return batch; 
    } 

e di utilizzare la funzione di cui sopra:

foreach (var list in Users.Batch(1000)) 
{ 

} 
+6

È possibile ottenere un lieve aumento dell'efficienza fornendo la dimensione del batch al costruttore dell'elenco. – Servy

+0

Questo è buono. Completamente popolando ogni Lista in questo metodo è necessario per saltare l'enumerazione di quell'elenco nel chiamante per fare in modo che il successivo (e così via) gruppo abbia gli elementi giusti al suo interno. – ErikE

3

Alcuni cosa come questo dovrebbe funzionare:

List<MyClass> batch = new List<MyClass>(); 
foreach (MyClass item in items) 
{ 
    batch.Add(item); 

    if (batch.Count == 1000) 
    { 
     // Perform operation on batch 
     batch.Clear(); 
    } 
} 

// Process last batch 
if (batch.Any()) 
{ 
    // Perform operation on batch 
} 

E si potrebbe generalizzare questo in un metodo generico, in questo modo:

static void PerformBatchedOperation<T>(IEnumerable<T> items, 
             Action<IEnumerable<T>> operation, 
             int batchSize) 
{ 
    List<T> batch = new List<T>(); 
    foreach (T item in items) 
    { 
     batch.Add(item); 

     if (batch.Count == batchSize) 
     { 
      operation(batch); 
      batch.Clear(); 
     } 
    } 

    // Process last batch 
    if (batch.Any()) 
    { 
     operation(batch); 
    } 
}