Introduction:

In C# and many other languages that support generic programming, covariance and contravariance are concepts used to describe how you can assign a more derived (or less derived) type to a base type (or vice versa) when dealing with generic types.

First, we need to understand what Assignment Compatibility is. In C#, assignment compatibility refers to the ability to assign a value of one type to a variable of another type. It’s a foundational concept and is crucial when discussing covariance and contravariance.

Basic rules of assignment compatibility are:

  • You can assign a value of a derived type to a variable of a base type.
  • You cannot assign a value of a base type to a variable of a derived type (unless using explicit casting).
Base baseObj = new Derived(); //Valid, assignment compatibility.
Derived derivedObj = baseObj; //Invalid, requires explicit cast.

Covariance

Covariance enables you to use a more derived type than originally specified. If you have a class Derived that inherits from Base, you can use an IEnumerable<Derived> wherever an IEnumerable<Base> is expected.

In simpler terms, if you have a sequence of derived objects (IEnumerable<Derived>), you can treat it as a sequence of base objects (IEnumerable<Base>).

C# supports covariance for delegates and for generic interfaces.

Example using interfaces and delegates:

//Interfaces
IEnumerable<Derived> derivedList = new List<Derived>();
IEnumerable<Base> baseList = derivedList; // Covariance

//Delegates
Func<Derived> derivedFactory = () => new Derived();
Func<Base> baseFactory = derivedFactory; // Covariance

Contravariance

Contravariance permits a method with the parameter of a less derived type to be assigned to a delegate that represents a method with the parameter of a more derived type. To put it another way, contravariance allows you to use a more generic (less derived) type than originally specified.

C# supports contravariance for delegates and some generic interfaces.

Example using delegates:

Action<Base> baseAction = (baseObj) => Console.WriteLine(baseObj);
Action<Derived> derivedAction = baseAction; //Contravariance!

The in keyword in interfaces:

using System;

public interface IContravariant<in T>
{
    void Display(T item);
}

public class SampleContravariant : IContravariant<object>
{
    public void Display(object item)
    {
        Console.WriteLine(item.GetType().Name);
    }
}

public class Base { }
public class Derived : Base { }

public class Program
{
    public static void Main()
    {
        IContravariant<Derived> contravariantInstance = new SampleContravariant(); //This does not compile without the in keyword
        
        contravariantInstance.Display(new Derived()); //Outputs "Derived"
    }
}

Real-world Use Cases:

Covariance and contravariance might seem theoretical, but they manifest quite practically:

  • Collections: Ever tried filtering a list of cats and dogs (both inheriting from animals) and then treating the result as a list of animals? That’s covariance.
  • Event Handlers: Custom events in C# often lean on delegates, where these variance concepts play a part, especially when ensuring your event handlers are flexible.
  • API Design: If you’re creating libraries or frameworks, understanding covariance and contravariance ensures your APIs are both robust and flexible.

Conclusion:

Covariance and contravariance, while daunting in name, are treasures in C#. They ensure our generics aren’t just type-safe but also malleable enough for real-world scenarios. As you journey through C#, keep an eye out for these patterns—they’re more common than you might think!

Leave a comment

Trending