AddRange à une collection

109

Un collègue m'a demandé aujourd'hui comment ajouter une gamme à une collection. Il a une classe qui hérite de Collection<T>. Il existe une propriété get-only de ce type qui contient déjà certains éléments. Il souhaite ajouter les éléments d'une autre collection à la collection de propriétés. Comment peut-il le faire de manière conviviale C # 3? (Notez la contrainte concernant la propriété get-only, qui empêche des solutions telles que faire Union et réaffecter.)

Bien sûr, un foreach avec la propriété. L'ajout fonctionnera. Mais un List<T>style AddRange serait beaucoup plus élégant.

Il est assez facile d'écrire une méthode d'extension:

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

Mais j'ai le sentiment de réinventer la roue. Je n'ai rien trouvé de similaire dans System.Linqou morelinq .

Mauvaise conception? Appelez simplement Ajouter? Vous manquez l'évidence?

TrueWill
la source
5
Rappelez-vous que le Q de LINQ est une `` requête '' et concerne en réalité l'extraction de données, la projection, la transformation, etc. La modification de collections existantes ne relève vraiment pas de l'objectif visé par LINQ, c'est pourquoi LINQ ne fournit rien - de la boîte pour cela. Mais les méthodes d'extension (et en particulier votre exemple) seraient idéales pour cela.
Levi
Un problème, ICollection<T>ne semble pas avoir de Addméthode. msdn.microsoft.com/en-us/library/... Cependant en Collection<T>a un.
Tim Goodman
@TimGoodman - C'est l'interface non générique. Voir msdn.microsoft.com/en-us/library/92t2ye13.aspx
TrueWill
«La modification de collections existantes ne relève pas vraiment de l'objectif de LINQ». @Levi Alors pourquoi même avoir Add(T item)en premier lieu? Cela ressemble à une approche à moitié cuite pour offrir la possibilité d'ajouter un seul élément, puis de s'attendre à ce que tous les appelants itèrent pour en ajouter plusieurs à la fois. Votre déclaration est certainement vraie, IEnumerable<T>mais je me suis retrouvé frustré à ICollectionsplus d'une occasion. Je ne suis pas en désaccord avec vous.
akousmata

Réponses:

62

Non, cela semble parfaitement raisonnable. Il existe une méthode List<T>.AddRange () qui fait exactement cela, mais nécessite que votre collection soit un béton List<T>.

Reed Copsey
la source
1
Merci; très vrai, mais la plupart des propriétés publiques suivent les directives MS et ne sont pas des listes.
TrueWill
7
Ouais - je donnais plus de raisons pour expliquer pourquoi je ne pense pas qu'il y ait un problème à faire cela. Sachez simplement que ce sera moins efficace que la version List <T> (puisque la liste <T> peut pré-allouer)
Reed Copsey
Veillez simplement à ce que la méthode AddRange dans .NET Core 2.2 puisse montrer un comportement étrange si elle est utilisée de manière incorrecte, comme indiqué dans ce numéro: github.com/dotnet/core/issues/2667
Bruno
36

