Will Custode
Will Custode

Reputation: 4594

Covariance with C# Generics

Given an interface IQuestion and an implementation of that interface AMQuestion, suppose the following example:

List<AMQuestion> typed = new List<AMQuestion>();
IList<IQuestion> nonTyped = typed;

This example yields, as expected, a compile error saying the two are not of the same type. But it states an explicit conversion exists. So I change it to look like this:

List<AMQuestion> typed = new List<AMQuestion>();
IList<IQuestion> nonTyped = typed as IList<IQuestion>;

Which then compiles but, at run time, nonTyped is always null. If someone could explain two things:

It would be greatly appreciated. Thank you!

Upvotes: 22

Views: 1552

Answers (7)

Olivier Jacot-Descombes
Olivier Jacot-Descombes

Reputation: 112334

A type with a generic type parameter can only be covariant if this generic type occurs only in read accesses and contravariant, if it occurs only in write accesses. IList<T> allows both, read and write access to values of type T, so it cannot be variant!

Let's assume that you were allowed to assign a List<AMQuestion> to a variable of type IList<IQuestion>. Now let’s implement a class XYQuestion : IQuestion and insert a value of that type into our IList<IQuestion>, which seems perfectly legal. This list still references a List<AMQuestion>, but we cannot insert a XYQuestion into a List<AMQuestion>! Therefore the two list types are not assignment compatible.

IList<IQuestion> list = new List<AMQuestion>(); // Not allowed!
list.Add(new XYQuestion()); // Uuups!

Upvotes: 2

Rotem
Rotem

Reputation: 21917

The fact that AMQuestion implements the IQuestion interface does not translate into List<AMQuestion> deriving from List<IQuestion>.

Because this cast is illegal, your as operator returns null.

You must cast each item individually as such:

IList<IQuestion> nonTyped = typed.Cast<IQuestion>().ToList();

Regarding your comment, consider the following code, with the usual cliché animal examples:

//Lizard and Donkey inherit from Animal
List<Lizard> lizards = new List<Lizard> { new Lizard() };
List<Donkey> donkeys = new List<Donkey> { new Donkey() };

List<Animal> animals = lizards as List<Animal>; //let's pretend this doesn't return null
animals.Add(new Donkey()); //Reality unravels!

if we were allowed to cast List<Lizard> to a List<Animal>, then we could theoretically add a new Donkey to that list, which would break inheritance.

Upvotes: 32

supercat
supercat

Reputation: 81149

Because List<T> is not a sealed class, it would be possible for a type to exist which would inherit from List<AMQuestion> and implement IList<IQuestion>. Unless you implement such a type yourself, it's extremely unlikely that one will ever actually exist. Nonetheless, it would be perfectly legitimate to say, e.g.

class SillyList : List<AMQuestion>, IList<IQuestion> { ... }

and explicitly implement all the type-specific members of IList<IQuestion>. It would thus also be perfectly legitimate to say "If this variable holds a reference to an instance of a type derived from List<AMQuestion>, and if that instance's type also implements IList<IQuestion>, convert the reference to the latter type.

Upvotes: 1

chiccodoro
chiccodoro

Reputation: 14716

The issue is that List<AMQuestion> cannot be cast to IList<IQuestion>, so using the as operator does not help. Explicit conversion in this case means to cast AMQuestion to IQuestion:

IList<IQuestion> nonTyped = typed.Cast<IQuestion>.ToList();

By the way, you have the term "Covariance" in your title. In IList the type is not covariant. This is exactly why the cast does not exist. The reason is that the IList interface has T in some parameteres and in some return values, so neither in nor out can be used for T. (@Sneftel has a nice example to show why this cast is not allowed.)

If you only need to read from the list, you can use IEnumerable instead:

IEnumerable<IQuestion> = typed;

This will work because IEnumerable<out T> has out defined, since you can't pass it a T as parameter. You should usually make the weakest "promise" possible in your code to keep it extensible.

Upvotes: 7

jakobbotsch
jakobbotsch

Reputation: 6337

IList<T> is not covariant for T; it can't be, as the interface defines functions that take values of type T in an "input" position. However, IEnumerable<T> is covariant for T. If you can limit your type to IEnumerable<T>, you can do this:

List<AMQuestion> typed = new List<AMQuestion>();
IEnumerable<IQuestion> nonTyped = typed;

This does not do any conversions on the list.

The reason you cannot convert a List<AMQuestion> to a List<IQuestion> (assuming AMQuestion implements the interface) is that there would have to be several runtime checks on functions like List<T>.Add, to make sure you were really adding an AMQuestion.

Upvotes: 3

Immortal Blue
Immortal Blue

Reputation: 1700

The "as" operator will always return null there as no valid cast exists - this is defined behavior. You have to convert or cast the list like this:

IList<IQuestion> nonTyped = typed.Cast<IQuestion>().ToList();

Upvotes: 2

Sneftel
Sneftel

Reputation: 41474

Why it doesn't work: as returns null if the value's dynamic type cannot be casted to the target type, and List<AMQuestion> cannot be casted to IList<IQuestion>.

But why can't it? Well, check it:

List<AMQuestion> typed = new List<AMQuestion>();
IList<IQuestion> nonTyped = typed as IList<IQuestion>;
nonTyped.Add(new OTQuestion());
AMQuestion whaaaat = typed[0];

IList<IQuestion> says "You can add any IQuestion to me". But that's a promise it couldn't keep if it were a List<AMQuestion>.

Now, if you didn't want to add anything, just view it as a collection of IQuestion-compatible things, then the best thing to do would be to cast it to an IReadOnlyList<IQuestion> with List.AsReadOnly. Since a read-only list can't have strange things added to it, it can be casted properly.

Upvotes: 12

Related Questions