¿Manera elegante de combinar múltiples colecciones de elementos?

94

Digamos que tengo un número arbitrario de colecciones, cada una con objetos del mismo tipo (por ejemplo, List<int> fooy List<int> bar). Si estas colecciones estuvieran en una colección (por ejemplo, de tipo List<List<int>>, podría usar SelectManypara combinarlas todas en una colección.

Sin embargo, si estas colecciones no están ya en la misma colección, tengo la impresión de que tendría que escribir un método como este:

public static IEnumerable<T> Combine<T>(params ICollection<T>[] toCombine)
{
   return toCombine.SelectMany(x => x);
}

Que luego llamaría así:

var combined = Combine(foo, bar);

¿Existe una forma limpia y elegante de combinar (cualquier número de) colecciones sin tener que escribir un método de utilidad como el Combineanterior? Parece bastante simple que debería haber una forma de hacerlo en LINQ, pero tal vez no.

Rosquilla
fuente
Cuidado con los de concatenación para concatenar número muy grande de enumerables, podría destruir tu pila: programmaticallyspeaking.com/...
nawfal

Respuestas:

107

Creo que podrías estar buscando LINQ's .Concat().

var combined = foo.Concat(bar).Concat(foobar).Concat(...);

Alternativamente, .Union()eliminará elementos duplicados.

Domenic
fuente
2
Gracias; La única razón por la que no hice esto en primer lugar es que (para mí) parece ponerse más feo cuanto más colecciones tienes que lidiar. Sin embargo, esto tiene la ventaja de utilizar funciones LINQ existentes, con las que probablemente los futuros desarrolladores ya estén familiarizados.
Donut
2
Gracias por el .Union()dato. Tenga en cuenta que deberá implementar IComparer en su tipo personalizado en caso de que lo sea.
Gonzo345
29

A mi Concat como método de extensión, no es muy elegante en mi código cuando tengo varias secuencias grandes para concatizar. Esto es simplemente un problema de formato / sangría de codificación y algo muy personal.

Seguro que se ve bien así:

var list = list1.Concat(list2).Concat(list3);

No es tan legible cuando se lee como:

var list = list1.Select(x = > x)
   .Concat(list2.Where(x => true)
   .Concat(list3.OrderBy(x => x));

O cuando parece:

return Normalize(list1, a, b)
    .Concat(Normalize(list2, b, c))
       .Concat(Normalize(list3, c, d));

o cualquiera que sea su formato preferido. Las cosas empeoran con concats más complejos. La razón de mi tipo de disonancia cognitiva con el estilo anterior es que la primera secuencia se encuentra fuera del Concatmétodo, mientras que las secuencias posteriores se encuentran dentro. Prefiero llamar al Concatmétodo estático directamente y no al estilo de extensión:

var list = Enumerable.Concat(list1.Select(x => x),
                             list2.Where(x => true));

Para más cantidad de concats de secuencias, llevo el mismo método estático que en OP:

public static IEnumerable<T> Concat<T>(params IEnumerable<T>[] sequences)
{
    return sequences.SelectMany(x => x);
}

Entonces puedo escribir:

return EnumerableEx.Concat
(
    list1.Select(x = > x),
    list2.Where(x => true),
    list3.OrderBy(x => x)
);

Se ve mejor. El nombre de clase adicional, por lo demás redundante, que tengo que escribir no es un problema para mí, considerando que mis secuencias se ven más limpias con la Concatllamada. Es un problema menor en C # 6 . Puedes simplemente escribir:

return Concat(list1.Select(x = > x),
              list2.Where(x => true),
              list3.OrderBy(x => x));

Ojalá tuviéramos operadores de concatenación de listas en C #, algo como:

list1 @ list2 // F#
list1 ++ list2 // Scala

Mucho más limpio de esa manera.

nawfal
fuente
4
Aprecio su preocupación por el estilo de codificación. Esto es mucho más elegante.
Bryan Rayner
Quizás una versión más pequeña de su respuesta se pueda combinar con la respuesta principal aquí.
Bryan Rayner
2
También me gusta este formato, aunque prefiero realizar las operaciones de listas individuales (por ejemplo Where, OrderBy) primero para mayor claridad, especialmente si son algo más complejas que este ejemplo.
brichins
27

Para el caso en el que hacer tener una colección de colecciones, es decir una List<List<T>>, Enumerable.Aggregatees una forma más elegante para combinar todas las listas en una sola:

var combined = lists.Aggregate((acc, list) => { return acc.Concat(list); });
astreltsov
fuente
1
¿Cómo se obtiene el listsaquí primero? Ese es el primer problema que tiene OP. Si tuviera una collection<collection>para empezar, entonces SelectManyes mucho más simple.
nawfal
No estoy seguro de qué sucedió, pero esto parece responder a la pregunta incorrecta. Respuesta editada para reflejar esto. Muchas personas parecen terminar aquí debido a la necesidad de combinar una Lista <Lista <T>>
astreltsov
7

Siempre puede usar Aggregate combinado con Concat ...

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

        IEnumerable<int> combined = new List<int>();
        combined = listOfLists.Aggregate(combined, (current, list) => current.Concat(list)).ToList();
drs
fuente
1
La mejor solución hasta ahora.
Oleg
@Oleg SelectManyes simplemente más simple.
nawfal
6

La única forma que veo es usar Concat()

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

 var result = foo.Concat(bar).Concat(tor);

Pero debes decidir qué es mejor:

var result = Combine(foo, bar, tor);

o

var result = foo.Concat(bar).Concat(tor);

Un punto por el Concat()que será una mejor opción es que será más obvio para otro desarrollador. Más legible y simple.

Restuta
fuente
3

Puede utilizar Union de la siguiente manera:

var combined=foo.Union(bar).Union(baz)...

Sin embargo, esto eliminará elementos idénticos, por lo que si los tiene, es posible que desee usarlos en su Concatlugar.

Michael Goldshteyn
fuente
4
¡No! Enumerable.Uniones una unión de conjuntos que no produce el resultado deseado (solo produce duplicados una vez).
Jason
Sí, necesito las instancias específicas de los objetos, ya que necesito hacer un procesamiento especial en ellos. Usar inten mi ejemplo podría haber desconcertado un poco a la gente; pero Unionno funcionará para mí en este caso.
Donut
2

Un par de técnicas que utilizan los inicializadores de colección:

asumiendo estas listas:

var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 4, 5, 6 };

SelectMany con un inicializador de matriz (no es realmente tan elegante para mí, pero no depende de ninguna función auxiliar):

var combined = new []{ list1, list2 }.SelectMany(x => x);

Defina una extensión de lista para Agregar que permita IEnumerable<T>en el List<T> inicializador :

public static class CollectionExtensions
{
    public static void Add<T>(this ICollection<T> collection, IEnumerable<T> items)
    {
        foreach (var item in items) collection.Add(item);
    }
}

Luego puede crear una nueva lista que contenga los elementos de los demás como este (también permite que se mezclen elementos individuales).

var combined = new List<int> { list1, list2, 7, 8 };
Dave Andersen
fuente
1

Dado que está comenzando con un montón de colecciones separadas, creo que su solución es bastante elegante. Vas a tener que hacer algo para unirlos.

Sintácticamente, sería más conveniente crear un método de extensión a partir de su método Combine, lo que lo haría disponible en cualquier lugar al que vaya.

Dave Swersky
fuente
Me gusta la idea del método de extensión, simplemente no quería reinventar la rueda si LINQ ya tenía un método que haría lo mismo (y sería inmediatamente comprensible para otros desarrolladores más adelante)
Donut
1

Todo lo que necesita es esto, para cualquier IEnumerable<IEnumerable<T>> lists:

var combined = lists.Aggregate((l1, l2) => l1.Concat(l2));

Esto combinará todos los elementos en listsuno IEnumerable<T>(con duplicados). Úselo en Unionlugar de Concatpara eliminar duplicados, como se indica en las otras respuestas.

Nir Maoz
fuente