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.



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.


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.


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); });

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



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();

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.


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.


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 };

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.


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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.