Covariance in C#: Arrays vs Generic Collections
In C#, covariance enables you to use a more derived type than originally specified, but its implementation differs significantly between arrays and generic collections. While both allow implicit reference conversions from a collection of derived types to a collection of base types, array covariance is implicitly unsafe and enforced at runtime, whereas generic collection covariance is type-safe, read-only, and enforced at compile time. Understanding these differences is essential for writing robust, error-free C# code.
Understanding Covariance
Covariance preserves assignment compatibility. If type
Derived inherits from type Base, covariance
allows a collection of Derived objects to be treated as a
collection of Base objects. In code, this means you can
assign a reference of type Collection<Derived> to a
variable of type Collection<Base>.
Array Covariance: Implicit and Runtime-Checked
Array covariance was introduced in C# 1.0, primarily to mirror Java's
design and allow generic-like utility methods before generic types
existed. In C#, any array of reference types Derived[] can
be implicitly converted to Base[].
string[] strings = new string[] { "apple", "banana" };
object[] objects = strings; // Valid at compile time due to array covarianceThe major drawback of array covariance is that it breaks compile-time
type safety. Because the underlying array remains a
string[], attempting to write an incompatible type into
objects will cause the runtime to throw an
ArrayTypeMismatchException:
objects[0] = 42; // Throws ArrayTypeMismatchException at runtimeEvery write operation on a reference-type array incurs a hidden runtime type check to ensure the value matches the actual array element type, which introduces both performance overhead and potential runtime crashes.
Generic Collection Covariance: Safe and Compile-Time Enforced
Generics were introduced in C# 2.0, but generic variance was
officially added in C# 4.0. Unlike arrays, generic classes like
List<T> are invariant. You cannot assign a
List<string> to a List<object>
because doing so would allow adding invalid types to the list, which C#
prevents at compile time.
Instead, generic covariance is restricted to interfaces and delegates
using the out parameter keyword. A prominent example is
IEnumerable<out T>:
IEnumerable<string> stringEnumerable = new List<string> { "apple", "banana" };
IEnumerable<object> objectEnumerable = stringEnumerable; // Perfectly safeThe out keyword ensures that T is only used
in output positions, such as method return types or property getters.
Because covariant interfaces do not allow methods that accept
T as an input parameter (like an Add method),
it is impossible to introduce an invalid type into the collection. This
restriction eliminates the possibility of runtime type mismatch
exceptions.
Key Differences Summary
- Type Safety: Generic covariance is fully checked
and guaranteed at compile time. Array covariance is checked at runtime,
making it prone to
ArrayTypeMismatchException. - Mutability: Generic covariance only applies to
read-only interfaces (like
IEnumerable<T>andIReadOnlyList<T>) and delegates (likeFunc<T>). Arrays are inherently mutable, which causes safety conflicts when combined with covariance. - Performance: Generic covariance introduces no runtime overhead. Arrays incur a runtime type check on every write operation to reference arrays to prevent type corruption.
- Syntax: Generic covariance is explicitly declared
by interface designers using the
outmodifier. Array covariance is implicit and built into the language for all reference-type arrays.
When designing modern C# applications, favor covariant generic
interfaces such as IEnumerable<T> or
IReadOnlyCollection<T> over arrays when accepting
broader type hierarchies to ensure compile-time safety and prevent
runtime errors.