Essayez de lancer un cast en List dans la méthode d'extension avant d'exécuter la boucle. De cette façon, vous pouvez profiter des performances 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
la source
2
L' asopérateur ne lancera jamais. Si destinationne peut pas être casté, listsera nul et le elsebloc s'exécutera.
rymdsmurf
4
arrgggh! Échangez les branches de condition, pour l'amour de tout ce qui est saint!
nicodemus13
13
Je suis sérieux, en fait, la principale raison est que c'est une charge cognitive supplémentaire, qui est souvent très difficile. Vous essayez constamment d'évaluer des conditions négatives, ce qui est généralement relativement difficile, vous avez les deux branches de toute façon, il est (IMO) plus facile de dire `` si nul '', `` sinon '', faites cela, plutôt que l'inverse. Il s'agit aussi de valeurs par défaut, elles devraient être le concept positif aussi souvent que possible, .eg `if (! Thing.IsDisabled) {} ​​else {} 'vous oblige à vous arrêter et à penser' ah, not is disabled signifie est activé, d'accord, obtenu cela, donc l'autre branche est lorsqu'elle est désactivée). Difficile à analyser.
nicodemus13
13
L'interprétation de "quelque chose! = Null" n'est pas plus difficile que d'interpréter "quelque chose == null". L'opérateur de négation est cependant une chose complètement différente, et dans votre dernier exemple, la réécriture de l'instruction if-else éliminerait cet opérateur. C'est objectivement une amélioration, mais qui n'est pas liée à la question initiale. Dans ce cas particulier, les deux formes sont une question de préférences personnelles, et je préférerais l'opérateur "! =" -, étant donné le raisonnement ci-dessus.
rymdsmurf
15
La correspondance des motifs rendra tout le monde heureux ... ;-)if (destination is List<T> list)
Jacob Foshee
28

Puisque .NET4.5si vous voulez une seule ligne, vous pouvez utiliser System.Collections.GenericForEach.

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

ou même plus court que

source.ForEach(destination.Add);

En termes de performances, c'est la même chose que pour chaque boucle (sucre syntaxique).

Aussi n'essayez d' affecter comme

var x = source.ForEach(destination.Add) 

la cause ForEachest nulle.

Edit: Copié à partir des commentaires, l'opinion de Lipert sur ForEach

Matas Vaitkevicius
la source
9
Personnellement, je suis avec Lippert sur celui-ci: blogs.msdn.com/b/ericlippert/archive/2009/05/18
...
1
Doit-il être source.ForEach (destination.Add)?
Frank
4
ForEachsemble être défini uniquement sur List<T>, non Collection?
Protector one
Lippert peut maintenant être trouvé à web.archive.org/web/20190316010649/https
//...
Mise à jour du lien vers le billet de blog d'Eric Lippert: Fabulous Adventures in Coding | "Foreach" vs "ForEach"
Alexander
19

N'oubliez pas que chacun Addvérifiera la capacité de la collection et la redimensionnera chaque fois que nécessaire (plus lentement). Avec AddRange, la collection sera définie la capacité, puis ajouté les éléments (plus rapide). Cette méthode d'extension sera extrêmement lente, mais fonctionnera.

jvitor83
la source
3
Pour ajouter à cela, il y aura également une notification de modification de collection pour chaque ajout, par opposition à une notification en masse avec AddRange.
Nick Udell
3

Voici une version un peu plus avancée / prête pour la production:

    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
la source
La réponse de rymdsmurf peut sembler naïve, trop simple, mais elle fonctionne avec des listes hétérogènes. Est-il possible de faire en sorte que ce code prenne en charge ce cas d'utilisation?
richardsonwtr
Par exemple: destinationest une liste de Shape, une classe abstraite. sourceest une liste de Circle, une classe héritée.
richardsonwtr
1

Les classes de la bibliothèque de collections génériques C5 prennent toutes en charge la AddRangeméthode. C5 a une interface beaucoup plus robuste qui expose en fait toutes les fonctionnalités de ses implémentations sous-jacentes et est compatible avec les interfaces System.Collections.Generic ICollectionet IList, ce qui signifie que C5les collections de s peuvent être facilement remplacées comme implémentation sous-jacente.

Marcus Griep
la source
0

Vous pouvez ajouter votre plage IEnumerable à une liste, puis définir ICollection = sur la liste.

        IEnumerable<T> source;

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

        ICollection<item> destination = list;
Jonathan Jansen
la source
3
Bien que cela fonctionne fonctionnellement, cela enfreint les directives de Microsoft pour rendre les propriétés de la collection en lecture seule ( msdn.microsoft.com/en-us/library/ms182327.aspx )
Nick Udell
0

Ou vous pouvez simplement créer une extension ICollection comme ceci:

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

        return @this;
    }

L'utiliser serait comme l'utiliser sur une liste:

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