Вы можете использовать несколько запросов, которые используют 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
возвращает ложь Когда счетчик находится в этой позиции, последующие
вызовы 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,
Важное замечание: это не будет работать, если вы не истощите всю дочернюю последовательность или не прервитесь в какой-либо точке родительской последовательности. Это важное предупреждение, но если ваш вариант использования заключается в том, что вы будете использовать каждый элемент последовательности последовательностей, то это будет работать для вас.
Кроме того, он будет делать странные вещи, если вы играете с ордером, так же, как Сэм делал в один момент .