DEV Community

mohamed Tayel
mohamed Tayel

Posted on

2

Covariance and Contravariance in C#: Real-World Scenarios Explained

Variance in C# is a powerful concept that allows you to use type hierarchies more flexibly in generic types. By understanding covariance and contravariance, you can design and work with collections, delegates, and comparers more effectively. In this article, we’ll explain these concepts and demonstrate them using practical, real-world scenarios.


What Are Covariance and Contravariance?

  1. Covariance:

    • Allows a derived type to be used where a base type is expected.
    • Applies to output types, like return values.
    • Example: Assigning IEnumerable<Dog> to IEnumerable<Animal>.
  2. Contravariance:

    • Allows a base type to be used where a derived type is expected.
    • Applies to input types, like method parameters.
    • Example: Assigning Action<Animal> to Action<Dog>.

Real-World Scenarios

Let’s explore some practical examples that illustrate covariance and contravariance in action.


Scenario 1: Covariance with Collections

Use Case

You have a collection of derived objects (Dog) and need to treat them as their base type (Animal).

Example

public class Animal
{
    public string Name { get; set; }
}

public class Dog : Animal
{
    public string Breed { get; set; }
}

IEnumerable<Dog> dogs = new List<Dog>
{
    new Dog { Name = "Buddy", Breed = "Labrador" },
    new Dog { Name = "Max", Breed = "Beagle" }
};

// Covariance allows this assignment
IEnumerable<Animal> animals = dogs;

foreach (var animal in animals)
{
    Console.WriteLine($"Animal Name: {animal.Name}");
}
Enter fullscreen mode Exit fullscreen mode

Key Points

  • Covariance in IEnumerable<T> allows treating Dog objects as Animal.
  • This is safe because we’re only reading from the collection.

Scenario 2: Contravariance with Comparers

Use Case

You need a comparer that can handle a collection of Dog objects but is defined for their base type Animal.

Example

public class AnimalComparer : IComparer<Animal>
{
    public int Compare(Animal x, Animal y)
    {
        return string.Compare(x.Name, y.Name, StringComparison.Ordinal);
    }
}

var dogs = new List<Dog>
{
    new Dog { Name = "Buddy", Breed = "Labrador" },
    new Dog { Name = "Max", Breed = "Beagle" }
};

// Contravariance allows this
dogs.Sort(new AnimalComparer());

foreach (var dog in dogs)
{
    Console.WriteLine($"Dog Name: {dog.Name}, Breed: {dog.Breed}");
}
Enter fullscreen mode Exit fullscreen mode

Key Points

  • IComparer<T> is contravariant, allowing AnimalComparer to sort Dog objects.
  • The comparison logic applies to the shared Name property.

Scenario 3: Delegates and Event Handling

Use Case

You want to handle events for derived types (AdvancedButtonClickEventArgs) with a handler designed for the base type (ButtonClickEventArgs).

Example

public class ButtonClickEventArgs : EventArgs
{
    public string ButtonName { get; set; }
}

public class AdvancedButtonClickEventArgs : ButtonClickEventArgs
{
    public DateTime ClickTime { get; set; }
}

public class Button
{
    public event EventHandler<ButtonClickEventArgs> Clicked;

    public void OnClick(ButtonClickEventArgs args)
    {
        Clicked?.Invoke(this, args);
    }
}

void HandleClick(object sender, AdvancedButtonClickEventArgs args)
{
    Console.WriteLine($"Button {args.ButtonName} clicked at {args.ClickTime}");
}

var button = new Button();

// Contravariance allows this assignment
button.Clicked += HandleClick;

button.OnClick(new AdvancedButtonClickEventArgs
{
    ButtonName = "Submit",
    ClickTime = DateTime.Now
});
Enter fullscreen mode Exit fullscreen mode

Key Points

  • The event expects EventHandler<ButtonClickEventArgs>.
  • Contravariance allows HandleClick to handle events for the derived type.

