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 outforeach (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>fromGetByType<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>orIReadOnlyList<out T>for outputs – covariant and flexible. - For
List<T>mutation, copy:new List<Fruit>(appleList)or.Cast<Fruit>().ToList(). out Tonly in output positions (returns); no input params likevoid Set(T).- Avoid contravariant
in Tunless for inputs (predicates, actions). - Enable nullable reference types; test with multiple derived types.
- Pro tip:
yield returnfor 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?