19

Is it possible to cast a List<Subclass> to List<Superclass> in C# 4.0?

Something along these lines:

class joe : human {}

List<joe> joes = GetJoes();

List<human> humanJoes = joes;

Isn't this what covariance is for?

if you can do:

human h = joe1 as human;

why shouldn't you be able to do

List<human> humans = joes as List<human>; 

than it wouldn't be legal to do (joe)humans[0] because that item has been down casted.. and everyone would be happy. Now the only alternative is to create a new List

Sonic Soul
  • 23,855
  • 37
  • 130
  • 196
  • 2
    This is basically the same as [In C#, why can't a List object be stored in a List variable](http://stackoverflow.com/questions/6557/in-c-why-cant-a-liststring-object-be-stored-in-a-listobject-variable). – Matthew Flaschen Oct 27 '10 at 22:15
  • because `humans` would then be referring to an instance of `List`, which would cause problems as illustrated in @Jon's example. – Jeff Ogata Oct 27 '10 at 22:55
  • yep, after he corrected the example i got it.. makes sense – Sonic Soul Oct 28 '10 at 15:01

5 Answers5

26

You can't do this, because it wouldn't be safe. Consider:

List<Joe> joes = GetJoes();    
List<Human> humanJoes = joes;
humanJoes.Clear();
humanJoes.Add(new Fred());
Joe joe = joes[0];

Clearly the last line (if not an earlier one) has to fail - as a Fred isn't a Joe. The invariance of List<T> prevents this mistake at compile time instead of execution time.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • well humanJoes is a list of Human, so you shouldn't be attempting (or able) to extra a Joe from it. but each joe is a human, so it should be safe to cast all to subclass safely. – Sonic Soul Oct 27 '10 at 22:22
  • you certainly can do Human h = joesList[0] as Human.. and that's what i was looking for / asking about.. accept on a whole list level – Sonic Soul Oct 27 '10 at 22:23
  • @Sonic: Why wouldn't you be add an extra `Joe` or `Fred` to it? *That* sort of variance is *always* acceptable. Try it: `List list = new List(); list.Add("this is a string)";`. Why would you expect that to fail? – Jon Skeet Oct 27 '10 at 22:31
  • Shouldn't the last line be `Joe joe = joes[0];`? – Ian Henry Oct 27 '10 at 22:32
  • just pointing out that your example should not compile, but that should not prevent the ability to downcast a list. if you can do Human h = joe as Human. Why can't you do List h = List as List – Sonic Soul Oct 27 '10 at 22:41
  • @Sonic: Doh - my last line was wrong. Now it would compile, if the second line were valid - and it would have to fail at execution time. Both `joes` and `humanJoes` would refer to the same list... but `joes` is meant to be a list which *only* has `Joe` elements (or subclasses). – Jon Skeet Oct 27 '10 at 22:48
  • @Ian: Yes, it should. Kinda messed up the example - hopefully Sonic will still take another look and see the point though :) – Jon Skeet Oct 27 '10 at 22:49
  • oh.. i see what you did there :) – Sonic Soul Oct 27 '10 at 23:02
  • @JonSkeet: I don't understand the design decision to make this fail at compile-time. If I do `Human person = new Fred();` and `Joe person2 = (Joe) person;` I will get a run-time error. If I can add a `Joe` instance to a `List`, why can't `List` be widened to `List`? It seems like the CLR should allow line 2 and just throw an `InvalidCastException` on line 5, if only for consistency with the type system. – Rob Nov 17 '11 at 17:20
  • @robjb: Other than array covariance, the type system ensures that type failures only come through deliberate actions (such as the cast in your example). A cast indicates to the compiler, "I know you can't prove this is okay, but I believe it is. Please check at execution time." The last line *should* be safe - if you've genuinely got a `List`, that should only contain `Joe` values. You shouldn't need to worry about every single statement in your code for type safety. – Jon Skeet Nov 17 '11 at 17:28
6

Instantiate a new human-list that takes the joes as input:

List<human> humanJoes = new List<human>(joes);
Paw Baltzersen
  • 2,662
  • 3
  • 24
  • 33
2

No. The co/contravariance features of C# 4.0 only support interfaces and delegates. The do not support concrete types like List<T>.

JaredPar
  • 733,204
  • 149
  • 1,241
  • 1,454
1

No. As Jared said, the co/contravariance features of C# 4.0 only support interfaces and delegates. However it doesn't work with IList<T> either, and the reason is that IList<T> contains methods to add and change items in the list -- as Jon Skeet's new answer says.

The only way to be able to cast a list of "joe" to "human" is if the interface is purely read-only by design, something like this:

public interface IListReader<out T> : IEnumerable<T>
{
    T this[int index] { get; }
    int Count { get; }
}

Even a Contains(T item) method would not be allowed, because when you cast IListReader<joe> to IListReader<human>, there is no Contains(human item) method in IListReader<joe>.

You could "force" a cast from IList<joe> to IListReader<joe>, IListReader<human> or even IList<human> using a GoInterface. But if the list is small enough to copy, a simpler solution is to just copy it into a new List<human>, as Paw pointed out.

Qwertie
  • 16,354
  • 20
  • 105
  • 148
0

If I allow your List<Joe> joes to be generalized as ...

  List<Human> humans = joes;

... the two references humans and joes are, now onward, pointing to the exact same list. The code following the above assignment has no way of preventing an insertion/addition of an instance of another type of human , say a Plumber, into the list. Given that class Plumber: Human {}

humans.Add(new Plumber()); // Add() now accepts any Human not just a Joe 

the list that humans refers to now contains both joes and plumbers. Note that the same list object is still referred to by the reference joes. Now if I use the reference joes to read from the list object I might pop out a plumber instead of a joe. Plumber and Joe are not known to be implicitly interconvertable... so my getting of a plumber instead of a joe from the list breaks down type safety. A plumber is certainly not welcome through a reference to a list of joes.

However in the recent versions of C# , its kind of possible to work around this limitation for a generic class/collection by implementing a generic interface whose type parameter has an out modifier on it. Say we now have ABag<T> : ICovariable<out T>. The out modifier restricts the T to ouput positions only (e.g. method return types). You cannot enter any T into the bag. You can only read them out of it. This allows us to generalize joes to an ICovariable<Human> without worrying about inserting a Plumber into it as the interface doesnt allow that. We can now write ...

ICovariable<Human> humans = joes ; // now its good !
humans.Add(new Plumber()); // error 
explorer
  • 11,710
  • 5
  • 32
  • 39