AddRange a uma coleção

109

Um colega de trabalho me perguntou hoje como adicionar um intervalo a uma coleção. Ele tem uma classe que herda Collection<T>. Há uma propriedade get-only desse tipo que já contém alguns itens. Ele deseja adicionar os itens de outra coleção à coleção de propriedades. Como ele pode fazer isso de uma forma amigável com C # 3? (Observe a restrição sobre a propriedade get-only, que impede soluções como fazer Union e reatribuir.)

Claro, um foreach com Property. Adicionar funcionará. Mas um List<T>AddRange no estilo seria muito mais elegante.

É fácil escrever um método de extensão:

public static class CollectionHelpers
{
    public static void AddRange<T>(this ICollection<T> destination,
                                   IEnumerable<T> source)
    {
        foreach (T item in source)
        {
            destination.Add(item);
        }
    }
}

Mas tenho a sensação de que estou reinventando a roda. Não encontrei nada semelhante em System.Linqou morelinq .

Design ruim? Basta ligar para adicionar? Perdendo o óbvio?

TrueWill
fonte
5
Lembre-se de que o Q do LINQ é 'consulta' e é realmente sobre recuperação de dados, projeção, transformação etc. pronto para isso. Mas os métodos de extensão (e em particular sua amostra) seriam ideais para isso.
Levi,
Um problema, ICollection<T>não parece haver um Addmétodo. msdn.microsoft.com/en-us/library/… No entanto, Collection<T>tem um.
Tim Goodman
@TimGoodman - Essa é a interface não genérica. Consulte msdn.microsoft.com/en-us/library/92t2ye13.aspx
TrueWill
"Modificar coleções existentes realmente não se enquadra no objetivo pretendido do LINQ". @Levi Então, por que ter feito isso Add(T item)em primeiro lugar? Parece uma abordagem incompleta para oferecer a capacidade de adicionar um único item e então esperar que todos os chamadores iterem para adicionar mais de um de cada vez. Sua afirmação é certamente verdadeira para, IEnumerable<T>mas me senti frustrado ICollectionsem mais de uma ocasião. Eu não discordo de você, apenas desabafando.
akousmata

Respostas:

62

Não, isso parece perfeitamente razoável. Existe um método List<T>.AddRange () que basicamente faz exatamente isso, mas requer que sua coleção seja concreta List<T>.

Reed Copsey
fonte
1
Obrigado; muito verdadeiro, mas a maioria das propriedades públicas segue as diretrizes da MS e não são listas.
TrueWill
7
Sim - eu estava dando mais uma justificativa para o porquê eu não acho que há um problema em fazer isso. Apenas perceba que será menos eficiente do que a versão List <T> (uma vez que a lista <T> pode pré-alocar)
Reed Copsey
Apenas tome cuidado para que o método AddRange no .NET Core 2.2 possa apresentar um comportamento estranho se usado incorretamente, conforme mostrado neste problema: github.com/dotnet/core/issues/2667
Bruno
36

Tente lançar em List no método de extensão antes de executar o loop. Dessa forma, você pode aproveitar o desempenho de List.AddRange.

