Dividir lista en sublistas con LINQ

¿Hay alguna manera de que pueda separar una List en varias listas separadas de SomeObject , usando el índice de elementos como el delimitador de cada división?

Déjame ejemplificar:

Tengo una List y necesito una List<List> o List[] , de modo que cada una de estas listas contendrá un grupo de 3 elementos de la lista original (secuencialmente).

p.ej.:

  • Lista original: [a, g, e, w, p, s, q, f, x, y, i, m, c]

  • Listas resultantes: [a, g, e], [w, p, s], [q, f, x], [y, i, m], [c]

También necesitaría que el tamaño de las listas resultantes sea un parámetro de esta función.

Pruebe el siguiente código.

 public static IList> Split(IList source) { return source .Select((x, i) => new { Index = i, Value = x }) .GroupBy(x => x.Index / 3) .Select(x => x.Select(v => v.Value).ToList()) .ToList(); } 

La idea es agrupar primero los elementos por índices. Dividir por tres tiene el efecto de agruparlos en grupos de 3. Luego convierta cada grupo en una lista y el IEnumerable de List en una List de IEnumerable

Esta pregunta es un poco vieja, pero acabo de escribir esto, y creo que es un poco más elegante que las otras soluciones propuestas:

 ///  /// Break a list of items into chunks of a specific size ///  public static IEnumerable> Chunk(this IEnumerable source, int chunksize) { while (source.Any()) { yield return source.Take(chunksize); source = source.Skip(chunksize); } } 

En general, el enfoque sugerido por CaseyB funciona bien, de hecho, si está pasando una List es difícil culparlo, tal vez lo cambiaría a:

 public static IEnumerable> ChunkTrivialBetter(this IEnumerable source, int chunksize) { var pos = 0; while (source.Skip(pos).Any()) { yield return source.Skip(pos).Take(chunksize); pos += chunksize; } } 

Lo cual evitará cadenas de llamadas masivas. No obstante, este enfoque tiene un defecto general. Se materializa dos enumeraciones por fragmento, para resaltar el problema intente ejecutar:

 foreach (var item in Enumerable.Range(1, int.MaxValue).Chunk(8).Skip(100000).First()) { Console.WriteLine(item); } // wait forever 

Para superar esto, podemos probar el enfoque de Cameron , que pasa la prueba anterior en gran medida, ya que solo recorre la enumeración una vez.

El problema es que tiene un defecto diferente, materializa cada ítem en cada porción, el problema con ese enfoque es que te quedas muy arriba en la memoria.

Para ilustrar eso intente ejecutar:

 foreach (var item in Enumerable.Range(1, int.MaxValue) .Select(x => x + new string('x', 100000)) .Clump(10000).Skip(100).First()) { Console.Write('.'); } // OutOfMemoryException 

Finalmente, cualquier implementación debería ser capaz de manejar la iteración de trozos fuera de servicio, por ejemplo:

 Enumerable.Range(1,3).Chunk(2).Reverse().ToArray() // should return [3],[1,2] 

Muchas soluciones altamente óptimas como mi primera revisión de esta respuesta fallaron allí. El mismo problema se puede ver en la respuesta optimizada de casperOne .

