Maneira elegante de combinar várias coleções de elementos?

94

Digamos que eu tenha um número arbitrário de coleções, cada uma contendo objetos do mesmo tipo (por exemplo, List<int> fooe List<int> bar). Se essas coleções estivessem em uma coleção (por exemplo, do tipo List<List<int>>, eu poderia usar SelectManypara combiná-las todas em uma coleção.

No entanto, se essas coleções ainda não estiverem na mesma coleção, tenho a impressão de que teria que escrever um método como este:

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

Que eu então chamaria assim:

var combined = Combine(foo, bar);

Existe uma maneira limpa e elegante de combinar (qualquer número de) coleções sem ter que escrever um método utilitário como o Combineacima? Parece simples o suficiente para que haja uma maneira de fazer isso no LINQ, mas talvez não.

Rosquinha
fonte
Cuidado com o Concat para concatenar um número muito grande de enumeráveis, pode explodir sua pilha: programmaticallyspeaking.com/…
nawfal

Respostas:

107

Eu acho que você pode estar procurando LINQ .Concat()?

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

Alternativamente, .Union()irá remover elementos duplicados.

Domenic
fonte
2
Obrigado; a única razão pela qual não fiz isso em primeiro lugar é que (para mim) parece ficar mais feio quanto mais coleções você tem que lidar. No entanto, isso tem a vantagem de usar funções LINQ existentes, com as quais os desenvolvedores futuros provavelmente já estarão familiarizados.
Donut de
2
Obrigado pela .Union()dica. Tenha em mente que você precisará implementar IComparer em seu tipo personalizado, caso seja.
Gonzo345
29

Para mim, Concatcomo um método de extensão, não é muito elegante em meu código quando tenho várias sequências grandes para concat. Este é apenas um problema de recuo / formatação de código e algo muito pessoal.

Claro que fica bem assim:

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

Não é tão legível quando se lê como:

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

Ou quando parece:

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

ou qualquer que seja sua formatação preferida. As coisas pioram com concats mais complexos. A razão para meu tipo de dissonância cognitiva com o estilo acima é que a primeira sequência está fora do Concatmétodo, enquanto as sequências subsequentes estão dentro. Prefiro chamar o Concatmétodo estático diretamente e não o estilo de extensão:

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

Para mais número de concats de sequências, carrego o mesmo método estático do OP:

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

Então posso escrever:

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

Parece melhor. O nome de classe extra, caso contrário redundante, que tenho que escrever não é um problema para mim, considerando que minhas sequências parecem mais claras com a Concatchamada. É um problema menor em C # 6 . Você pode apenas escrever:

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

Gostaríamos de ter operadores de concatenação de lista em C #, algo como:

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

Muito mais limpo assim.

Nawfal
fonte
4
Agradeço sua preocupação com o estilo de codificação. Isso é muito mais elegante.
Bryan Rayner
Talvez uma versão menor de sua resposta possa ser mesclada com a resposta principal aqui.
Bryan Rayner
2
Eu gosto dessa formatação também - embora eu prefira executar as operações de lista individual (por exemplo Where, OrderBy) primeiro para maior clareza, especialmente se elas forem mais complexas do que este exemplo.
brichins
27

Para o caso quando você faz tem uma coleção de coleções, ou seja, um List<List<T>>, Enumerable.Aggregateé uma maneira mais elegante para combinar todas as listas em um:

var combined = lists.Aggregate((acc, list) => { return acc.Concat(list); });
Astreltsov
fonte
1
Como você consegue o listsaqui primeiro? Esse é o primeiro problema que o OP tem. Se ele tivesse um collection<collection>para começar, então SelectManyé muito mais simples.
nawfal
Não tenho certeza do que aconteceu, mas isso parece responder à pergunta errada. Resposta editada para refletir isso. Muitas pessoas parecem acabar aqui devido à necessidade de combinar uma Lista <Lista <T>>
astreltsov
14

Use Enumerable.Concatassim:

var combined = foo.Concat(bar).Concat(baz)....;
Jason
fonte
7

Você sempre pode usar Aggregate combinado com 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
fonte
1
A melhor solução até agora.
Oleg,
@Oleg SelectManyé apenas mais simples.
nawfal
6

A única maneira que vejo é 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);

Mas você deve decidir o que é melhor:

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

ou

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

Um ponto por Concat()que será uma escolha melhor é que será mais óbvio para outro desenvolvedor. Mais legível e simples.

Restuta
fonte
3

Você pode usar o Union da seguinte maneira:

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

Isso removerá elementos idênticos, portanto, se você os tiver, pode querer usá-los Concat.

Michael Goldshteyn
fonte
4
Não! Enumerable.Unioné uma união de conjuntos que não produz o resultado desejado (produz duplicatas apenas uma vez).
jason
Sim, preciso das instâncias específicas dos objetos, pois preciso fazer algum processamento especial neles. Usar intno meu exemplo pode ter confundido um pouco as pessoas; mas Unionnão funcionará para mim neste caso.
Donut de
2

Algumas técnicas usando inicializadores de coleção -

assumindo essas listas:

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

SelectMany com um inicializador de array (não muito elegante para mim, mas não depende de nenhuma função auxiliar):

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

Defina uma extensão de lista para Adicionar que permite IEnumerable<T>no 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);
    }
}

Em seguida, você pode criar uma nova lista contendo os elementos dos outros como esta (também permite que itens individuais sejam misturados).

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

Considerando que você está começando com um monte de coleções separadas, acho que sua solução é bastante elegante. Você vai ter que fazer algo para costurá-los juntos.

Seria mais conveniente sintaticamente fazer um método de extensão de seu método Combine, que o tornaria disponível em qualquer lugar que você fosse.

Dave Swersky
fonte
Gosto da ideia do método de extensão, só não queria reinventar a roda se o LINQ já tivesse um método que fizesse a mesma coisa (e fosse imediatamente compreensível para outros desenvolvedores mais adiante)
Donut
1

Tudo que você precisa é isso, para qualquer IEnumerable<IEnumerable<T>> lists:

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

Isso combinará todos os itens em listsum IEnumerable<T>(com duplicatas). Use em Unionvez de Concatpara remover duplicatas, conforme observado nas outras respostas.

Nir Maoz
fonte