125
votes

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?

9
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
One problem, ICollection<T> does not seem to have an Add method. msdn.microsoft.com/en-us/library/… However Collection<T> has one.Tim Goodman
@TimGoodman - That's the non-generic interface. See msdn.microsoft.com/en-us/library/92t2ye13.aspxTrueWill
"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<T> but I have found myself frustrated with ICollections on more than one occasion. I don't disagree with you, just venting.akousmata

9 Answers

69
votes

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>.

41
votes

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

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.

20
votes

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.

3
votes

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

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.

0
votes

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;
0
votes

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
votes

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.