Você pode usar várias consultas que usam Take
e Skip
, mas acredito que adicionariam muitas iterações à lista original.
Em vez disso, acho que você deve criar um iterador próprio, assim:
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;
}
}
Você pode chamar isso e ele está ativado para LINQ, para que você possa executar outras operações nas seqüências resultantes.
À luz da resposta de Sam , senti que havia uma maneira mais fácil de fazer isso sem:
- Iterando a lista novamente (o que não fiz originalmente)
- Materializar os itens em grupos antes de liberar o pedaço (para pedaços grandes de itens, haveria problemas de memória)
- Todo o código que Sam postou
Dito isto, aqui está outra passagem, que eu codifiquei em um método de extensão IEnumerable<T>
chamado 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);
}
Nada de surpreendente lá em cima, apenas uma verificação básica de erros.
Passando para 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);
}
Basicamente, ele obtém IEnumerator<T>
e itera manualmente através de cada item. Ele verifica se há algum item a ser enumerado no momento. Depois que cada pedaço é enumerado, se não houver nenhum item, ele será interrompido.
Depois de detectar que há itens na sequência, ele delega a responsabilidade pela IEnumerable<T>
implementação interna para 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());
}
Como MoveNext
já foi chamado no IEnumerator<T>
passado ChunkSequence
, ele gera o item retornado Current
e depois incrementa a contagem, certificando-se de nunca retornar mais do que chunkSize
itens e passar para o próximo item na sequência após cada iteração (mas em curto-circuito se o número de itens produzidos excede o tamanho do pedaço).
Se não houver itens restantes, o InternalChunk
método fará outra passagem no loop externo, mas quando MoveNext
for chamado pela segunda vez, ainda retornará falso, conforme a documentação (ênfase minha):
Se MoveNext passar o final da coleção, o enumerador será posicionado após o último elemento da coleção e MoveNext retornará false. Quando o enumerador está nessa posição, as chamadas subseqüentes para MoveNext também retornam false até que Reset seja chamado.
Nesse ponto, o loop será interrompido e a sequência de sequências será encerrada.
Este é um teste simples:
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();
}
}
Resultado:
Group: 1 - a, g, e,
Group: 2 - w, p, s,
Group: 3 - q, f, x,
Group: 4 - y, i, m,
Group: 5 - c,
Uma observação importante: isso não funcionará se você não drenar a sequência filho inteira ou interromper em qualquer ponto da sequência pai. Essa é uma ressalva importante, mas se o seu caso de uso for o de consumir todos os elementos da sequência de sequências, isso funcionará para você.
Além disso, ele fará coisas estranhas se você jogar com a ordem, assim como Sam fez em um ponto .