Skip to main content
๐Ÿ“œ WAYPOINT LESSON

Covariance and Contravariance

โญโญ intermediateโณ 15 min read๐Ÿ“ Lesson 102 of 265

Which generic directions convert safely, why Dictionary is invariant, and the IEnumerable<out T> rule.

Two directions of safe conversion

Covariance (out T): if string : object, then IEnumerable<string> converts to IEnumerable<object>. Output positions only โ€” you can pull a string out and hand it over as object.

IEnumerable<object> shapes = new List<string> { "a" };   // legal: out T

Contravariance (in T): Action<object> converts to Action<string>. Input positions only โ€” a delegate that accepts any object can serve where a string-eater is needed.

Action<object> logAny = o => Console.WriteLine(o);
Action<string> logStr = logAny;                          // legal: in T

Both are compile-time guarantees the compiler checks for you: a T in an output position makes the parameter covariant-eligible; in input positions, contravariant. Mutating interfaces (IList<T>, Dictionary<K,V>) are invariant โ€” a list you can insert into must reject the conversion, or shapes.Add(42) would corrupt a List<string>.

The mental model

  • out T = "I only produce T" โ†’ covariant (more specific collection is-a less specific one).
  • in T = "I only consume T" โ†’ contravariant (a general consumer is-a specific one).
  • Both = invariant (I do both; no safe direction).

Func<in T, out TResult> has both annotations โ€” read it as a promise about each position.

Check your understanding

  • Why is IList<string> โ†’ IList<object> illegal? (Add(object) would insert non-strings.)
  • Comparison<in T> โ€” which variance and why? (Contravariant: it consumes T.)

โšก Now practice

Ready to Code
Generic Design PracticeConstraints that promise, algorithms that stay type-safe, and variance you can predict without the compiler guessing for you.
3 challenges ยท ยท ~25 min