public static void AddRange<T>(this ICollection<T> destination,
                               IEnumerable<T> source)
{
    List<T> list = destination as List<T>;

    if (list != null)
    {
        list.AddRange(source);
    }
    else
    {
        foreach (T item in source)
        {
            destination.Add(item);
        }
    }
}
rymdsmurf
fonte
2
O asoperador nunca vai jogar. Se destinationnão puder ser convertido, listserá nulo e o elsebloco será executado.
rymdsmurf
4
arrgggh! Troque os ramos de condição, pelo amor de tudo que é sagrado!
nicodemus13
13
Estou falando sério, na verdade. O principal motivo é que é uma carga cognitiva extra, o que muitas vezes é muito difícil. Você está constantemente tentando avaliar condições negativas, o que geralmente é relativamente difícil, você tem ambos os ramos de qualquer maneira, é (IMO) mais fácil dizer 'se nulo' faça isso, 'senão' faça isso, ao invés do oposto. É também sobre padrões, eles devem ser o conceito positivo sempre que possível, por exemplo, `if (! Thing.IsDisabled) {} ​​else {} 'requer que você pare e pense' ah, não está desativado significa está ativado, certo, consegui isso, então o outro ramo é quando está desabilitado). Difícil de analisar.
nicodemus13 de
13
Interpretar "algo! = Nulo" não é mais difícil do que interpretar "algo == nulo". O operador de negação, entretanto, é uma coisa completamente diferente e, em seu último exemplo, reescrever a instrução if-else eliminaria esse operador. Essa é uma melhoria objetivamente, mas que não está relacionada com a questão original. Nesse caso particular, as duas formas são uma questão de preferências pessoais, e eu preferiria o operador "! =" -, dado o raciocínio acima.
rymdsmurf
15
A correspondência de padrões deixará todos felizes ... ;-)if (destination is List<T> list)
Jacob Foshee
28

Já que .NET4.5se você quiser uma linha, você pode usar System.Collections.GenericForEach.

source.ForEach(o => destination.Add(o));

ou ainda mais curto como

source.ForEach(destination.Add);

Em termos de desempenho, é o mesmo que para cada loop (açúcar sintático).

Também não tente atribuí-lo como

var x = source.ForEach(destination.Add) 

a causa ForEaché nula.

Edit: Copiado de comentários, opinião de Lipert sobre ForEach

Matas Vaitkevicius
fonte
9
Pessoalmente, estou com Lippert neste aqui: blogs.msdn.com/b/ericlippert/archive/2009/05/18/…
TrueWill
1
Deve ser source.ForEach (destination.Add)?
Frank,
4
ForEachparece ser definido apenas em List<T>, não Collection?
Protetor um
Lippert agora pode ser encontrado em web.archive.org/web/20190316010649/https://…
user7610
Link atualizado para a postagem do blog de Eric Lippert: Fabulous Adventures in Coding | “Foreach” vs “ForEach”
Alexander
19

Lembre-se de que cada Addum verificará a capacidade da coleção e a redimensionará sempre que necessário (mais lento). Com AddRange, a coleção será definida a capacidade e, em seguida, adicionados os itens (mais rápido). Este método de extensão será extremamente lento, mas funcionará.

jvitor83
fonte
3
Para adicionar a isso, também haverá uma notificação de alteração de coleção para cada adição, ao contrário de uma notificação em massa com AddRange.
Nick Udell
3

Aqui está uma versão um pouco mais avançada / pronta para produção:

    public static class CollectionExtensions
    {
        public static TCol AddRange<TCol, TItem>(this TCol destination, IEnumerable<TItem> source)
            where TCol : ICollection<TItem>
        {
            if(destination == null) throw new ArgumentNullException(nameof(destination));
            if(source == null) throw new ArgumentNullException(nameof(source));

            // don't cast to IList to prevent recursion
            if (destination is List<TItem> list)
            {
                list.AddRange(source);
                return destination;
            }

            foreach (var item in source)
            {
                destination.Add(item);
            }

            return destination;
        }
    }
MovGP0
fonte
A resposta de rymdsmurf pode parecer ingênua, simples demais, mas funciona com listas heterogêneas. É possível fazer com que este código suporte este caso de uso?
richardsonwtr
Ex: destinationé uma lista de Shapeuma classe abstrata. sourceé uma lista de Circleuma classe herdada.
richardsonwtr
1

Todas as classes C5 Generic Collections Library suportam o AddRangemétodo. C5 tem uma interface muito mais robusta que realmente expõe todos os recursos de suas implementações subjacentes e é compatível com as interfaces System.Collections.Generic ICollectione IList, o que significa que C5as coleções podem ser facilmente substituídas como a implementação subjacente.

Marcus Griep
fonte
0

Você pode adicionar seu intervalo IEnumerable a uma lista e definir ICollection = para a lista.

        IEnumerable<T> source;

        List<item> list = new List<item>();
        list.AddRange(source);

        ICollection<item> destination = list;
Jonathan Jansen
fonte
3
Embora funcione funcionalmente, ele quebra as diretrizes da Microsoft para tornar as propriedades da coleção somente leitura ( msdn.microsoft.com/en-us/library/ms182327.aspx )
Nick Udell
0

Ou você pode apenas fazer uma extensão ICollection como esta:

 public static ICollection<T> AddRange<T>(this ICollection<T> @this, IEnumerable<T> items)
    {
        foreach(var item in items)
        {
            @this.Add(item);
        }

        return @this;
    }

Usá-lo seria como usá-lo em uma lista:

collectionA.AddRange(IEnumerable<object> items);
Katarina Kelam
fonte