Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is implicit child to parent conversion in generic type parameters not possible?

I've been learning about covariance and contravariance in C#, but I can't understand something:

class Program
{
    static void Main(string[] args)
    {
        PersonCollection<Person> personCollection = new PersonCollection<Person>();
        IMyCollection<Teacher> myCollection = personCollection; // Here's the error:
        // Cannot implicitly convert type 'PersonCollection<Teacher>' to 'IMyCollection<Person>'
    }
}
class Person { }
class Teacher : Person { }
interface IMyCollection<T> { }
class PersonCollection<T> : IMyCollection<T> { }

as we all know, we can implicitly convert an instance of the derived class into the base class. So, in the code above, while the 'Teacher' class derives from the 'Person' class, an IMyCollection<Teacher> can't be converted to an IMyCollection<Person>, Why?!

Note: I want to know the reason, not the solution.

like image 715
Arad Avatar asked Mar 25 '18 13:03

Arad


2 Answers

Note: I want to know the reason, not the solution

While the reason for this is exactly why contra- and covariance exist, let me quickly show you an explanation with your example that highlights why this does not work:

So let’s assume the following setup code:

PersonCollection<Person> personCollection = new PersonCollection<Person>();

personCollection.Add(new Teacher("Teacher A"));
personCollection.Add(new Teacher("Teacher B"));
personCollection.Add(new Student("Student A"));
personCollection.Add(new Student("Student B"));
personCollection.Add(new Student("Student C"));
personCollection.Add(new Student("Student D"));

So now, I have a PersonCollection<Person> with two Teacher and four Student objects (here, Student also inherits from Person). This is completely valid since any Teacher and Student is also a Person. So I can add the elements to the collection.

Now, imagine the following was allowed:

IMyCollection<Teacher> myCollection = personCollection;

Now, I have a myCollection which apparently contains Teacher objects. But since this is just a reference assignment, myCollection is still the exact same collection as personCollection.

So myCollection will contain four Student objects although its contract defines that it only contains Teacher element. Doing the following should be totally allowed going by the contract of the interface:

Teacher teacher = personCollection[4];

But personCollection[4] is Student C, so obviously this would not work.

Since the compiler cannot make this verification during this item assignment, and since we want type safety instead of runtime validation, the only sane way the compiler can prevent this is by not allowing you to cast your collection to a IMyCollection<Teacher>.

You could make your IMyCollection<T> contravariant by declaring it as IMyCollection<in T> which would fix your situation and allow you to make that assignment but at the same time it would prevent you from retrieving a Teacher object out of it since it’s not covariant.

Generally, the only way to both set and retrieve generic values from a collection is to make it invariant (which is the default), which is also why all generic collections in the BCL are invariant and only some interfaces are contra- or covariant (e.g. IEnumerable<T> is covariant since it’s only about retrieving values).


Since you changed the error inside of your question to “Cannot implicitly convert type 'PersonCollection' to 'IMyCollection'”, let me also explain this case (turning this answer into a full contra- & covariance answer *sigh*…).

So the code would be the following for that:

PersonCollection<Teacher> personCollection = new PersonCollection<Teacher>();
IMyCollection<Person> myCollection = personCollection;

Again, let’s assume that this is valid and works. So now, we have a IMyCollection<Person> we can work with! So let’s add some persons here:

myCollection.Add(new Teacher("Teacher A"));
myCollection.Add(new Teacher("Teacher B"));
myCollection.Add(new Student("Student A"));

Whoops! The actual collection is still a PersonCollection<Teacher> which can only take Teacher objects. But the IMyCollection<Person> type allows us to add Student objects which are also persons! So this would fail at run-time, and again, since we want type safety at compile-time, the compiler has to disallow the assignment here.

This kind of assignment would only be valid for a covariant IMyCollection<out T> but that would also disallow us from adding elements of type T to it (for the same reasons as above).

Now, instead of adding to the PersonCollection<Teacher> here, let’s just work with the

like image 194
poke Avatar answered Oct 18 '22 01:10

poke


Going by your source code as the source of truth, you actually misread the error.

So, in the code above, while the class 'Teacher', is derived from class 'Person', the IMyCollection<Teacher> can't be converted to IMyCollection<Person> ,Why?

The actual error is

Cannot implicitly convert type PersonCollection<Person> to IMyCollection<Teacher>.

From an OO perspective at least, this is expected behavior. It's important to understand that the default T is invariant. That's why you ran into this problem to begin with. Meaning if T is Teacher then T can only be Teacher and not Person. Similarly, if T is Person then T can only be Person and not Teacher.

This is because Covariance and Contravariance are mutually exclusive. There are ways to support both, but you have to split things up into interfaces that support Invariance, Covariance and Contravariance seperately. In your case, you would need to add support for Contravariance. E.G.

interface IMyCollection<in T> { }

In other words, what comes into ( see in generic modifier keyword) the interface can be of type T. Not what "goes out" (see out generic modifier keyword) or is returned from your interface (that would be Covariance).

like image 1
P.Brian.Mackey Avatar answered Oct 18 '22 01:10

P.Brian.Mackey