This article is the third in a series on covariance and contravariance:
- Covariance in arrays
- Contravariance in interfaces
- Covariance in interfaces
In the previous article I showed how an interface could be contravariant, meaning it is expecting a smaller type for something you pass it. In this article I will show how an interface can be covariant, meaning it returns something with a bigger type than the place, e.g. variable, used to store it.
Code overview
This is much simpler than in the previous article. It uses a factory like the previous article, but the return types use the Animal / Bird hierarchy from the first article in the series.
I will define an interface for a factory, using a type variable. I will then create a class that implements that interface for a particular type. Finally, I will create an instance of that factory and use it to create an object. The covariance will be needed because of the difference between the return type of the factory, and the type of the variable used to store it.
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // RunCovariantCode doesn't compile until out is added to T, to mark it as a covariant type public interface IFactory<out T> { T GetInstance(); } private class BirdFactory : IFactory<Bird> { public Bird GetInstance() { return new Bird { Weight = 100, WingSpan = 200 }; } } public static void RunCovariantCode() { IFactory<Animal> animalFactory = new BirdFactory(); Animal instance = animalFactory.GetInstance(); Console.WriteLine("Thing returned from a BirdFactory that's assigned to an animal factory variable: " + instance.ToString()); } |
BirdFactory is a factory that returns Birds (as you might expect). Remember, as in the other variance examples, even though Bird is derived from Animal, IFactory<Bird> is not derived from IFactory<Animal> (or vice versa). This means it is invalid to assign an instance of BirdFactory to a variable with the type IFactory<Animal>, unless variance is there to save the day.
If you remove the out modifier from the type variable in the definition of IFactory, then the code will not compile. You need to add the out modifier to show that T is being used only for outputs in the interface (the return type of GetInstance) and so covariance is possible.
You could remove the out modifier if you also changed the type of animalFactory to either IFactory<Bird> or just var (which would just make the compiler infer that its type was IFactory<Bird>).
This is the first time in this series that a listing has included the in or out modifiers, because it’s the first time I’m creating new variant code rather than using existing variant code.
Output
The factory is definitely assigned to a variable of type IFactory<Animal>, which means it’s something that returns objects that can act like an Animal. Without covariance, the factory needs to return objects that are exactly Animals but with covariance the factory needs to return objects that are Animals or a type derived from Animal.
So, it’s fine when the factory returns a Bird:
- Thing returned from a BirdFactory that’s assigned to an animal factory variable: {Bird, wing span:200, weight:100}
As a human this makes sense, but without variance the rules that the compiler and runtime system operate under would stop this from working.