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 ofT.IEnumerable<out T>only returnsT, so anIEnumerable<string>can be used asIEnumerable<object>. Same forFunc<out TResult>andIReadOnlyList<out T>. - Contravariance (
in T): a consumer ofT.Action<in T>andIComparer<in T>only acceptT, so anIComparer<object>can sort strings. - Invariance:
List<T>both reads and writesT, soList<string>is not aList<object>; otherwise you could add anintinto 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