Para abordar todos estos problemas, puede usar lo siguiente:

 namespace ChunkedEnumerator { public static class Extensions { class ChunkedEnumerable : IEnumerable { class ChildEnumerator : IEnumerator { ChunkedEnumerable parent; int position; bool done = false; T current; public ChildEnumerator(ChunkedEnumerable parent) { this.parent = parent; position = -1; parent.wrapper.AddRef(); } public T Current { get { if (position == -1 || done) { throw new InvalidOperationException(); } return current; } } public void Dispose() { if (!done) { done = true; parent.wrapper.RemoveRef(); } } object System.Collections.IEnumerator.Current { get { return Current; } } public bool MoveNext() { position++; if (position + 1 > parent.chunkSize) { done = true; } if (!done) { done = !parent.wrapper.Get(position + parent.start, out current); } return !done; } public void Reset() { // per http://msdn.microsoft.com/en-us/library/system.collections.ienumerator.reset.aspx throw new NotSupportedException(); } } EnumeratorWrapper wrapper; int chunkSize; int start; public ChunkedEnumerable(EnumeratorWrapper wrapper, int chunkSize, int start) { this.wrapper = wrapper; this.chunkSize = chunkSize; this.start = start; } public IEnumerator GetEnumerator() { return new ChildEnumerator(this); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } } class EnumeratorWrapper { public EnumeratorWrapper (IEnumerable source) { SourceEumerable = source; } IEnumerable SourceEumerable {get; set;} Enumeration currentEnumeration; class Enumeration { public IEnumerator Source { get; set; } public int Position { get; set; } public bool AtEnd { get; set; } } public bool Get(int pos, out T item) { if (currentEnumeration != null && currentEnumeration.Position > pos) { currentEnumeration.Source.Dispose(); currentEnumeration = null; } if (currentEnumeration == null) { currentEnumeration = new Enumeration { Position = -1, Source = SourceEumerable.GetEnumerator(), AtEnd = false }; } item = default(T); if (currentEnumeration.AtEnd) { return false; } while(currentEnumeration.Position < pos) { currentEnumeration.AtEnd = !currentEnumeration.Source.MoveNext(); currentEnumeration.Position++; if (currentEnumeration.AtEnd) { return false; } } item = currentEnumeration.Source.Current; return true; } int refs = 0; // needed for dispose semantics public void AddRef() { refs++; } public void RemoveRef() { refs--; if (refs == 0 && currentEnumeration != null) { var copy = currentEnumeration; currentEnumeration = null; copy.Source.Dispose(); } } } public static IEnumerable> Chunk(this IEnumerable source, int chunksize) { if (chunksize < 1) throw new InvalidOperationException(); var wrapper = new EnumeratorWrapper(source); int currentPos = 0; T ignore; try { wrapper.AddRef(); while (wrapper.Get(currentPos, out ignore)) { yield return new ChunkedEnumerable(wrapper, chunksize, currentPos); currentPos += chunksize; } } finally { wrapper.RemoveRef(); } } } class Program { static void Main(string[] args) { int i = 10; foreach (var group in Enumerable.Range(1, int.MaxValue).Skip(10000000).Chunk(3)) { foreach (var n in group) { Console.Write(n); Console.Write(" "); } Console.WriteLine(); if (i-- == 0) break; } var stuffs = Enumerable.Range(1, 10).Chunk(2).ToArray(); foreach (var idx in new [] {3,2,1}) { Console.Write("idx " + idx + " "); foreach (var n in stuffs[idx]) { Console.Write(n); Console.Write(" "); } Console.WriteLine(); } /* 10000001 10000002 10000003 10000004 10000005 10000006 10000007 10000008 10000009 10000010 10000011 10000012 10000013 10000014 10000015 10000016 10000017 10000018 10000019 10000020 10000021 10000022 10000023 10000024 10000025 10000026 10000027 10000028 10000029 10000030 10000031 10000032 10000033 idx 3 7 8 idx 2 5 6 idx 1 3 4 */ Console.ReadKey(); } } } 

También hay una ronda de optimizaciones que podría introducir para la iteración fuera de orden de los fragmentos, que está fuera del scope aquí.

¿En cuanto a qué método debes elegir? Depende totalmente del problema que estás tratando de resolver. Si no está preocupado con el primer defecto, la respuesta simple es increíblemente atractiva.

Tenga en cuenta que, como con la mayoría de los métodos, esto no es seguro para múltiples subprocesos, las cosas pueden volverse raras si desea que sea seguro para subprocesos, debería enmendar EnumeratorWrapper .

Podría usar una cantidad de consultas que utilicen Take y Skip , pero eso agregaría demasiadas iteraciones en la lista original, creo.

Por el contrario, creo que deberías crear un iterador propio, así:

 public static IEnumerable> GetEnumerableOfEnumerables( IEnumerable enumerable, int groupSize) { // The list to return. List list = new List(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(groupSize); } } // Return the remainder if there is any, if (list.Count != 0) { // Return the list. yield return list; } } 

A continuación, puede llamar a esto y está LINQ habilitado para que pueda realizar otras operaciones en las secuencias resultantes.


A la luz de la respuesta de Sam , sentí que había una manera más fácil de hacer esto sin:

  • Iterando a través de la lista nuevamente (lo cual no hice originalmente)
  • Materializar los elementos en grupos antes de soltar el trozo (para trozos grandes de elementos, habría problemas de memoria)
  • Todo el código que Sam publicó

Dicho esto, aquí hay otro pase, que he codificado en un método de extensión para IEnumerable llamado Chunk :

 public static IEnumerable> Chunk(this IEnumerable 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 sorprendente, solo comprobación básica de errores.

Pasando a ChunkInternal :

 private static IEnumerable> ChunkInternal( this IEnumerable source, int chunkSize) { // Validate parameters. Debug.Assert(source != null); Debug.Assert(chunkSize > 0); // Get the enumerator. Dispose of when done. using (IEnumerator 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); } 

Básicamente, obtiene IEnumerator y itera manualmente a través de cada elemento. Comprueba si hay elementos actualmente enumerados. Después de enumerar cada fragmento, si no quedan elementos, se rompe.

Una vez que detecta que hay elementos en la secuencia, delega la responsabilidad de la IEnumerable interna de IEnumerable a ChunkSequence :

 private static IEnumerable ChunkSequence(IEnumerator 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 ya fue llamado en IEnumerator pasado a ChunkSequence , produce el elemento devuelto por Current y luego incrementa el conteo, asegurándose de nunca devolver más que los elementos de chunkSize y pasar al siguiente elemento de la secuencia después de cada iteración ( pero cortocircuitado si la cantidad de elementos generados excede el tamaño del fragmento).

Si no quedan elementos, entonces el método InternalChunk hará otra pasada en el bucle externo, pero cuando MoveNext se MoveNext segunda vez, aún devolverá false, según la documentación (énfasis mío):

Si MoveNext pasa el final de la colección, el enumerador se posiciona después del último elemento en la colección y MoveNext devuelve falso. Cuando el enumerador está en esta posición, las llamadas subsiguientes a MoveNext también devuelven falso hasta que se llame a Reset.

En este punto, el ciclo se interrumpirá y la secuencia de secuencias terminará.

Esta es una prueba simple:

 static void Main() { string s = "agewpsqfxyimc"; int count = 0; // Group by three. foreach (IEnumerable 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(); } } 

Salida:

 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, esto no funcionará si no se drena toda la secuencia secundaria o se rompe en cualquier punto de la secuencia principal. Esta es una advertencia importante, pero si su caso de uso es que consumirá todos los elementos de la secuencia de secuencias, entonces esto funcionará para usted.

Además, hará cosas extrañas si juegas con el orden, tal como lo hizo Sam en un punto .

Ok, aquí está mi opinión:

  • completamente vago: funciona en enumerables infinitos
  • sin copia intermedia / almacenamiento en búfer
  • O (n) tiempo de ejecución
  • funciona también cuando las secuencias internas solo se consumen parcialmente

 public static IEnumerable> Chunks(this IEnumerable enumerable, int chunkSize) { if (chunkSize < 1) throw new ArgumentException("chunkSize must be positive"); using (var e = enumerable.GetEnumerator()) while (e.MoveNext()) { var remaining = chunkSize; // elements remaining in the current chunk var innerMoveNext = new Func(() => --remaining > 0 && e.MoveNext()); yield return e.GetChunk(innerMoveNext); while (innerMoveNext()) {/* discard elements skipped by inner iterator */} } } private static IEnumerable GetChunk(this IEnumerator e, Func innerMoveNext) { do yield return e.Current; while (innerMoveNext()); } 

Ejemplo de uso

 var src = new [] {1, 2, 3, 4, 5, 6}; var c3 = src.Chunks(3); // {{1, 2, 3}, {4, 5, 6}}; var c4 = src.Chunks(4); // {{1, 2, 3, 4}, {5, 6}}; var sum = c3.Select(c => c.Sum()); // {6, 15} var count = c3.Count(); // 2 var take2 = c3.Select(c => c.Take(2)); // {{1, 2}, {4, 5}} 

Explicaciones

El código funciona anidando dos iteradores basados ​​en yield .

El iterador externo debe realizar un seguimiento de cuántos elementos han sido efectivamente consumidos por el iterador interno (fragmento). Esto se hace cerrando el remaining con innerMoveNext() . Los elementos no consumidos de un fragmento se descartan antes de que el fragmento siguiente sea cedido por el iterador externo. Esto es necesario porque de lo contrario se obtienen resultados inconsistentes, cuando los enumerables internos no se consumen (por completo) (por ejemplo, c3.Count() devolvería 6).

Nota: La respuesta se ha actualizado para abordar las deficiencias señaladas por @aolszowka.

completamente vago, sin contar ni copiar:

 public static class EnumerableExtensions { public static IEnumerable> Split(this IEnumerable source, int len) { if (len == 0) throw new ArgumentNullException(); var enumer = source.GetEnumerator(); while (enumer.MoveNext()) { yield return Take(enumer.Current, enumer, len); } } private static IEnumerable Take(T head, IEnumerator tail, int len) { while (true) { yield return head; if (--len == 0) break; if (tail.MoveNext()) head = tail.Current; else break; } } } 

Creo que la siguiente sugerencia sería la más rápida. Estoy sacrificando la pereza de la fuente Enumerable por la capacidad de usar Array.Copy y saber de antemano el tiempo de cada una de mis sublistas.

 public static IEnumerable Chunk(this IEnumerable items, int size) { T[] array = items as T[] ?? items.ToArray(); for (int i = 0; i < array.Length; i+=size) { T[] chunk = new T[Math.Min(size, array.Length - i)]; Array.Copy(array, i, chunk, 0, chunk.Length); yield return chunk; } } 

Podemos mejorar la solución de @JaredPar para hacer una verdadera evaluación perezosa. Utilizamos un método GroupAdjacentBy que genera grupos de elementos consecutivos con la misma clave:

 sequence .Select((x, i) => new { Value = x, Index = i }) .GroupAdjacentBy(x=>x.Index/3) .Select(g=>g.Select(x=>x.Value)) 

Debido a que los grupos se producen uno a uno, esta solución funciona de manera eficiente con secuencias largas o infinitas.

System.Interactive proporciona Buffer() para este propósito. Algunas pruebas rápidas muestran que el rendimiento es similar a la solución de Sam.

Aquí hay una rutina de división de listas que escribí hace un par de meses:

 public static List> Chunk( List theList, int chunkSize ) { List> result = theList .Select((x, i) => new { data = x, indexgroup = i / chunkSize }) .GroupBy(x => x.indexgroup, x => x.data) .Select(g => new List(g)) .ToList(); return result; } 

Escribí un método de extensión Clump hace varios años. Funciona muy bien, y es la implementación más rápida aquí. :PAG

 ///  /// Clumps items into same size lots. ///  ///  /// The source list of items. /// The maximum size of the clumps to make. /// A list of list of items, where each list of items is no bigger than the size given. public static IEnumerable> Clump(this IEnumerable source, int size) { if (source == null) throw new ArgumentNullException("source"); if (size < 1) throw new ArgumentOutOfRangeException("size", "size must be greater than 0"); return ClumpIterator(source, size); } private static IEnumerable> ClumpIterator(IEnumerable source, int size) { Debug.Assert(source != null, "source is null."); T[] items = new T[size]; int count = 0; foreach (var item in source) { items[count] = item; count++; if (count == size) { yield return items; items = new T[size]; count = 0; } } if (count > 0) { if (count == size) yield return items; else { T[] tempItems = new T[count]; Array.Copy(items, tempItems, count); yield return tempItems; } } } 

Esta es una vieja pregunta, pero esto es con lo que terminé; enumera el enumerable solo una vez, pero crea listas para cada una de las particiones. No sufre un comportamiento inesperado cuando se llama a ToArray() como lo hacen algunas de las implementaciones:

  public static IEnumerable> Partition(IEnumerable source, int chunkSize) { if (source == null) { throw new ArgumentNullException("source"); } if (chunkSize < 1) { throw new ArgumentException("Invalid chunkSize: " + chunkSize); } using (IEnumerator sourceEnumerator = source.GetEnumerator()) { IList currentChunk = new List(); while (sourceEnumerator.MoveNext()) { currentChunk.Add(sourceEnumerator.Current); if (currentChunk.Count == chunkSize) { yield return currentChunk; currentChunk = new List(); } } if (currentChunk.Any()) { yield return currentChunk; } } } 

Me parece que este pequeño fragmento hace el trabajo bastante bien.

 public static IEnumerable> Chunked(this List source, int chunkSize) { var offset = 0; while (offset < source.Count) { yield return source.GetRange(offset, Math.Min(source.Count - offset, chunkSize)); offset += chunkSize; } } 

Descubrimos que la solución de David B funcionaba mejor. Pero lo adaptamos a una solución más general:

 list.GroupBy(item => item.SomeProperty) .Select(group => new List(group)) .ToArray(); 

Esta solución siguiente es la más compacta que pude encontrar que es O (n).

 public static IEnumerable Chunk(IEnumerable source, int chunksize) { var list = source as IList ?? source.ToList(); for (int start = 0; start < list.Count; start += chunksize) { T[] chunk = new T[Math.Min(chunksize, list.Count - start)]; for (int i = 0; i < chunk.Length; i++) chunk[i] = list[start + i]; yield return chunk; } } 

Código antiguo, pero esto es lo que he estado usando:

  public static IEnumerable> InSetsOf(this IEnumerable source, int max) { var toReturn = new List(max); foreach (var item in source) { toReturn.Add(item); if (toReturn.Count == max) { yield return toReturn; toReturn = new List(max); } } if (toReturn.Any()) { yield return toReturn; } } 

Si la lista es de tipo system.collections.generic, puede usar el método “CopyTo” disponible para copiar elementos de su matriz a otras sub-matrices. Usted especifica el elemento de inicio y la cantidad de elementos para copiar.

También podría hacer 3 clones de su lista original y usar “Eliminar Rango” en cada lista para reducir la lista al tamaño que desea.

O simplemente crea un método de ayuda para hacerlo por ti.

¿Qué hay de este?

 var input = new List { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" }; var k = 3 var res = Enumerable.Range(0, (input.Count - 1) / k + 1) .Select(i => input.GetRange(i * k, Math.Min(k, input.Count - i * k))) .ToList(); 

Por lo que sé, GetRange () es lineal en términos de cantidad de elementos tomados. Entonces esto debería funcionar bien.

Es una solución antigua pero tenía un enfoque diferente. Uso Skip para mover al desplazamiento deseado y Take para extraer el número deseado de elementos:

 public static IEnumerable> Chunk(this IEnumerable source, int chunkSize) { if (chunkSize <= 0) throw new ArgumentOutOfRangeException($"{nameof(chunkSize)} should be > 0"); var nbChunks = (int)Math.Ceiling((double)source.Count()/chunkSize); return Enumerable.Range(0, nbChunks) .Select(chunkNb => source.Skip(chunkNb*chunkSize) .Take(chunkSize)); } 

Usando particionamiento modular:

 public IEnumerable> Split(IEnumerable input, int chunkSize) { var chunks = (int)Math.Ceiling((double)input.Count() / (double)chunkSize); return Enumerable.Range(0, chunks).Select(id => input.Where(s => s.GetHashCode() % chunks == id)); } 

Solo poniendo mi granito de arena. Si desea “agrupar” la lista (visualizar de izquierda a derecha), podría hacer lo siguiente:

  public static List> Buckets(this List source, int numberOfBuckets) { List> result = new List>(); for (int i = 0; i < numberOfBuckets; i++) { result.Add(new List()); } int count = 0; while (count < source.Count()) { var mod = count % numberOfBuckets; result[mod].Add(source[count]); count++; } return result; } 

Tomé la respuesta principal e hice que fuera un contenedor de COI para determinar dónde dividir. (¿ Para quién realmente busca dividir solo 3 artículos, leyendo esta publicación mientras busca una respuesta? )

Este método permite dividir en cualquier tipo de elemento según sea necesario.

 public static List> SplitOn(List main, Func splitOn) { int groupIndex = 0; return main.Select( item => new { Group = (splitOn.Invoke(item) ? ++groupIndex : groupIndex), Value = item }) .GroupBy( it2 => it2.Group) .Select(x => x.Select(v => v.Value).ToList()) .ToList(); } 

Entonces, para OP, el código sería

 var it = new List() { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" }; int index = 0; var result = SplitOn(it, (itm) => (index++ % 3) == 0 ); 

So performatic as the Sam Saffron ‘s approach.

 public static IEnumerable> Batch(this IEnumerable source, int size) { if (source == null) throw new ArgumentNullException(nameof(source)); if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size), "Size must be greater than zero."); return BatchImpl(source, size).TakeWhile(x => x.Any()); } static IEnumerable> BatchImpl(this IEnumerable source, int size) { var values = new List(); var group = 1; var disposed = false; var e = source.GetEnumerator(); try { while (!disposed) { yield return GetBatch(e, values, group, size, () => { e.Dispose(); disposed = true; }); group++; } } finally { if (!disposed) e.Dispose(); } } static IEnumerable GetBatch(IEnumerator e, List values, int group, int size, Action dispose) { var min = (group - 1) * size + 1; var max = group * size; var hasValue = false; while (values.Count < min && e.MoveNext()) { values.Add(e.Current); } for (var i = min; i <= max; i++) { if (i <= values.Count) { hasValue = true; } else if (hasValue = e.MoveNext()) { values.Add(e.Current); } else { dispose(); } if (hasValue) yield return values[i - 1]; else yield break; } } 

}

Can work with infinite generators:

 a.Zip(a.Skip(1), (x, y) => Enumerable.Repeat(x, 1).Concat(Enumerable.Repeat(y, 1))) .Zip(a.Skip(2), (xy, z) => xy.Concat(Enumerable.Repeat(z, 1))) .Where((x, i) => i % 3 == 0) 

Demo code: https://ideone.com/GKmL7M

 using System; using System.Collections.Generic; using System.Linq; public class Test { private static void DoIt(IEnumerable a) { Console.WriteLine(String.Join(" ", a)); foreach (var x in a.Zip(a.Skip(1), (x, y) => Enumerable.Repeat(x, 1).Concat(Enumerable.Repeat(y, 1))).Zip(a.Skip(2), (xy, z) => xy.Concat(Enumerable.Repeat(z, 1))).Where((x, i) => i % 3 == 0)) Console.WriteLine(String.Join(" ", x)); Console.WriteLine(); } public static void Main() { DoIt(new int[] {1}); DoIt(new int[] {1, 2}); DoIt(new int[] {1, 2, 3}); DoIt(new int[] {1, 2, 3, 4}); DoIt(new int[] {1, 2, 3, 4, 5}); DoIt(new int[] {1, 2, 3, 4, 5, 6}); } } 
 1 1 2 1 2 3 1 2 3 1 2 3 4 1 2 3 1 2 3 4 5 1 2 3 1 2 3 4 5 6 1 2 3 4 5 6 

But actually I would prefer to write corresponding method without linq.

For anyone interested in a packaged/maintained solution, the MoreLINQ library provides the Batch extension method which matches your requested behavior:

 IEnumerable source = "Example string"; IEnumerable> chunksOfThreeChars = source.Batch(3); 

The Batch implementation is similar to Cameron MacFarland’s answer , with the addition of an overload for transforming the chunk/batch before returning, and performs quite well.

To insert my two cents…

By using the list type for the source to be chunked, I found another very compact solution:

 public static IEnumerable> Chunk(this IEnumerable source, int chunkSize) { // copy the source into a list var chunkList = source.ToList(); // return chunks of 'chunkSize' items while (chunkList.Count > chunkSize) { yield return chunkList.GetRange(0, chunkSize); chunkList.RemoveRange(0, chunkSize); } // return the rest yield return chunkList; }