What is Covariance?
Covariance may sound like a complex term, but it simply means 'the property of being able to change together.'
For example, when a type A can be changed to type B, the derived type C<A> can also change to C<B>. This is what we refer to as covariance.
A common example of covariance in C# is IEnumerable<T>.
IEnumerable<string> strings = new List<string>();
// When string can be changed to object,
// IEnumerable<string> can also be changed to IEnumerable<object>
IEnumerable<Object> objects = strings;
Runtime Errors Due to Covariance
Another example of covariance in C# is arrays.
string[] strArray = new string[5];
// string[] can be treated as object[]
object[] objArray = strArray;
But what happens if we try to insert an instance of a class that is not a string into the object array?
.NET checks at runtime whether an unsafe type is being inserted into the array, resulting in an ArrayTypeMismatchException.
object[] objArray = new string[5];
// System.ArrayTypeMismatchException:
// 'Attempted to access an element as a type incompatible with the array.'
objArray[0] = 1234;
Avoiding Type Check Costs (Stelem_Ref)
The fact that .NET checks whether the elements being stored in the array are safe means that there is an additional cost involved.
In typical logic, this overhead is small enough to be ignored, and optimizations are made to skip checks if the value being stored is null or the same as an already validated value in the array (JIT: avoid store covariance check for ref-type ldelem from same array · Issue #9159 · dotnet/runtime).
However, if this code is executed frequently in a hot path, it may require attention.
If profiling indicates that this overhead is present, it will appear under the name Stelem_Ref.
Option 1 - Use the sealed Keyword
As discussed in the article .NET Sealed Class Performance Considerations, the compiler can be sure that no further inheritance occurs with a sealed class.
Therefore, if you apply the sealed modifier to the class that is the target of the array, it can be assured that the type being stored is not actually a different type (subclass), allowing for optimizations that skip type checks.
sealed class MyClass { }
// No array type check when storing in the array
MyClass[] myArray = new MyClass[5];
myArray[0] = new MyClass();
Option 2 - Direct Memory Writing
If unsafe code is allowed and you can be sure of the variable's type, you can bypass type checks and write directly to memory.
Having a lot of pinned memory can affect GC performance, and if an incorrect type is inserted, it may cause runtime errors elsewhere, so caution is advised when using values stored in the array.
public static void SetArrayValue<T>(T[] array, int index, T value)
{
unsafe
{
fixed (T* _ = array) // Pinning to prevent GC from moving it
{
ref T start = ref MemoryMarshal.GetArrayDataReference(array);
Unsafe.Add(ref start, index) = value;
}
}
}
object[] objArray = new string[5];
SetArrayValue(objArray, 0, 4); // No runtime error
SetArrayValue(objArray, 1, "test");
Option 3 - Using Struct Wrappers
Option 1 can only be used when directly defining the target of the array and relies on uncertain compiler optimizations.
Option 2 uses unsafe code and affects GC.
A better approach is to wrap the data in a struct to create the array. Since structs are value types, they do not undergo covariance type checks. Like option 2, this also skips type checks, so caution is needed when using values stored in the array.
class MyClass { }
class MyChildClass : MyClass { }
struct Element
{
public MyClass Value;
}
Element[] elementArray = new Element[5];
elementArray[0] = new Element { Value = new MyClass() };
elementArray[1] = new Element { Value = new MyChildClass() }; // No runtime error
This struct wrapper approach is often used in object pools, where the class of the stored object is defined by the user.
A notable example is the ObjectPool implementation in Roslyn: