15

Why does the C# compiler not allow polymorphic type (T) parameters in generic collections (ie, List[T]) ?

Take class 'A' and 'B' for example, where 'B' is a subclass of 'A'

class A { }
class B : A { }

and consider a function that takes a list of type 'A'

void f(List<A> aL) { }

that gets called with a list of type 'B'

List<B> bL = new List<B>();

f(bL);

The following error is given

ERROR: cannot convert from List<B> to List<A>

What semantic rule is being violated ?

Also is there an "elegant" mean to this end, aside from looping through and casting each element (I want some sugar please) ? Thanks.

John Saunders
  • 160,644
  • 26
  • 247
  • 397
samus
  • 6,102
  • 6
  • 31
  • 69

6 Answers6

12

Take this little example as to why this cannot work. Imagine we have another subtype C of A:

class A {}
class B : A {}
class C : A {}

Then obviously, I can put a C object in a List<A> list. But now imagine the following function taking an A-list:

public void DoSomething (List<A> list)
{
    list.Add(new C());
}

If you pass a List<A> it works as expected because C is a valid type to put in a List<A>, but if you pass a List<B>, then you cannot put a C into that list.

For the general problem that’s happening here, see covariance and contravariance for arrays.

poke
  • 369,085
  • 72
  • 557
  • 602
12

List<B> simply is not a subtype of List<A>. (I'm never sure about what "covariant" and what "contravariant" is in this context so I'll stick with "subtype".) Consider the case where you do this:

void Fun(List<A> aa) {
    aa(new A());
}

var bb = new List<B>();
Fun(bb); // whoopsie

If what you want to do was allowed it would be possible to add an A to a list of Bs which is clearly not type-safe.

Now, clearly it's possible to read elements from the list safely, which is why C# lets you create covariant (i.e. "read-only") interfaces - which let the compiler know it's not possible to cause this sort of corruption through them. If you only need read access, for collections, the usual one is IEnumerable<T>, so in your case you might just make the method:

void Fun(IEnumerable<A> aa) { ... }

and use the Enumerable methods - most should be optimised if the underlying type is List.

Unfortunately, because of how the C# generics stuff works, classes can't be variant at all, only interfaces. And as far as I know, all the collection interfaces "richer" than IEnumerable<T> are "read-write". You could technically make your own covariant wrapper interface that only exposes the read operations you want.

millimoose
  • 39,073
  • 9
  • 82
  • 134
3

There's nothing inherently wrong with passing a collection of B to a method that expects a collection of A. However, there are many things that can go wrong depending on what you are going to do with the collection.

Consider:

void f(List<A> aL)
{
    aL.(new A()); // oops! what happens here?
}

Obviously there is a problem here: if aL were allowed to be a List<B> then this implementation would result in some type of runtime error, either on the spot or (much worse) if later on the code handles the A instance we put in as a B.

The compiler does not allow you to use a List<B> as a List<B> in order to preserve type safety and guarantee that your code will not need runtime checks to be correct. Note that this behavior is different than what (unfortunately) happens with arrays -- the language designer's decision is a tradeoff, and they decided differently on different occasions:

void f(A[] arr)
{
    arr[0] = new A(); // exception thrown at runtime
}

f(new B[1]);
Jon
  • 428,835
  • 81
  • 738
  • 806
1

I think you may be looking for 'out' generic modifiers, which allow for covariance between two generic types.

http://msdn.microsoft.com/en-us/library/dd469487.aspx

An example as posted on that page:

// Covariant delegate. 
public delegate R DCovariant<out R>();

// Methods that match the delegate signature. 
public static Control SampleControl()
{ return new Control(); }

public static Button SampleButton()
{ return new Button(); }

public void Test()
{            
    // Instantiate the delegates with the methods.
    DCovariant<Control> dControl = SampleControl;
    DCovariant<Button> dButton = SampleButton;

    // You can assign dButton to dControl 
    // because the DCovariant delegate is covariant.
    dControl = dButton;

    // Invoke the delegate.
    dControl(); 
}

I'm not sure whether C# currently supports covariance for its current collections.

Kenogu Labz
  • 1,094
  • 1
  • 9
  • 20
  • Excellent example, this should help a lot when I can get some time to explore this concept of convariance. Thanks Kenogu. – samus Jul 30 '13 at 18:03
1

You error is that B inherits from A; but List<B> don't inherits from List<A>. List<A> != A;

You can do this:

List<A> aL = new List<A>();
aL.Add(new B());

f (aL)

You can detect the type in void f(List<A> list)

foreach(A a in list)
{
  if (a is B)
    //Do B stuff
  else
    //Do A stuff
}
mtzaldo
  • 105
  • 4
  • Ahh, I was suspecting this, but don't like cluttering my questions with my own hypothetical answers, and so didn't mention. Thank you. – samus Jul 30 '13 at 16:53
0

your question is very similar to mine: the answer is that you can't do that cast because thoose are diferent types create by a template class and they do not inherit. what you can do is:

f(bL.Cast<A>());
Community
  • 1
  • 1
No Idea For Name
  • 11,411
  • 10
  • 42
  • 70
  • +1 Your "different types" explaination gives a precise reason why this doesn't work, so thank you, and nice syntax, I'll give it a try. – samus Jul 30 '13 at 18:05
  • @samusArin wow, i've never seen anyone putting so much effort commenting every answe! just for that i'll search every good answer you gave and +1 them. keep up the good work – No Idea For Name Jul 30 '13 at 20:51
  • Thank you, but you really didn't have to... I do it b/c I appreciate all helpful answers, even though I can only accept one. I think its quiet rude to ignore people that are helping you, let alone at least acknowledge their help and efforts !! – samus Jul 31 '13 at 15:18