Advanced generics and variance

C# · Advanced · 12 min

Variance explains why IEnumerable<string> fits IEnumerable<object> but List<string> doesn't fit List<object>.

Variance describes when a generic type with one type argument can be used where another is expected.

  • Covariance (out T): a producer of T. IEnumerable<out T> only returns T, so an IEnumerable<string> can be used as IEnumerable<object>. Same for Func<out TResult> and IReadOnlyList<out T>.
  • Contravariance (in T): a consumer of T. Action<in T> and IComparer<in T> only accept T, so an IComparer<object> can sort strings.
  • Invariance: List<T> both reads and writes T, so List<string> is not a List<object>; otherwise you could add an int into a list of strings.

Variance is allowed only on interfaces and delegates, and only for reference type arguments (IEnumerable<int> is not an IEnumerable<object>). Arrays are covariant for historical reasons, which is unsafe: storing the wrong type throws ArrayTypeMismatchException at runtime.

Static abstract interface members (C# 11) let an interface require static methods and operators. This powers generic math: INumber<T>, IAdditionOperators<T, T, T>, so one algorithm works for int, double and decimal without boxing. You can define your own: interface IParsable<TSelf> where TSelf : IParsable<TSelf> is the "self type" pattern.

Other tools: default constraints, allows ref struct (C# 13) for generics over spans, and the typeof(T) checks that the JIT eliminates for value types.

Example

IEnumerable<string> words = ["b", "a"];
IEnumerable<object> objs = words;            // covariance: OK
Console.WriteLine(objs.Count());             // 2

IComparer<object> byText = Comparer<object>.Create(
    (x, y) => string.Compare(x?.ToString(), y?.ToString()));
var list = new List<string> { "pear", "apple" };
list.Sort(byText);                           // contravariance: OK
Console.WriteLine(string.Join(",", list));   // apple,pear

object[] arr = new string[1];                // array covariance
try { arr[0] = 42; }
catch (ArrayTypeMismatchException) { Console.WriteLine("runtime error"); }

Console.WriteLine(Zero<Money>().Cents);      // 0
static T Zero<T>() where T : IHasZero<T> => T.Zero;

interface IHasZero<TSelf> where TSelf : IHasZero<TSelf>
{
    static abstract TSelf Zero { get; }
}
record Money(long Cents) : IHasZero<Money>
{
    public static Money Zero => new(0);
}

Remember

  • out T = covariant producer, in T = contravariant consumer
  • Variance applies to interfaces and delegates with reference types only
  • Array covariance is unsafe and checked at runtime
  • Static abstract interface members enable generic math

Check yourself

  1. Why can't a List<string> be assigned to a List<object> variable?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance

Builds on

Unlocks

Nothing builds on this yet.