2011-03-02 12 views
6

Para admitir una API que solo acepta una cantidad específica de elementos (5 elementos), quiero transformar un resultado LINQ en grupos más pequeños de elementos que siempre contienen esa cantidad establecida de artículos.Divida un IEnumerable grande en IEnumerable menor de un monto fijo del elemento

Suponiendo que la lista {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18}

quiero conseguir tres listas más pequeñas de un máximo de 5 elementos cada

{1, 2, 3, 4, 5}

{6, 7, 8, 9, 10}

{11, 12, 13, 14, 15}

{16, 17, 18}

¿Cómo puedo hacer eso con LINQ? Supongo que implica Group o Aggregate, pero tengo problemas para entender cómo escribir eso.

+0

http://stackoverflow.com/questions/1349491/how-can-i-split-an-ienumerablestring-into-groups-of-ienumerablestring – diceguyd30

+0

posible duplicado de la [Lista de Split en sublistas con LINQ] (http://stackoverflow.com/questions/419019/split-list-into-sublists-with-linq) – nawfal

Respuesta

15

intentar algo como esto:

var result = items.Select((value, index) => new { Index = index, Value = value}) 
        .GroupBy(x => x.Index/5) 
        .Select(g => g.Select(x => x.Value).ToList()) 
        .ToList(); 

Funciona mediante la partición de los elementos en grupos en función de su índice en la lista original.

+0

Leer su código provocó una idea. 'int index = 0; var groups = items.GroupBy (x => index ++/5); 'Y este trabajo. Gracias. Siempre es simple, como de costumbre. –

+3

@ Pierre-Alain: El uso de efectos secundarios en una consulta LINQ generalmente no está bien visto; va en contra de los principios de diseño de LINQ, que es principalmente funcional. Funcionará en cierta medida ... pero obtendrá algunos efectos interesantes si evalúa 'groups' dos veces, por ejemplo. –

+0

@Pierre La función pasada a 'GroupBy' que tiene un efecto secundario es fea. Linq se trata de programación funcional y, por lo tanto, libre de efectos secundarios. – CodesInChaos

4

Una posibilidad sencilla es utilizar los métodos Enumerable.Skip y Enumerable.Take, por ejemplo:

List<int> nums = new List<int>(){1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18}; 

var list1 = nums.Take(5); 
var list2 = nums.Skip(5).Take(5); 
var list3 = nums.Skip(10).Take(5); 
var list4 = nums.Skip(15).Take(5); 

Como Jon menciona en los comentarios, sin embargo, un enfoque sencillo como éste volverá a evaluar nums (en este ejemplo) cada vez, lo que afectará el rendimiento (según el tamaño de la colección).

+5

Tenga en cuenta que esto volverá a evaluar la fuente cada vez, lo que está bien en algunos casos (p. colecciones en memoria) pero no todas (por ejemplo, leer entradas de archivos de registro grandes). Siempre que sea posible, intento escribir mi código LINQ para evaluar solo la fuente una vez. –

+0

@Jon Buen punto, gracias. – Donut

1

Tenemos un método Batch en MoreLINQ. Debe tener cuidado de cómo lo usa, ya que el lote que se pasa al selector cada vez es una referencia a la misma matriz, pero funciona.

Usted puede utilizar GroupBy, pero eso no puede ser perezoso - tiene que acumular todos los resultados antes de que pueda devolver nada. Eso puede estar bien para ti, pero vale la pena ser consciente de ello.

+0

'Línea 86: bucket = null;' ¿Esto no asegura que la matriz sea diferente cada vez? – CodesInChaos

+0

@CodeInChaos: el 'x => x' no causa una asignación por lote; esto significa que cada resultado es la misma referencia de matriz (hasta el final). De hecho, es algo peligroso (por ejemplo, 'x.Batch (10) .ToList()') –

+0

@Jon. Borré mis comentarios originales porque al principio omité el 'bucket = null'. – CodesInChaos

13

que acababa de hacer algo como esto:

public static IEnumerable<IEnumerable<T>> TakeChunks<T>(this IEnumerable<T> source, int size) 
{ 
    // Typically you'd put argument validation in the method call and then 
    // implement it using a private method... I'll leave that to your 
    // imagination. 

    var list = new List<T>(size); 

    foreach (T item in source) 
    { 
     list.Add(item); 
     if (list.Count == size) 
     { 
      List<T> chunk = list; 
      list = new List<T>(size); 
      yield return chunk; 
     } 
    } 

    if (list.Count > 0) 
    { 
     yield return list; 
    } 
} 

Uso:

var list = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 

foreach (var chunk in list.TakeChunks(3)) 
{ 
    Console.WriteLine(string.Join(", ", chunk)); 
} 

Salida:

 
1, 2, 3 
4, 5, 6 
7, 8, 9 
10 

Justificación:

En comparación con otros métodos tales múltiple llama t O Skip y Take o una consulta LINQ gran lujo, lo anterior es:

  • más eficiente
  • más evidente en la función (en mi opinión)
  • más legible en la ejecución (de nuevo, en mi opinión)
+1

Si va a devolver una copia cada vez, ¿por qué no comienza una nueva lista para cada lote y la devuelve? ¿Dónde está el beneficio de llamar a ToArray? –

+0

@Jon: Buena pregunta. Supongo que no había una buena razón para eso. –

0
var list = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; 
var result = new List<List<int>>(); 
while (list.Count != 0) { 
    result.Add(list.TakeWhile(x => x++ <= 5).ToList()); 
    list.RemoveRange(0, list.Count < 5 ? list.Count : 5); 
} 
Cuestiones relacionadas