125

A coworker asked me today how to add a range to a collection. He has a class that inherits from Collection<T>. There's a get-only property of that type that already contains some items. He wants to add the items in another collection to the property collection. How can he do so in a C#3-friendly fashion? (Note the constraint about the get-only property, which prevents solutions like doing Union and reassigning.)

Sure, a foreach with Property. Add will work. But a List<T>-style AddRange would be far more elegant.

It's easy enough to write an extension method:

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

But I have the feeling I'm reinventing the wheel. I didn't find anything similar in System.Linq or morelinq.

Bad design? Just Call Add? Missing the obvious?

Juan
  • 4,291
  • 3
  • 36
  • 40
TrueWill
  • 23,842
  • 7
  • 88
  • 133
  • 5
    Remember that the Q from LINQ is 'query' and is really about data retrieval, projection, transformation, etc. Modifying existing collections really doesn't fall into the realm of LINQ's intended purpose, which is why LINQ doesn't provide anything out-of-the-box for this. But extension methods (and in particular your sample) would be ideal for this. – Levi Sep 25 '09 at 00:46
  • One problem, `ICollection` does not seem to have an `Add` method. http://msdn.microsoft.com/en-us/library/system.collections.icollection_methods(v=vs.100).aspx However `Collection` has one. – Tim Goodman Jul 11 '13 at 05:58
  • @TimGoodman - That's the non-generic interface. See http://msdn.microsoft.com/en-us/library/92t2ye13.aspx – TrueWill Jul 11 '13 at 17:26
  • "Modifying existing collections really doesn't fall into the realm of LINQ's intended purpose". @Levi Then why even have `Add(T item)` in the first place? Seems like a half-baked approach to offer the ability to add a single item and then expect all callers to iterate in order to add more than one at a time. Your statement is certainly true for `IEnumerable` but I have found myself frustrated with `ICollections` on more than one occasion. I don't disagree with you, just venting. – akousmata Mar 18 '19 at 16:57

9 Answers9

69

No, this seems perfectly reasonable. There is a List<T>.AddRange() method that basically does just this, but requires your collection to be a concrete List<T>.

Neuron
  • 3,776
  • 3
  • 24
  • 44
Reed Copsey
  • 522,342
  • 70
  • 1,092
  • 1,340
  • 1
    Thanks; very true, but most public properties follow the MS guidelines and are not Lists. – TrueWill Sep 25 '09 at 00:52
  • 7
    Yeah - I was giving it more as rationale for why I don't think there is a problem with doing this. Just realize it will be less efficient than the List version (since the list can pre-allocate) – Reed Copsey Sep 25 '09 at 01:12
  • Just take care that AddRange method in .NET Core 2.2 might show a weird behavior if used incorrectly, as shown in this issue: https://github.com/dotnet/core/issues/2667 – Bruno May 02 '19 at 12:58
40

Try casting to List in the extension method before running the loop. That way you can take advantage of the performance of 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);
        }
    }
}
TrueWill
  • 23,842
  • 7
  • 88
  • 133
