Covariance and inversion in C# generics

C # uses inheritance and generics to write reusable code across types. A generic type is a type that contains variable type parameters. At run time, the generic type parameters are filled with specific types to create a specific type. Technically, the type with unfilled type parameters is called open type, and the type with filled type parameters is called closed type.

//public class List<T> : IList<T>, System.Collections.IList, IReadOnlyList<T>
//List < T > is an open type and list < int > is a closed type
List<int> d = new List<int>();

I Generic related type conversions

Like other types, closed types of generic types can also be converted. Their type conversion can be divided into two cases:

1. The filled type parameters remain unchanged. In this case, the closed type can be regarded as an ordinary type, and the ordinary type conversion rules apply.

class Base {}
class Derived : Base {}

//List < derived > can be implicitly converted to IEnumerable < derived >
//Because list < T > indirectly implements the IEnumerable < T > interface
IEnumerable<Derived> d = new List<Derived>();

2. The filled type parameter changes. In this case, it is called covariance or inversion of generics, or collectively called Variance of generics). Specifically:

Covariance: the type parameter filled by the received type is less derived than the type parameter originally filled.

contravariance: the type parameters filled by the received type are more derived than the originally filled type parameters.

//IEnumerable < derived > can be implicitly converted to IEnumerable < base >
//Because the type parameter of IEnumerable < T > is covariable
//This transformation is called covariance
IEnumerable<Derived> d = new List<Derived>();
IEnumerable<Base> b = d;

//Action < base > can be implicitly converted to action < derived >
//Because the type parameter of action < T > is invertible
//This conversion is called inversion
Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b;
d(new Derived());

The above code is type safe at compile time and run time.

II Covariant type parameters and inverse type parameters

Type parameters in generics are immutable by default, that is, they can neither be covariant nor inverse. If you want to achieve variability, you need to modify type parameters with corresponding keywords. Use the keyword {in} to specify that the type parameter is invertible, and use the keyword out to specify that the type parameter is covariant.

//This is a NET built-in generic delegate
//The type parameter T is invertible
//The type parameter TResult is covariable
public delegate TResult Func<in T, out TResult>(T arg);

Generally speaking, covariant type parameters can be used as the return type of delegates, and inverse type parameters can be used as parameter types. For interfaces, covariant type parameters can be used as the return type of interface methods, and inverse type parameters can be used as the parameter types of interface methods. Otherwise, runtime errors may occur, such as the following code:

//Compile time error:
//Invalid variant: type parameter 'TResult' must be a valid covariant on 'func < TResult >. Invoke()'.
//"TResult" is inverse.
public delegate MyFunc<in TResult>();

//Imagine if the above statement is valid
//The following first line of statements can be executed normally
//A run-time error occurs in the second line of the statement
//Because the Base type cannot be implicitly converted to the Derived type
//Therefore, the compiler does not allow the above declaration to pass
MyFunc<Derived> foo = ()=>new Base();

The use of variable type parameters also has the following limitations

  • Can only be used in declarations of generic interfaces and generic delegates
  • Only applicable to reference types. If the type parameter is filled with value type, the resulting enclosing type is immutable
  • Variability cannot be used in a delegation chain, that is, two delegates whose closed types are not completely consistent cannot be combined
//Compile time error
//Cannot implicitly convert type 'system. Func < int >' to 'system. Func < Object >
Func<object> foo = new Func<int>(()=>3);

Func<Derived> bar = ()=>new Derived();
Func<Base> foo = bar;
Console.WriteLine(foo().ToString()); //Derived

foo = ()=>new Base();
foo += bar;
//System.ArgumentException: Delegates must be of the same type

Keywords: C#

Added by lou28 on Wed, 29 Dec 2021 01:15:27 +0200