What is Covariance?
Covariance may sound like a complicated term at first, but it simply means the property of being able to change in accordance with something else.
For example, when a type A can be changed to type B, and the derived type C<A> can also change to C<B>, this is referred to as covariance.
In C#, a class that exhibits covariance 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 class in C# that exhibits covariance is arrays.
string[] strArray = new string[5];
// string[] can be converted to object[]
object[] objArray = strArray;
Arrays are classes that can hold values. But what happens if we try to insert an instance of a class that is not a string into an object array?
Although objArray is an object array, it is actually a string array. Therefore, inserting an object of a type other than string can lead to problems later. As a result, .NET checks at runtime whether an unsafe type is being inserted into the array, which can raise 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)
When .NET checks whether elements are safely stored in an array, it implies additional costs.
In typical scenarios, this overhead is negligible, and optimizations are made to skip checks for values that are null or already validated values from the same array (JIT: avoid store covariance check for ref-type ldelem from same array · Issue #9159 · dotnet/runtime).
However, if the code is frequently executed in a hot path, it may require attention.
If profiling reveals this overhead, it will appear as Stelem_Ref.
Option 1 - Use the sealed Keyword
As discussed in the article on .NET Sealed Class Performance Considerations, the compiler can be certain that no further inheritance occurs with sealed classes.
Thus, if you apply the sealed modifier to the class that is the target of the array, the compiler can optimize by skipping type checks, as it can be assured that the type being stored is not a different type (subclass).
sealed class MyClass { }
// No array type check during storage
MyClass[] myArray = new MyClass[5];
myArray[0] = new MyClass();
Option 2 - Direct Memory Writing
In situations where unsafe code is permitted and you can be certain 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 inserting the wrong type can lead to 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) // Pin to avoid being moved by GC
{
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 - Use a Struct Wrapper
Option 1 can only be used when directly defining the target of the array, relying on uncertain compiler optimizations. Option 2 uses unsafe code and impacts GC.
A better approach is to wrap the target in a struct. Since structs are value types, they do not undergo covariance type checks. Like option 2, this also avoids type checks but can still lead to the possibility of inserting the wrong type, so caution is advised 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
In particular, object pools often use the struct wrapper method since the classes of the stored objects are defined by the user.
A notable example is the ObjectPool implementation in Roslyn: