Understanding Generics and Covariance in C#

Introduction to Generics in C#

Ever hit a wall like this?

class Fruit { }
sealed class Apple : Fruit { }
static List<Fruit> GetFruits() => new List<Apple> { new Apple() }; // Boom! Compile error

Yet swap to IEnumerable<Fruit> and it magically works? This trips up devs assuming inheritance flows through generics. It spotlights invariance vs. covariance – List<T> blocks it to prevent bugs, while IEnumerable<out T> enables safe read-only views. Crucial for bulletproof APIs.

Core Mechanism

Generics like List<T> are invariant: List<Apple> and List<Fruit> (even with Apple : Fruit) are distinct types. No implicit conversion exists, as List<T> allows mutation – adding an Orange to a “fruits” list could corrupt it.

List<Fruit> fruits = new List<Apple> { new Apple() }; // Compile error!
fruits.Add(new Orange()); // Danger: mixes types at runtime!

C# blocks this proactively. You should thank it for that!

Simplifying with Covariance

Covariant interfaces like IEnumerable<out T> (C# 4.0+) allow “upcasting” for read-only scenarios. out T is a variance annotation declaring T covariant – the generic only produces (outputs) T values, never consumes them as inputs.

// Covariant: T only flows OUT (returns)
public interface IEnumerable<out T>
{
IEnumerator<T> GetEnumerator(); // T produced here
// NO methods taking T as input!
}

This enables:

static IEnumerable<Fruit> GetFruits() => new List<Apple> { new Apple() }; // Works!

out T promises no writes through the interface, so foreach (Fruit f in fruits) is safe – you read as Fruit without mutating the List<Apple>.

Step-by-Step Example

Broken version:

static List<Fruit> GetFruits() => new List<Apple> { new Apple() }; // Error: Can't convert

Fixed with covariance:

var apples = new List<Apple> { new Apple() };
IEnumerable<Fruit> fruits = apples; // OK: covariant out
foreach (Fruit f in fruits) { /* Safe read */ }

Full runnable console:

using System;
using System.Collections.Generic;
class Fruit { }
sealed class Apple : Fruit { }
class Program
{
static void Main()
{
var fruits = GetFruits();
foreach (var f in fruits) Console.WriteLine(f); // Apple as Fruit
}
static IEnumerable<Fruit> GetFruits() => new List<Apple> { new Apple() };
}

Warnings: Covariance is read-only – no Add() exposed. For mutation, copy explicitly.

Real-World Use Cases

  • Repository Patterns: IEnumerable<Entity> from GetByType<T>() – LINQ like .Where(e => e.IsActive) works without copies.
  • ASP.NET APIs: IEnumerable<Product> from derived categories – serializes cleanly.
  • Event Streams: IReadOnlyList<Event> for logs – covariant with count/index access.

Benefits: Safer contracts, zero allocation overhead, IDE autocompletion.

Best Practices

  • Prefer IEnumerable<out T> or IReadOnlyList<out T> for outputs – covariant and flexible.
  • For List<T> mutation, copy: new List<Fruit>(appleList) or .Cast<Fruit>().ToList().
  • out T only in output positions (returns); no input params like void Set(T).
  • Avoid contravariant in T unless for inputs (predicates, actions).
  • Enable nullable reference types; test with multiple derived types.
  • Pro tip: yield return for lazy covariance: yield return new Apple();.

Call to Action

Paste this into a console app or LINQPad – swap Apple for your types and test covariance. Share your invariance horror stories below! Ready to make your APIs covariant-safe?