With the introduction of generics, the concept of subtypes becomes more complicated. It is not easy to define methods with newly learned generics, but the compiler has various obstacles to use them. Listen as I break it down and put it back together.
subtypes
Any time you want to use A value of type A, you can replace it with A value of type B (as A value of A) and say that B is A subtype of A.
As you can see from the definition, any type is also a subtype of itself.
To put the definition more generally, “a small class can replace a large class.” Int is a subclass of Number because the set range of numbers represented by Int is a subset of the set represented by Number.
Is one class a subtype of another? This is an important problem for the compiler because it is checked every time you assign a value to a variable or pass an argument to a function. A variable is allowed to store a value only if the value’s type is a subtype of the variable.
Subtypes in generics
Before generics were introduced, subtypes were clearly defined, and it was straightforward to determine if one class was a subtype of another; for example, String is a subclass of CharSequence.
Once generics are introduced, it gets complicated, such as: Is List
a subclass of List
? In other words, can we use List
instead of List
? It’s kind of hard to make a quick judgment.
To solve this conundrum, two inferences need to be derived from the original definition:
- The range of parameters that a subtype method receives must not be smaller than that of a parent type method
Give a counter example: “1.0” Meituan claimed to accept WeChat and pay treasure to two kinds of payment, its subtypes “Meituan 2.0” claims can only accept WeChat payments, if use “Meituan 2.0” replacement “Meituan 1.0”, the original and the “1.0” Meituan interaction code may be an error, because they tend to pay treasure, but can’t deal with it “Meituan 2.0”. In order for “Meituan 2.0” to replace “1.0,” it must support at least wechat and Alipay, and there is no reason to add other payment methods.
- A subtype method must not return a greater range of values than a parent type method
A counterexample: The “Merchant 1.0” poster included rice and soup, so I prepared chopsticks and spoons. Its subgenre, “Merchant 2.0”, has suddenly added apples, but I’m not ready to eat them. So businesses can’t return more stuff, or I can’t cope.
For List
to subclass List
, the following two conditions must be met:
List<Sring>
The range of parameters received by the method inList<CharSequence>
The method ofList<Sring>
The range of values returned by the method inList<CharSequence>
The method of
List is defined as follows:
// List with generic definition
public interface List<E> extends Collection<E> {
boolean add(E e);
E get(int index);
}
// List
public interface List<String> extends Collection<String> {
boolean add(String e);
String get(int index);
}
// List
public interface List<CharSequence> extends Collection<String> {
boolean add(CharSequence e);
CharSequence get(int index);
}
Copy the code
Although String get(int index); Return value range than CharSequence get(int index); Small. Satisfies the second condition.
But the Boolean add (String) e; Boolean add(CharSequence e); Small. It doesn’t satisfy the first condition.
So List
is not a subtype of List
. In other words, it is not safe to replace the List
with the List
in the program, because there may be code such as:
val spannable = Spannable()
val list: List<CharSequence> = mutableListOf()
list.add(spannable)
Copy the code
If you replace List
here with List
, the compiler will report an error. Because Boolean add(String e); Only String arguments will be handled; Spannable is out of scope.
Not to distort
To express the above example as a more abstract definition:
Suppose the generic class A contains the type parameter T, that is, class A
, and Type1 is A subclass of Type2. If there is no parent-child relationship between A
and A
, then class A is said to be invariant on the type parameters.
Classes in Kotlin and Java are immutable.
This is a bit of a constraint, and we’ve worked hard to abstract a method that takes a List
as an argument: handle(chars: List
), assuming the compiler will report an error if we pass the List
in… Do you need to redefine the same method for List
?
covariance
Invariance describes the fact that there is no subtype relationship between generic classes. There is also a subtype relationship between generic classes called covariant.
Covariant means that the class changes in the same direction as the abstraction of its type parameters.
(When trying to sum up an abstract concept, he often says something that makes people confused…)
In other words: as the type parameters become more concrete, the class also becomes more concrete. As the type parameters become more abstract, the class becomes more abstract.
For example, if you go from List
to List
, the type parameter changes from String to the more abstract CharSequence, and if the direction of change from List
to List
is also more abstract (the former is a subclass of the latter), The List
is said to be covariant on the type parameter T. (Obviously this example is not covariant but invariable)
If a generic class is covariant, it means that it preserves subtype relationships for type parameters at the class level
In Kotlin, to declare that the class is covariant on the type parameter, we need to add the out reserved word:
class MyList<out T>{... }Copy the code
While declaring a generic class as covariant makes the subtyped relationship more intuitive, it comes at a cost:
class MyList<out T> {
fun set(item: T) {}// 错 错: Type parameter is declare as "out" but occur at "in" position in Type T
fun get(a): T {...}
}
Copy the code
-
If T appears in the parameter bit of the method, the set(item: T) is said to consume a value of type T.
-
If T occurs in the return value bit, it is called GET (): T produces a value of type T.
When T is decorated by out, it can only appear in the return value bit, that is, it can only be produced by the generic class, not consumed.
So out has two effects:
- It preserves the subtyping of generic classes.
- It limits the type parameter to only appear in the return value bit.
The two points are complementary: because it restricts type parameters from appearing in parameter bits, subtyping is preserved. Because it preserves subtyping, type parameters can only appear in the return value bit.
If the type parameter appears in the parameter bit, it will appear in the following case:
class MyList<String> {
fun set(itme: String)
fun get(a): String
}
class MyList<CharSequence> {
fun set(itme: CharSequence)
fun get(a): CharSequence
}
Copy the code
Because fun Set (itme: String) can accept a smaller range of parameters than fun Set (itme: CharSequence), which does not conform to the first fallback, MyList
is not a subtype of MyList
.
Adding out, however, tells the compiler to remove the method that failed to preserve subtyping:
class MyList<out T> {
fun get(a): T {...}
}
class MyList<String> {
fun get(a): String
}
class MyList<CharSequence> {
fun get(a): CharSequence
}
Copy the code
Fun GET (): String returns a smaller range than fun Get (): CharSequence, which fits the second corollary, so MyList
is a subtype of MyList
.
inverter
In addition to invariant and covariant, there is another subtype relationship between generic classes: contravariant.
Contravariant means that a class changes in the opposite direction to the abstraction of its type parameters.
In other words: As the type parameters become more concrete, the class becomes more abstract. As type parameters become more abstract, classes become more concrete.
Contravariant is a bit counterintuitive, it wants to achieve the effect that List
becomes a subtype of List
.
If a generic class is contravariant, it means that it reverses the subtype relationships of type parameters at the class level
In Kotlin, to declare that a class is invert on a type parameter, add the in reserved word:
class MyList<in T>{... }Copy the code
Again, there is a price to pay:
class MyList<in T> {
fun set(item: T) {}
fun get(a): T {... }// 错 错: Type parameter is declare as "in" but occur at "out" position in Type T
}
Copy the code
When T is modified by in, it can only appear in the argument bit, i.e. it can only be consumed by the generic class, not produced.
It follows that:
Out and int not only limit where arguments can appear, but also what classes can be subtypes.
Types of projection
The projection in our life is to take a three-dimensional object and turn it into a two-dimensional object, and the projection looks like it’s the same object just one dimension down.
A type projection in a program has a similar meaning:
Projecting a type means that some capabilities of the type are retained and others are removed. Type projection allows you to dynamically change the subtype relationships of generic classes.
Type projection is commonly used to dynamically transform invariant generic classes into inverters or covariant classes.
For example, a MutableList is a static one:
public interface MutableList<E> : List<E>, MutableCollection<E> {
// The type argument appears in the out position
public fun removeAt(index: Int): E
// The type argument appears at the in position
public fun add(index: Int, element: E): Unit. }Copy the code
MutableList
is invariant, so generic parameters can appear in either the in or out positions at will.
But invariance can sometimes narrow the scope of the method, such as:
fun <T> copy(source: MutableList<T>, destination: MutableList<T>){
for (item in source){
destination.add(item)
}
}
Copy the code
This is a way to copy a set. Generics were introduced to avoid having to redefine the method for each specific type. Now this method can copy contents in any two lists of the same data type.
But what if I want to copy a collection of strings into a collection that can contain any object?
val strings = mutableListOf( "a"."b"."c" )
val anys = mutableListOf<Any>()
copy( strings, anys )/ / an error
Copy the code
Because the definition of copy() requires that the source and destination collections have the same type.
To make the copy() method work in this case, rewrite it like this:
fun <R: T, T> copy(source: MutableList<R>, destination: MutableList<T>){
for (item in source){
destination.add(item)
}
}
Copy the code
Introduce a second generic type R, which is a subtype of T, and specify it as the source collection type parameter.
This change suddenly expands the range of argument types that the source parameter can accept. Instead of using only the same type as destination, it can now use all subtypes of destination.
This change can be simplified by using variations:
fun <T> copy(source: MutableList<out T>, destination: MutableList<T>){
for (item in source){
destination.add(item)
}
}
Copy the code
Where the source parameter is declared, an out type projection occurs, which removes all methods with consumption type parameters from the MutableList and retains all methods with production type parameters.
Although the source argument has lost some of its power, the sacrifice is always rewarded, and the range of types that source can accept has been expanded. (As it happens, the body of the copy() method doesn’t need the capabilities that source loses either.)
MutableList
and MutableList
public interface MutableList<out T> {
// Methods whose type arguments appear in the out position remain unchanged
public fun removeAt(index: Int): T
// Methods whose type arguments appear in the in position are overwritten
public fun add(index: Int, element: Nothing): Unit. }Copy the code
The out reserved word commands the compiler to overwrite all the methods in the generic class that consume type arguments, changing the argument type in the in position to Nothing.
Nothing is a subclass of all classes, so why do I change that? Because you want MutableList
to be a subtype of MutableList
.
Recall two corollary to the sudden-type:
- The range of parameters received by a subtype method must not be smaller than that of a parent type method
- A subtype method must not return a greater range of results than a parent type method
When there is no out projection, public fun add(index: Int, element: String): Unit accepts arguments with a range smaller than public fun add(index: Int, element: CharSequence): Unit accepts a range of parameters, which does not conform to rule 1, so MutableList
is not a subtype of Group
.
Change it to Public Fun Add (index: Int, Element: Nothing): Unit. Nothing is a subclass of all classes, it cannot be instantiated, and has no subtypes. In other words, if a method accepts an argument of type Nothing, it means that Nothing can be passed as an argument (the only Nothing that can be passed cannot be instantiated). This way public fun add(index: Int, element: String): Unit can take arguments that are larger than “nothing” (at least it can take strings).
Similarly, the in reserved word commands the compiler to override all production-type arguments in a generic class, changing the out position argument to Any? .
public interface MutableList<in T> {
// Methods whose type arguments appear in the out position are overwritten
public fun removeAt(index: Int): Any?
// Methods whose type arguments appear in the in position are reserved
public fun add(index: Int, element: T): Unit. }Copy the code
Any? Is the parent of all classes, which neatly makes MutableList
fit the second corollary (Any? Is already the maximum range. Whatever type is returned is a subclass of it.)
Finally, there is a special projection called the Star projection, which has the effect of adding the in projection and the out projection. (See table below)
There are three types of projections in Kotlin, summarized as follows (where Group, Dog, and Animal are all class names, and Dog is a subtype of Animal) :
Projection type | Projection instance | variant | Inheritance relationships | limit |
---|---|---|---|---|
Out of projection | Group< out Animal > | covariance | Group< Dog > is a subclass of Group< out Animal > | Type parameters cannot be used as method parameters |
In the projection | Group< in Animal > | inverter | Group< in Animal > is a subclass of Group< Dog > | Type parameters cannot be used as method return values |
Star projection | Group< * > | — | Group< any type > is a subclass of Group< * > | Type parameters cannot do method parameters and they cannot do return values |
Recommended reading
- Kotlin base | entrusted and its application
- Kotlin basic grammar | refused to noise
- Kotlin advanced | not variant, covariant and inverter
- Kotlin combat | after a year, with Kotlin refactoring a custom controls
- Kotlin combat | kill shape with syntactic sugar XML file
- Kotlin base | literal-minded Kotlin set operations
- Kotlin source | magic weapon to reduce the complexity of code
- Why Kotlin coroutines | CoroutineContext designed indexed set? (a)
- Kotlin advanced | the use of asynchronous data stream Flow scenarios