Skip to Content
All posts

Mastering Variance in C#

Invariance, Covariance, and Contravariance Explained

3 min read ·  — #csharp-interview#senior#invariance#covariance#contravariance

Mastering Variance in C#

Introduction: In the evolving landscape of C#, understanding the concepts of variance plays a pivotal role in crafting resilient and efficient applications. Whether you're refining your generic types or streamlining delegates, grasping the core ideas of invariance, covariance, and contravariance can set your code apart. Ready for a deep dive? Let’s embark on this variance voyage, elucidating these nuanced aspects of the language with real-world examples.


1. Invariance: Invariance is the default behavior in C# generics. It means that there's no relationship between the derived and base types of generic types.

Example: Let's consider the List<T> class. A List<Derived> is not a subtype of List<Base> even if Derived is a subtype of Base.

class Base { }
class Derived : Base { }

List<Derived> derivedList = new List<Derived>();
List<Base> baseList = derivedList;
// This will produce a compile-time error.

Real-World Scenario: Imagine an application where you maintain a collection of different types of messages. While EmailMessage and SMSMessage might inherit from a common Message class, using a List<EmailMessage> in place of List<Message> directly would result in a compile-time error.


2. Covariance: Covariance permits a method to return a more derived type than specified by the method signature or assigns a value of a more derived type to a location of a less derived type.

In C#, covariance is supported for reference conversion of array types, delegate types, and generic type arguments.

Example:

IEnumerable<Derived> derivedList = new List<Derived>();
IEnumerable<Base> baseEnumerable = derivedList;
// This is allowed.

Real-World Scenario: Consider a media player application where you have a base Media class and derived Audio and Video classes. If you want a method to return an IEnumerable of all media files, but internally you're only dealing with Audio files, using covariance will allow you to represent the Audio list as IEnumerable<Media> seamlessly.


3. Contravariance: Contravariance allows a method to accept parameters of less derived types than specified by its signature. In C#, contravariance is supported for delegate types and generic type arguments of interfaces and delegates.

Example: Consider the delegate:

delegate void Action<in T>(T arg);

Here's how contravariance works:

Action<Base> baseAction = (Base b) => { /*...*/ };
Action<Derived> derivedAction = baseAction;
// This is allowed.

Real-World Scenario: Imagine you're building a logging system where you have a base Log class and derived ErrorLog and InfoLog classes. If you have a method designed to process the base Log, using contravariance, you can still use it to handle specific log types like ErrorLog.


Conclusion: Variance in C# is more than just a theoretical concept. It’s a tool that, when harnessed effectively, can help developers write more versatile and adaptable code. As we've seen, understanding when and how to use invariance, covariance, and contravariance is crucial for ensuring type safety while maximizing code reuse. So the next time you're designing a system with generics or working with delegates, remember to apply these principles judiciously to strike the right balance between flexibility and robustness.


References: