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.
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
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 toIMyCollection<Person>
,Why?
The actual error is
Cannot implicitly convert type
PersonCollection<Person>
toIMyCollection<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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With