rymdsmurf
  • 625
  • 5
  • 11
  • It may be a bit n00bish question but what happens when the ```destination``` collection can't be cast into a ```List```? Does ```list``` automatically become ```null``` or is an exception thrown? – Uzair Sajid Oct 28 '14 at 10:14
  • 3
    The `as` operator will never throw. If `destination` cannot be cast, `list` will be null and the `else` block will execute. – rymdsmurf Oct 29 '14 at 12:04
  • 4
    arrgggh! Swap the condition branches, for the love of all that's holy! – nicodemus13 Jul 15 '16 at 16:11
  • 3
    @nicodemus13, (assuming that you are at least a little bit serious) what are your reasons for swapping them? Is it not more natural to first consider the case which is the _only_ reason we are doing the check at all? – rymdsmurf Jul 16 '16 at 18:11
  • 14
    I am serious, actually.The main reason is that it's extra cognitive-load, which is often really quite difficult. You're constantly trying to evaluate negative conditions, which is usually relatively hard, you have both branches anyway, it's (IMO) easier to say 'if null' do this, 'else' do this, rather than the opposite. It's also about defaults, they should be the positive concept as often as possible, .e.g `if (!thing.IsDisabled) {} else {}' requires you to stop and think 'ah, not is disabled means is enabled, right, got that, so the other branch is when it IS disabled). Hard to parse. – nicodemus13 Jul 20 '16 at 07:42
  • 14
    Interpreting "something != null" is not more difficult than interpreting "something == null". The negation operator however is a completely different thing, and in your last example rewriting the if-else-statement would _elliminate_ that operator. That is a objectively an improvement, but one that is not related to the original question. In that particular case the two forms are a matter of personal preferences, and I would prefer the "!="-operator, given the reasoning above. – rymdsmurf Jul 25 '16 at 09:39
  • 21
    Pattern matching will make everyone happy... ;-) `if (destination is List list)` – Jacob Foshee Sep 01 '17 at 21:47
34

Since .NET4.5 if you want one-liner you can use System.Collections.Generic ForEach.

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

or even shorter as

source.ForEach(destination.Add);

Performance-wise it's the same as for each loop (syntactic sugar).

Also don't try assigning it like

var x = source.ForEach(destination.Add) 

cause ForEach is void.

Edit: Copied from comments, Lippert's opinion on ForEach.

Pang
  • 8,605
  • 144
  • 77
  • 113
Matas Vaitkevicius
  • 49,230
  • 25
  • 212
  • 228
  • 11
    Personally I'm with Lippert on this one: http://blogs.msdn.com/b/ericlippert/archive/2009/05/18/foreach-vs-foreach.aspx – TrueWill Jan 15 '15 at 16:21
  • 1
    Should it be source.ForEach(destination.Add) ? – Frank Sep 08 '16 at 20:04
  • 5
    `ForEach` seems to only be defined on `List`, not `Collection`? – Protector one Jul 13 '18 at 12:09
  • 1
    Lippert can now be found at https://web.archive.org/web/20190316010649/https://blogs.msdn.microsoft.com/ericlippert/2009/05/18/foreach-vs-foreach/ – user7610 Mar 21 '19 at 17:28
  • 2
    Updated link to Eric Lippert's blog post: [Fabulous Adventures in Coding | “foreach” vs “ForEach”](https://docs.microsoft.com/en-us/archive/blogs/ericlippert/foreach-vs-foreach) – Alexander Mar 10 '20 at 18:07
20

Remember that each Add will check the capacity of the collection and resize it whenever necessary (slower). With AddRange, the collection will be set the capacity and then added the items (faster). This extension method will be extremely slow, but will work.

jonsca
  • 9,342
  • 26
  • 53
  • 60
jvitor83
  • 120
  • 7
  • 19
  • 4
    To add to this, there will also be a collection change notification for each addition, as opposed to one bulk notification with AddRange. – Nick Udell Sep 22 '14 at 09:58
3

Here is a bit more advanced/production-ready version:

    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
  • 5,754
  • 2
  • 40
  • 37
  • [rymdsmurf's answer](https://stackoverflow.com/a/26360010/9990676) may look naive, too simple, but it works with heterogeneous lists. Is it possible to make this code support this use case? – richardsonwtr Aug 11 '20 at 18:07
  • E.g.: `destination` is a list of `Shape`, an abstract class. `source` is a list of `Circle`, an inherited class. – richardsonwtr Aug 11 '20 at 18:08
1

The C5 Generic Collections Library classes all support the AddRange method. C5 has a much more robust interface that actually exposes all of the features of its underlying implementations and is interface-compatible with the System.Collections.Generic ICollection and IList interfaces, meaning that C5's collections can be easily substituted as the underlying implementation.

Marcus Griep
  • 7,288
  • 1
  • 20
  • 23
0

You could add your IEnumerable range to a list then set the ICollection = to the list.

        IEnumerable<T> source;

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

        ICollection<item> destination = list;
  • 4
    While this functionally works, it breaks the Microsoft guidelines to make collection properties read only (http://msdn.microsoft.com/en-us/library/ms182327.aspx) – Nick Udell Sep 22 '14 at 09:56
0

Or you can just make an ICollection extension like this:

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

        return @this;
    }

Using it would be just like using it on a list:

collectionA.AddRange(IEnumerable<object> items);
0

Agree with some guys above and Lipert's opinion. In my case, it's quite often to do like this:

ICollection<int> A;
var B = new List<int> {1,2,3,4,5};
B.ForEach(A.Add);

To have an extension method for such operation a bit redundant in my view.

  • Your answer is not adding anything that was not already suggested by the numerous older answers. – EricSchaefer Feb 16 '21 at 07:03
  • what can I do when I want this: aList = If(True,Nothing,aList.AddRange(...)) In expressionB I want to addItems. But the If expression does not allow this. Expression makes no value... – Jürgen Scheffler Feb 26 '21 at 14:31
  • Perhaps I am not fully understood your question, but if it about to add items by the condition, it can be done like this: `ICollection A = new List(); var B = new List { 1, 2, 3, 4, 5 }; B.ForEach(s =>{ if (s > 3) A.Add(s); });` – Egor Sindeev Feb 28 '21 at 14:52