C#4 and VB10, which were unveiled with Visual Studio 2010 CTP, have taken two quite different ways to support new language features: C# focuses on adding late binding and several features that are compatible with dynamic languages, and VB10 focuses on simplifying languages and improving abstraction capabilities; but both have added a feature: covariant and contravariant of generic types. Many people may be limited to the added in/out keywords, but have no idea about its many features. Below we will give some detailed explanations on this to help everyone use this feature correctly.
Background knowledge: Covariance and counter-change
Many people may not be able to understand these nouns from physics and mathematics well. We don't need to understand their mathematical definitions, but we should at least be able to distinguish between covariance and inverse change. In fact, this word comes from the binding between type and type. We start with arrays. An array is actually a type that binds to a specific type. The array type Int32[] corresponds to the original type of Int32. Any type T has its corresponding array type T[]. Then our question is, if there is a safe implicit conversion between the two types T and U, then is there such conversion between the corresponding array types T[] and U[]? This involves the ability to map the type conversions that exist on the original type to their array types, which is called "variance". In the .NET world, the only type conversion that allows mutability is the "subclass reference->parent reference" conversion brought by inheritance relationships. For example, the String type inherits from the Object type, so any String reference can be safely converted to an Object reference. We found that references to the String[] array type also inherit this conversion ability, which can be converted into references to the Object[] array type. The variability of the array with the same conversion direction as the original type is called covariant.
Since arrays do not support inverse denaturation, we cannot use array examples to explain inverse denaturation, so let's take a look at the mutability of generic interfaces and generic delegates now. Suppose there are two types: TSub is a subclass of TParent, obviously TSub-type references can be safely converted to TParent-type references. If a generic interface IFoo<T> and IFoo<TSub> can be converted to IFoo<TParent>, we call this process covariance, and this generic interface supports covariance of T. If a generic interface IBar<T> and IBar<TParent> can be converted to T<TSub>, we call this process contravariant, and this interface supports inverse change to T. Therefore, it is easy to understand that if a variability is the same as the conversion direction of a subclass to the parent class, it is called covariance; if the conversion direction of a subclass to the parent class is the opposite, it is called inverse. Did you remember it?
Generic covariance and anti-variance introduced by .NET 4.0
When we explained the concept just now, we used the covariance and inverse of the generic interface, but before .NET 4.0, the mutability of generics was not supported in C# or VB. However, they all support covariance and inverse of delegate parameter types. Since the variability of delegate parameter types is highly abstract, we will not discuss it here. Readers who have fully understood these concepts must be able to understand the variability of the delegated parameter types themselves. Why is IFoo<T> not allowed to covariate or inverse before .NET 4.0? Because for the interface, the parameter T type can be used for both method parameters and method return value. Imagine such an interface
Interface IFoo(Of T)
Sub Method1(ByVal param As T)
Function Method2() As T
End Interface
interface IFoo<T>
{
void Method1(T param);
T Method2();
}
If we allow covariance to convert from IFoo<TSub> to IFoo<TParent>, then IFoo.Method1(TSub) will become IFoo.Method1(TParent). We all know that TParent cannot be converted to TSub safely, so the Method1 method will become insecure. Similarly, if we allow inverse IFoo<TParent> to IFoo<TSub>, the TParent IFoo.Method2() method will become TSub IFoo.Method2(). The originally returned TParent reference may not be converted into a TSub reference, and the call of Method2 will be insecure. It can be seen that without the limitations of additional mechanisms, it is not safe to covariate or inverse the interface. What has .NET 4.0 improved? It allows an additional description when declaring a type parameter to determine the scope of use of this type parameter. We see that if a type parameter can only be used for the return value of a function, then this type parameter is compatible with covariance. On the contrary, if a type parameter can only be used for method parameters, then this type parameter is compatible with inverse transformation. As shown below:
Interface ICo(Of Out T)
Function Method() As T
End Interface
Interface IContra(Of In T)
Sub Method(ByVal param As T)
End Interface
interface ICo<out T>
{
T Method();
}
interface IContra<in T>
{
void Method(T param);
}
It can be seen that both C#4 and VB10 provide similar syntax. Use Out to describe type parameters that can only be used as return values, and use In to describe type parameters that can only be used as method parameters. An interface can carry multiple type parameters, which can include both In and Out. Therefore, we cannot simply say that an interface supports covariance or inverse, but we can only say that an interface supports covariance or inverse for a specific type parameter. For example, if there is an interface like IBar<in T1, out T2>, it supports inverse change for T1 and covariance for T2. For example, IBar<object, string> can be converted to IBar<string, object>, where there are both covariance and reverse change.
In the .NET Framework, many interfaces use type parameters only for parameters or return values. For ease of use, these interfaces will be redeclared as versions that allow covariance or inverse in .NET Framework 4.0. For example, IComparable<T> can be redeclared as IComparable<in T>, while IEnumerable<T> can be redeclared as IEnumerable<out T>. However, some interfaces IList<T> cannot be declared as in or out, so they cannot support covariance or inverse.
Here are a few things that are easy to ignore for generic covariance and inverse change:
1. Only generic interfaces and generic delegates support variability of type parameters, and generic classes or generic methods are not supported.
2. The value type does not participate in covariance or inverse, and IFoo<int> can never become IFoo<object>, regardless of whether it is declared out. Because of .NET generics, each value type generates an exclusive enclosed construct type, which is incompatible with the reference type version.
3. When declaring properties, be careful that readable and writeable properties will use the type for both the parameters and the return value. Therefore, only read-only attributes allow the use of out type parameters, and only write-only attributes can use in parameter.
The interaction between covariance and inverse
This is a pretty interesting topic, let's first look at an example:
Interface IFoo(Of In T)
End Interface
Interface IBar(Of In T)
Sub Test(ByVal foo As IFoo(Of T)) 'Is it right?
End Interface
interface IFoo<in T>
{
}
interface IBar<in T>
{
void Test(IFoo<T> foo); //Is it right?
}
Can you see what's wrong with the above code? I declared in T and then used it for the parameters of the method and everything worked fine. But to your surprise, this code cannot be compiled and passed! Instead, such code has been compiled:
Interface IFoo(Of In T)
End Interface
Interface IBar(Of Out T)
Sub Test(ByVal foo As IFoo(Of T))
End Interface
interface IFoo<in T>
{
}
interface IBar<out T>
{
void Test(IFoo<T> foo);
}
What? It is obviously an out parameter, but we need to use it as a method parameter to legally? It does look a little surprised at first glance. We need to take some trouble to understand this issue. Now we consider IBar<string>, which should be able to co-enter IBar<object> because string is a subclass of object. Therefore (IFoo<string>) becomes (IFoo<object>). When we call this covariant method, an IFoo<object> will be passed as the parameter. Think about it, this method is co-transformed from (IFoo<string>), so the parameter IFoo<object> must be able to become IFoo<string> to meet the needs of the original function. The requirement for IFoo<object> here is that it can reverse into IFoo<string>! Instead of covariation. That is, if an interface needs to covariate against T, then the parameter types of all methods of this interface must support inverse to T. Similarly, we can also see that if the interface wants to support inverse T, then the parameter types of the method in the interface must support covariation of T. This is the principle of covariance-inverse interchange of method parameters. Therefore, we cannot simply say that the out parameter can only be used for return values. It is indeed only used to declare the return value type directly, but as long as a type assistance that supports inverse change, the out type parameter can also be used for parameter types! In other words, in addition to directly declaring method parameters, the in parameter can only be used for method parameters with the help of covariance. Only types that support T inverse T are not allowed as method parameters. To deeply understand this concept, it may be a bit confusing to see it for the first time. It is recommended to conduct more experiments if conditions permit.
The mutual influence of covariance and inverse in method parameters was mentioned just now. So will the return value of the method have the same problem? Let's look at the following code:
Interface IFooCo(Of Out T)
End Interface
Interface IFooContra(Of In T)
End Interface
Interface IBar(Of Out T1, In T2)
Function Test1() As IFooCo(Of T1)
Function Test2() As IFooContra(Of T2)
End Interface
interface IFooCo<out T>
{
}
interface IFooContra<in T>
{
}
interface IBar<out T1, in T2>
{
IFooCo<T1> Test1();
IFooContra<T2> Test2();
}
We see that it is just the opposite. If an interface needs to covariate or inverse T, then the return value types of all methods of this interface must support covariate or inverse T in the same direction. This is the principle of covariance-inverse consistency of the method return value. That is to say, even the in parameter can be used for the return value type of the method, as long as a reverse-inverted type is used as a bridge. If this process is not very clear, it is recommended to write some code to experiment. So far we find that covariance and inverse have many interesting features, so that in and out are not as easy to understand as they literally mean. When you see the in parameter appearing in the return value type and the out parameter appearing in the parameter type, don't faint. Use the knowledge in this article to crack the mystery.
Summarize
After the explanation of this article, you should have already understood the meaning of covariance and counter-change, and be able to distinguish the process of covariance and counter-change. We also discussed new features and new syntax for .NET 4.0 that support generic interfaces, covariance and inverse of delegates. Finally, we also compiled the principles of interaction between covariance, inverse change and function parameters, return values, and the wonderful writing method that arises. I hope that after reading my article, you can use this knowledge to use in generic programming and correctly use the new features of .NET 4.0. I wish you all a happy use!