Scenario 4: Factory Pattern with Covariance

Use Case

You want a factory to produce objects of derived types (Dog) but expose them as their base type (Animal).

Example

public interface IFactory<out T>
{
    T Create();
}

public class DogFactory : IFactory<Dog>
{
    public Dog Create() => new Dog { Name = "Buddy", Breed = "Labrador" };
}

IFactory<Animal> animalFactory = new DogFactory(); // Covariance allows this

Animal animal = animalFactory.Create();
Console.WriteLine($"Animal Name: {animal.Name}");
Enter fullscreen mode Exit fullscreen mode

Key Points

  • IFactory<out T> is covariant because of the out keyword.
  • Covariance allows the derived type (Dog) to be treated as the base type (Animal).

Scenario 5: LINQ Queries and Covariance

Use Case

You want to perform LINQ queries on a derived type collection but treat it as the base type.

Example

var dogs = new List<Dog>
{
    new Dog { Name = "Buddy", Breed = "Labrador" },
    new Dog { Name = "Max", Breed = "Beagle" }
};

IEnumerable<Animal> animals = dogs; // Covariance allows this

var names = animals.Select(a => a.Name);

foreach (var name in names)
{
    Console.WriteLine($"Animal Name: {name}");
}
Enter fullscreen mode Exit fullscreen mode

Key Points

  • LINQ queries leverage covariance in IEnumerable<T>.
  • The flexibility to treat derived types as base types simplifies querying.

Scenario 6: Read-Only Collections with Covariance

Use Case

You expose a collection of derived types as a read-only collection of the base type.

Example

IReadOnlyList<Dog> dogList = new List<Dog>
{
    new Dog { Name = "Rex" },
    new Dog { Name = "Spot" }
};

IReadOnlyList<Animal> animalList = dogList; // Covariance allows this

foreach (var animal in animalList)
{
    Console.WriteLine(animal.Name);
}
Enter fullscreen mode Exit fullscreen mode

Key Points

  • Read-only collections like IReadOnlyList<T> are covariant.
  • Covariance ensures type safety while allowing flexible interfaces.

Best Practices for Using Variance

  1. Covariance:

    • Use for output types where only reading data is required.
    • Common with IEnumerable<T>, IReadOnlyList<T>, and factory patterns.
  2. Contravariance:

    • Use for input types where data is consumed.
    • Common with IComparer<T>, IEqualityComparer<T>, and delegates.
  3. Testing and Validation:

    • Always test scenarios where variance is applied, especially with mixed types.
    • Watch for logical bugs, such as ignoring derived type properties.
  4. Design with Intent:

    • Clearly define variance in your custom interfaces using out or in keywords.
    • Use covariance for flexibility in outputs and contravariance for generality in inputs.

Conclusion

Covariance and contravariance provide a flexible and type-safe way to handle generic types in C#. By understanding these concepts, you can design more reusable and robust code. Whether you’re working with collections, comparers, or delegates, these features enhance the expressiveness of your code without sacrificing type safety.

Heroku

Built for developers, by developers.

Whether you're building a simple prototype or a business-critical product, Heroku's fully-managed platform gives you the simplest path to delivering apps quickly — using the tools and languages you already love!

Learn More

Top comments (0)

ACI image

ACI.dev: Best Open-Source Composio Alternative (AI Agent Tooling)

100% open-source tool-use platform (backend, dev portal, integration library, SDK/MCP) that connects your AI agents to 600+ tools with multi-tenant auth, granular permissions, and access through direct function calling or a unified MCP server.

Star our GitHub!

👋 Kindness is contagious

Dive into this insightful write-up, celebrated within the collaborative DEV Community. Developers at any stage are invited to contribute and elevate our shared skills.

A simple "thank you" can boost someone’s spirits—leave your kudos in the comments!

On DEV, exchanging ideas fuels progress and deepens our connections. If this post helped you, a brief note of thanks goes a long way.

Okay