views:

1095

answers:

5

I recently run into trouble when trying to AddRange(IEnumerable) to a List. Probably a classic issue, but I do not really get it yet.

I understand that methods expecting a List parameter are not satisfied with a List, because they might try to add a Base to the List, which is obviously impossible.

But if i get this correctly, since IEnumerables themselves cannot be changed, it ought to work in this case.

The code i thought of looks like this:

class Foo
{
}

class Bar : Foo
{
}

class FooCol
{
 private List<Foo> m_Foos = new List<Foo> ();

 public void AddRange1(IEnumerable<Foo> foos)
 {
  m_Foos.AddRange (foos); // does work
 }

 public void AddRange2<T>(IEnumerable<T> foos) where T : Foo
 {
  m_Foos.AddRange (foos); // does not work
 }
}

class Program
{
 static void Main(string[] args)
 {
  FooCol fooCol = new FooCol ();

  List<Foo> foos = new List<Foo> ();
  List<Bar> bars = new List<Bar> ();

  fooCol.AddRange1 (foos); // does work
  fooCol.AddRange1 (bars); // does not work

  fooCol.AddRange2 (foos); // does work
  fooCol.AddRange2 (bars); // does work
 }
}

I tried to pass a hint to the compiler in the AddRange2 method, but this just moved to problem around.

Is my way of thinking flawed? Is this a limitation of the language or is it by design?

IIRC, support for this kind of operations was added to Java 1.5, so maybe it will be added to C# at some point in the future, too...?

+13  A: 

This is covariance, and will be fixed in C# 4.0 / .NET 4.0. For now, the generic option is the best answer (for IEnumerable<T> - not IList<T> etc).

But within the generic method, you have to think in terms of T. You could also use Cast<T> or OfType<T> with LINQ to achieve something similar.

Marc Gravell
Thank you for the new term, covariance. Didn't know about the existence.
Sung Meister
A: 

There is workaround with extension method:

public static IEnumerable<TBase> ToBaseEnumerable<TBase, TDerived>( this IEnumerable<TDerived> items ) where TDerived : TBase {
    foreach( var item in items ) {
        yield return item;
    }
}
...
IEnumerable<Employee> employees = GetEmployees(); //Emplyoee derives from Person
DoSomethingWithPersons( employees.ToBaseEnumerable<Person, Employee>() );

but the "<Person, Employee>" is little bit awkward :/.

TcKs
Cast<Person>() would be simpler...
Marc Gravell
A: 

This seems to be (effectively) a duplicate of this recent SO thread.

Noldorin
+1  A: 

In C# 3.0 you can use the "Cast" extension method. If you import System.Linq and then use this code:

public void AddRange2<T>(IEnumerable<T> foos) where T : Foo
{
    m_Foos.AddRange (foos.Cast<Foo>());
}

Then it should work for you.

Scott Wisniewski
A: 

The cast solution of course might generated class cast exceptions. The person who posted the enumerable extension work around said it was awkward. I came up with a solution which is only half as awkward, don't know if I'll use it:

public static class UpTo<T>
{
    public static IEnumerable<T> From<F>(IEnumerable<F> source) where F:T
    {
        // this cast is guaranteed to work
        return source.Select(f => (T) f);
    }
}

Usage:

IEnumerable mammals = UpTo<Mammal>.From(kennel.Dogs)