Covariance
The concept of covariance is puzzling, and it is mostly a pot of naming or translation, which is actually very easy to understand.
For example, there is a dog on the street, and I said, look, there is an animal here! This is very natural. Although animals are not strictly equivalent to dogs, no one will think what I said is wrong. Turning a dog into an animal is covariance. C# also supports this:
// C#6 Top StatementDog dog= new Dog(); Animal animal= dog; interface Animal {} class Dog : Animal {}
Then next, there was a group of dogs on the street. I said there was a group of animals. It was also true, but it seemed that C# didn't think so.
List<Dog> dogLst = new List<Dog>(); List<Animal> aniLst = dogLst; //It's soaring redinterface Animal {} class Dog : Animal {}
The reason is actually easy to understand. After all, in the above code, Dog:Animal is written, that is, it declares that dogs are subclasses of animals, but it is not written, List<Animal>: List<Dog>. In other words, it has never been declared that a group of dogs are subclasses of animals.
However, if you do not use List but use its parent class IEnumerable and write it as follows, you will not report an error.
List<Dog> dogLst = new List<Dog>(); IEnumerable<Animal> aniLst = dogLst;
In other words, C# recognizes List<Dog> as a subclass of IEnumerable<Animal>. The difference is just a glance at the source code and you will know:
public interface IEnumerable<out T> : IEnumerable public class List<T> : ..., IEnumerable<T>, ...
IEnumerable has just one more out parameter than List. With this parameter, it has covariation function. Therefore, when U is a subclass of T, it can support the conversion of IEnumerable<U> to IEnumerable<T>.
In the official documentation, generic interfaces with out keywords are specified including IEnumerable<T>, IEnumerator<T>, IQueryable<T> and IGrouping<TKey,TElement>.
Implementation of covariant interface
Covariance and inversion can only be used in generic interfaces and delegates at present. Create a new generic interface below and use the keyword out. Since the top-level statements of .Net6.0 are used, the declarations of interfaces and classes are placed behind.
IOut<string> outStr = new Out(); IOut<object> outObj = outStr; (()); interface IOut<out T> { T getName(); } class Out : IOut<string> { public string getName() { return GetType().Name; } }
Compile and run, and finally output Out, that is, outObj Although IOut<object> is used when declaring, under the action of the out modifier of IOut, IOut<object> has been successfully made the parent class of IOut<string>, and the methods in Out can be called smoothly.
Then, if you want to make getName more complete, for example, require the function of getName(T name), then the out modified covariant interface will be powerless. The following writing method is indeed ruthlessly popular.
interface IOut<out T> { void getName(T name); }
Inversion
As the top IDE in the universe, VS is very clear inverter. While the above code is so popular, it directly gives the following error.
Variation is invalid: The type parameter "T" must be an inverse that is valid on "(T)". "T" is covariance.
In other words, if you want the generic interface to enter generic parameters, you need to use inverter. The specific writing method is as follows, where the modifier in represents inverter
IIn<object> inObj = new In(); IIn<string> inStr = inObj; ("in"); interface IIn<in T> { void getName(T name); } class In : IIn<object> { public void getName(object name) { (name); } }
The biggest difference between inverter and covariance is not the word count of the two modifiers in and out, but the entire replacement logic has changed. In the above code, the string as a subclass actually calls a function defined by the parent class object as a parameter.
Lisch replacement principle
After implementing covariance and inversion in detail, I always feel that it is strange. The most strange thing is actually the error in the following line of code.
//Wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong,interface IOut<out T> { void getName(T name); }
And it can be imagined that the corresponding inverter code below is also wrong.
//Wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong, wrong,interface IOut<in T> { T getName(); }
Next, let’s review the reasons for this phenomenon. In order to eliminate the troubles caused by naming, consider the generic interface I<T>, which has a function T test(T t). There are two specific classes that inherit from the generic interfaces I<A> and I<B>. Assuming that I<A> wants to call the methods in I<B>, the process is as follows.
I<A>.test(A t), that is, enter a parameter of type A
2. Pass this parameter t of type A to B I<B>.test(B t). Since I<B> requires input of type B parameters, A can be converted to type B.
I<B>.test(B t) has been calculated and returns a B-type parameter
4. This B-type parameter is returned to the original caller A I<A>.test, and the I<A> function will eventually return a A-type parameter. In other words, in this step, B is required to be converted to A.
A can be converted to B, and then B can be converted to A, and A and B are not equal, which is obviously impossible.
Therefore, inversion and covariation have achieved steps 2 and 4 respectively.
If I<A> wants to call the function in I<B>test(Bt), then type A must be converted to type B. Just as string can be converted to object, this is inverter, modified with in, and its effect is when the subclass calls the methods in the parent class.
If I<A> wants to call B I<B>test(), then the B type as the return value must be converted to A type, which is covariance, modified with out, which is exactly the method of the parent class calling the child class.
The unity of covariance and inversion is that both strictly follow the rules that this subclass can be transformed into a parent class, which is Richter replacement. This was proposed by Barbara Liskov in 1987, and she was also the winner of the 2008 Turing Award.
In the process of covariance inversion, the following of Richter replacement is mainly manifested in that when the subclass method overloads the parent method
- The input parameters of the method should be looser, which is inverter (IOut<object> calls IOut<string>, object is looser than string)
- The return value of the method should be more stringent, which is covariance (IOut<string> calls IOut<object>, string is more stringent than object)
The above is an article that will help you learn about the detailed content of covariance and inversion in C#. For more information about covariance in C#, please pay attention to my other related articles!