Generics, variance, and boxing are tightly related. Generics exist in large part to avoid boxing. Variance controls when one generic type can stand in for another. Understanding all three together explains many of the constraints and design decisions in C# and .NET.
Generics
Generics C# 2.0 allow a type or method to be parameterized over one or more types using <T> syntax. The compiler and runtime enforce type safety without requiring casts and without boxing value types.
// Without generics (pre-C# 2.0)
ArrayList list = new ArrayList();
list.Add(42); // int is boxed to object
int n = (int)list[0]; // unboxed and cast, risky and slow
// With generics
List<int> list = new List<int>();
list.Add(42); // no boxing, stored as int directly
int n = list[0]; // no cast neededUnlike C++ templates or Java's erased generics, the .NET runtime generates specialized code for each type argument. For value types like int, this means the runtime creates a version of List<T> that stores int values directly without boxing. For reference types, a single shared implementation is used since all references are the same size.
Constraints
Constraints restrict what types can be used as a type argument, giving you access to more operations inside the generic code.
// Unconstrained: can only use object members
void Log<T>(T item) => Console.WriteLine(item?.ToString());
// Constrained: compiler knows T has .Name
void Print<T>(T item) where T : INameable => Console.WriteLine(item.Name);| Constraint | Meaning |
|---|---|
where T : class | Must be a reference type |
where T : struct | Must be a non-nullable value type |
where T : new() | Must have a parameterless constructor |
where T : BaseClass | Must inherit from BaseClass |
where T : IInterface | Must implement IInterface |
where T : unmanaged | Must be an unmanaged type (C# 7.3) |
where T : Enum | Must be an enum type (C# 7.3) |
where T : Delegate | Must be a delegate type (C# 7.3) |
where T : notnull | Must be a non-nullable type |
where T : allows ref struct | Allows ref struct types (C# 13.0) |
Multiple constraints can be combined: where T : class, IComparable<T>, new().
Boxing and unboxing
Every value type in .NET (int, bool, struct, enum, etc.) is stored inline: on the stack for locals, or directly inside the containing object for fields. Reference types (class, string, arrays) are stored on the heap and accessed via a pointer.
Boxing is what happens when a value type is assigned to a variable of type object, an interface, or any other reference type. The runtime allocates a new object on the heap, copies the value into it, and returns a reference. Unboxing is the reverse: extracting the value from the box.
int n = 42;
object boxed = n; // boxing: heap allocation + copy
int unboxed = (int)boxed; // unboxing: type check + copyBoxing is expensive in tight loops because each box is a separate heap allocation that the garbage collector must later clean up. It is also a source of subtle bugs because the boxed copy is independent, so mutating the original does not affect the box.
Common causes of boxing
| Cause | Example | Fix |
|---|---|---|
| Non-generic collections | ArrayList.Add(42) | Use List<int> |
Calling object methods on structs without overrides | myStruct.GetHashCode() if not overridden | Override GetHashCode, Equals, ToString |
| Interface dispatch on value types | IComparable c = 42; c.CompareTo(0); | Use constrained generics: where T : IComparable<T> |
| String interpolation (pre-C# 10) | $"Value: {myInt}" | Interpolated string handlers C# 10.0 eliminate this |
Nullable<T> to object | object o = (int?)42; | Avoid when possible |
Generics are the primary tool for avoiding boxing. A List<int> stores integers directly; a method constrained with where T : IComparable<T> calls CompareTo without boxing because the runtime uses a constrained call that dispatches directly on the value type.
Covariance and contravariance
Variance describes when one generic type can safely substitute for another based on the inheritance relationship of their type arguments.
Covariance (out T)
Generic covariance C# 4.0 applies to type parameters that are only output (returned). If Dog inherits from Animal, then IEnumerable<Dog> can be used where IEnumerable<Animal> is expected because every Dog you pull out is an Animal.
IEnumerable<Dog> dogs = GetDogs();
IEnumerable<Animal> animals = dogs; // OK, covariant
// This works because IEnumerable<out T> only produces T values.
// You can safely read a Dog as an Animal.The out keyword on the type parameter declares this intent and the compiler enforces that T is never used in an input position.
Contravariance (in T)
Generic contravariance C# 4.0 applies to type parameters that are only input (consumed). If Dog inherits from Animal, then Action<Animal> can be used where Action<Dog> is expected because a handler that accepts any Animal can certainly handle a Dog.
Action<Animal> feedAnimal = a => Console.WriteLine($"Feeding {a.Name}");
Action<Dog> feedDog = feedAnimal; // OK, contravariant
// This works because Action<in T> only consumes T values.
// A method that can feed any Animal can certainly feed a Dog.The in keyword on the type parameter declares this and the compiler enforces that T is never used in an output position.
Why IList<T> is invariant
IList<T> both produces T (indexer get) and consumes T (Add, indexer set). Neither in nor out is safe, so IList<T> is invariant and IList<Dog> cannot be assigned to IList<Animal> or vice versa.
IList<Dog> dogs = new List<Dog>();
// IList<Animal> animals = dogs; // Compile error, and for good reason:
// animals.Add(new Cat()); // This would put a Cat in a Dog list!Quick reference
| Keyword | Direction | Assignable when... | Common examples |
|---|---|---|---|
out T (covariance) | T is output only | Child to Parent | IEnumerable<out T>, IReadOnlyList<out T>, Func<out T> |
in T (contravariance) | T is input only | Parent to Child | Action<in T>, IComparer<in T>, IEqualityComparer<in T> |
| (neither) invariant | T is both | Exact match only | IList<T>, List<T>, Dictionary<K,V> |
Covariant returns
Separate from generic variance, covariant returns C# 9.0 allow an overriding method to return a more specific type than the base method. This is about method overrides, not generic type parameters.
abstract class Factory { public abstract Animal Create(); }
class DogFactory : Factory { public override Dog Create() => new Dog(); } // OK in C# 9+Variance only applies to interfaces and delegates
Classes and structs cannot be variant. Only interface and delegate type parameters support in/out because the runtime needs to guarantee that the memory layout is compatible, which it can only do for reference-based dispatch.
Variance also only applies to reference types. IEnumerable<int> cannot be assigned to IEnumerable<object> because int is a value type and the conversion would require boxing every element.
How they connect
- Generics eliminate boxing. The primary motivation for generics was type-safe collections that store value types without boxing.
- Variance enables flexible generics.
outandinlet generic interfaces participate in the same inheritance-based substitution that non-generic types have always had. - Boxing limits variance. Variance only works with reference type arguments because value types would need boxing to convert, and implicit boxing on every access would defeat the performance goal of generics.
- Constraints bridge the gap. Constraints like
where T : structandwhere T : allows ref structgive the compiler enough information to generate efficient, box-free code while still being generic.