Si potrebbe utilizzare una serie di query che utilizzano Take
e Skip
, ma che sarebbe aggiungere troppi iterazioni sulla lista originale, credo.
Piuttosto, penso che dovresti creare un iteratore tuo, in questo modo:
public static IEnumerable<IEnumerable<T>> GetEnumerableOfEnumerables<T>(
IEnumerable<T> enumerable, int groupSize)
{
// The list to return.
List<T> list = new List<T>(groupSize);
// Cycle through all of the items.
foreach (T item in enumerable)
{
// Add the item.
list.Add(item);
// If the list has the number of elements, return that.
if (list.Count == groupSize)
{
// Return the list.
yield return list;
// Set the list to a new list.
list = new List<T>(groupSize);
}
}
// Return the remainder if there is any,
if (list.Count != 0)
{
// Return the list.
yield return list;
}
}
È quindi possibile chiamare questo ed è abilitato LINQ in modo da poter eseguire altre operazioni sulle sequenze risultanti.
Alla luce della risposta di Sam , ho sentito che c'era un modo più semplice per farlo senza:
- Scorrendo di nuovo l'elenco (cosa che non avevo fatto inizialmente)
- Materializzare gli oggetti in gruppi prima di rilasciare il blocco (per grossi pezzi di oggetti, ci sarebbero problemi di memoria)
- Tutto il codice pubblicato da Sam
Detto questo, ecco un altro passaggio, che ho codificato in un metodo di estensione per IEnumerable<T>
chiamare Chunk
:
public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source,
int chunkSize)
{
// Validate parameters.
if (source == null) throw new ArgumentNullException("source");
if (chunkSize <= 0) throw new ArgumentOutOfRangeException("chunkSize",
"The chunkSize parameter must be a positive value.");
// Call the internal implementation.
return source.ChunkInternal(chunkSize);
}
Niente di sorprendente lassù, solo il controllo degli errori di base.
Passando a ChunkInternal
:
private static IEnumerable<IEnumerable<T>> ChunkInternal<T>(
this IEnumerable<T> source, int chunkSize)
{
// Validate parameters.
Debug.Assert(source != null);
Debug.Assert(chunkSize > 0);
// Get the enumerator. Dispose of when done.
using (IEnumerator<T> enumerator = source.GetEnumerator())
do
{
// Move to the next element. If there's nothing left
// then get out.
if (!enumerator.MoveNext()) yield break;
// Return the chunked sequence.
yield return ChunkSequence(enumerator, chunkSize);
} while (true);
}
Fondamentalmente, ottiene il IEnumerator<T>
e scorre manualmente ogni elemento. Verifica se ci sono elementi da elencare al momento. Dopo che ogni blocco è stato elencato, se non ci sono elementi rimasti, scoppia.
Una volta rilevato che ci sono elementi nella sequenza, delega la responsabilità dell'implementazione interna IEnumerable<T>
a ChunkSequence
:
private static IEnumerable<T> ChunkSequence<T>(IEnumerator<T> enumerator,
int chunkSize)
{
// Validate parameters.
Debug.Assert(enumerator != null);
Debug.Assert(chunkSize > 0);
// The count.
int count = 0;
// There is at least one item. Yield and then continue.
do
{
// Yield the item.
yield return enumerator.Current;
} while (++count < chunkSize && enumerator.MoveNext());
}
Poiché MoveNext
è già stato chiamato al IEnumerator<T>
passato ChunkSequence
, restituisce l'oggetto restituito da Current
e quindi incrementa il conteggio, assicurandosi di non restituire più degli chunkSize
articoli e spostandosi all'elemento successivo nella sequenza dopo ogni iterazione (ma in cortocircuito se il numero di gli articoli prodotti superano le dimensioni del pezzo).
Se non sono rimasti elementi, il InternalChunk
metodo eseguirà un altro passaggio nel loop esterno, ma quando MoveNext
viene chiamato una seconda volta, restituirà comunque false, come indicato nella documentazione (sottolineatura mia):
Se MoveNext supera la fine della raccolta, l'enumeratore viene posizionato dopo l'ultimo elemento della raccolta e MoveNext restituisce false. Quando l'enumeratore si trova in questa posizione, anche le chiamate successive a MoveNext restituiscono false fino a quando non viene chiamato Reset.
A questo punto, il loop si interromperà e la sequenza di sequenze terminerà.
Questo è un semplice test:
static void Main()
{
string s = "agewpsqfxyimc";
int count = 0;
// Group by three.
foreach (IEnumerable<char> g in s.Chunk(3))
{
// Print out the group.
Console.Write("Group: {0} - ", ++count);
// Print the items.
foreach (char c in g)
{
// Print the item.
Console.Write(c + ", ");
}
// Finish the line.
Console.WriteLine();
}
}
Produzione:
Group: 1 - a, g, e,
Group: 2 - w, p, s,
Group: 3 - q, f, x,
Group: 4 - y, i, m,
Group: 5 - c,
Una nota importante, questo non funzionerà se non si esaurisce l'intera sequenza figlio o si interrompe in qualsiasi punto della sequenza padre. Questo è un avvertimento importante, ma se il tuo caso d'uso è che ne consumerai tutti elemento della sequenza di sequenze, allora funzionerà per te.
Inoltre, farà cose strane se giochi con l'ordine, proprio come ha fatto Sam a un certo punto .