Вы могли бы использовать несколько запросов, которые используют Take
и Skip
, но я бы добавил слишком много итераций в исходный список.
Скорее, я думаю, что вы должны создать свой собственный итератор, вот так:
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;
}
}
Затем вы можете вызвать это и включить LINQ, чтобы вы могли выполнять другие операции с результирующими последовательностями.
В свете ответа Сэма я почувствовал, что есть более простой способ сделать это без:
- Итерация по списку снова (что я не делал изначально)
- Материализация элементов в группах перед выпуском фрагмента (для больших фрагментов могут возникнуть проблемы с памятью)
- Весь код, который выложил Сэм
Тем не менее, вот еще один проход, который я записал в методе расширения для IEnumerable<T>
вызова 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);
}
Там нет ничего удивительного, просто базовая проверка ошибок.
Переходя к 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);
}
В основном, он получает IEnumerator<T>
и вручную перебирает каждый элемент. Он проверяет, есть ли какие-либо элементы, которые должны быть перечислены в настоящее время. После перечисления каждого чанка, если не осталось ни одного элемента, он вспыхивает.
Как только он обнаруживает, что в последовательности есть элементы, он делегирует ответственность за внутреннюю IEnumerable<T>
реализацию 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());
}
Так как MoveNext
он уже был вызван для IEnumerator<T>
переданного в ChunkSequence
, он возвращает элемент, возвращаемый, Current
а затем увеличивает счетчик, не заботясь о том, чтобы возвращать больше, чем chunkSize
элементы, и переходя к следующему элементу в последовательности после каждой итерации (но замыкается, если число количество полученных предметов превышает размер куска).
Если элементов больше не осталось, то InternalChunk
метод сделает еще один проход во внешнем цикле, но при MoveNext
вызове во второй раз он все равно вернет false, согласно документации (выделено мое):
Если MoveNext проходит через конец коллекции, перечислитель располагается после последнего элемента в коллекции, а MoveNext возвращает значение false. Когда перечислитель находится в этой позиции, последующие вызовы MoveNext также возвращают false, пока не будет вызван Reset.
В этот момент цикл разорвется, и последовательность последовательностей прекратится.
Это простой тест:
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();
}
}
Вывод:
Group: 1 - a, g, e,
Group: 2 - w, p, s,
Group: 3 - q, f, x,
Group: 4 - y, i, m,
Group: 5 - c,
Важное примечание: это не сработает, если вы не истощите всю дочернюю последовательность или не прервете ее в какой-либо точке родительской последовательности. Это важное предостережение, но если ваш вариант использования таков, что вы будете потреблять каждый элемент последовательности последовательностей, то это будет работать для вас.
Кроме того, он будет делать странные вещи, если вы играете с орденом, так же, как Сэм однажды .