112. Variant classes#
When we delved into covariance, contravariance, and invariance, we learnt about the flexibility of type relations, especially when working with generic types. Now, we turn our attention to how these concepts apply to classes.
Key points
C# supports covariant return types.
C# does not support contravariant parameter types.
Covariant Return Types#
In many object-oriented languages, when you override a method, the return type of the overridden method should match the return type of the base method. However, C# has added more flexibility by allowing return types to be covariant. This means that an overridden method in a derived class can have a more derived return type than the method in the base class.
Consider the following classes.
class Fruit { }
class Apple : Fruit { }
class FruitTree
{
public virtual Fruit GetFruit()
=> new Fruit();
}
class AppleTree : FruitTree
{
public override Apple GetFruit()
=> new Apple();
}
In the above example, the Apple
class is derived from the Fruit
class. Similarly, the AppleTree
class is derived from the FruitTree
class.
While the GetFruit
method in the FruitTree
class returns objects of type Fruit
, the overridden GetFruit
method in the AppleTree
class returns objects of type Apple
which is a more derived type.
Important
C# supports covariant return types.
Benefits#
What might the benefit of this be?
Covariant return types help us avoid loosing type-information.
When we have an AppleTree
and ask it for a Fruit
, the compiler will know that we won’t get any old Fruit
but rather precisely an Apple
.
Consider the code below.
AppleTree tree = new AppleTree();
Apple apple = tree.GetFruit();
The code compiles and runs as expected. However, had we not used a covariant return type, we would not have been able to extract an Apple
from the tree without downcasting. Compare the example above with the one below.
class InvariantAppleTree : FruitTree
{
public override Fruit GetFruit()
=> new Apple();
}
InvariantAppleTree tree = new InvariantAppleTree();
Apple apple = tree.GetFruit();
(2,15): error CS0266: Cannot implicitly convert type 'Fruit' to 'Apple'. An explicit conversion exists (are you missing a cast?)
Tip
Covariant return types enhance type safety. When working with derived classes, you can be more specific about what you return, preventing unexpected behaviors and potential issues down the line.
Contravariant Parameter Types#
Contravariance is when you can use a less derived (or “broader”) type instead of a more derived (or “narrower”) type. However, when it comes to method parameters in C#, contravariance isn’t supported. This means that if you override a method in a derived class, the parameter types of the overridden method must match exactly the parameter types of the method in the base class.
Let’s however entertain a hypothetical example of what this might have looked like if C# would have supported contravariant parameter types.
class AppleJuicer
{
public virtual void Juice(Apple apple)
=> Console.WriteLine("Juicing the apple.");
}
class FruitJuicer : AppleJuicer
{
// This won't compile!
public override void Juice(Fruit fruit)
=> Console.WriteLine("Juicing the fruit.");
}
(4,26): error CS0115: 'FruitJuicer.Juice(Fruit)': no suitable method found to override
The Juice
method in the FruitJuicer
class overrides the Juice
method in the AppleJuicer
class and accepts a less derived parameter type. However, since C# does not support contravariant parameters in classes, this code does not compile.
Tip
When overriding methods in C#, the parameter types must match those of the overridden method.
Covariant Parameter Types in Other Languages#
Some programming languages permit covariant parameter types. This means that a derived class, can use a narrower parameter type compared to the same method in the base class.
For example, if a base class method accepts a parameter of type Fruit
, the corresponding method in a derived class would be allowed to specify a narrower type like Apple
.
class AppleJuicer
{
public virtual void Juice(Fruit fruit)
=> Console.WriteLine("Juicing the fruit.");
}
class FruitJuicer : AppleJuicer
{
// This won't compile and is not type-safe!
public override void Juice(Apple apple)
=> Console.WriteLine("Juicing the apple.");
}
(4,26): error CS0115: 'FruitJuicer.Juice(Apple)': no suitable method found to override
Here, the PerformAction
method in Dog
narrows down its parameter type to Dog
, even though the corresponding method in Animal
accepts any Animal
.
Attention
This might seem flexible, but as we have learned in the chapter on the Liskov Substitution Principle, covariance in input will lead to a loss of static-type safety. Languages with this feature are thus prone to run-time errors.
Conclusion#
Variance in programming is all about flexibility and type safety. While C# offers a certain level of flexibility with covariant return types, it does not support contravariant parameter types. Possibly as a consequence of not supporting multiple inheritance. As we continue to explore deeper concepts, always keep in mind the delicate balance between flexibility and safety that C# aims to achieve. It’s all about writing robust and maintainable code.