Brief description: The other day we laid a good foundation for Kotlin’s generic typing, taking a closer look at types and classes, the relationship between subtypes and subclasses, what subtyping relationships are, and why they exist. So today I’m going to talk about something a little bit more exciting, which is the hardest part of Kotlin generics, which is covariant, contravariant, invariant in Kotlin. It’s hard to understand, but it’s relatively easy to understand given the basics of how to overcome Kotlin’s generics variants (part 1). If you are a beginner, it is not recommended to read this article directly, but it is recommended to understand the previous part of the series.

Hell, I’ve been thinking about this for a few days, because the official conclusion is too formal, and hopefully the better developers just remember the official conclusion and the rules of use, but don’t really understand why, what’s the point of this design?

Without further ado, continue with the mind map of the previous article

1. Generic covariant – preserve subtyping relations

1. Basic definition and introduction of covariant

Remember the subtyping relationships from the previous article? Covariation is actually preserving the subtyping relationship. First of all, we need to clarify who is preserving the subtyping relationship for?

  • Basic introduction

Let’s take an example. String is String, right? We know that the base type List

is covariant, so List

is List

subtype. So obviously the roles that we’re targeting here are List

and List

, do they keep String to String? The subtyping relationship of. Or in other words, two generic covariant types that have the same underlying type have a subtyping relationship in the same direction if the type argument has a subtyping relationship. So you have a subtyped relationship where the value of a subtype can actually replace the value of a supertype anywhere at any time.
,>

,>

  • The basic definition
interface Producer<out T> {// Specify the out modifier before the generic type parameter
   val something: T
   fun produce(a): T
}
Copy the code

2. What is out covariant point

From the perspective of the basic structure of defined above, in fact, covariant point is the position that produce above the function return value T, specified in a generic Kotlin covariant class, in front of the generic parameter with out after the modification, then modify the generic parameter in the function for internal use scope will be limited only as a function return values or modify read-only access attributes.

interface Producer<out T> {// Specify the out modifier before the generic type parameter
   val something: T//T is the type of the read-only attribute, where T is also the out covariant point
   fun produce(a): T//T is output as the return value of the function, where T is the out covariant point
}
Copy the code

The above covariant points are standard T types. In fact, the following method is also covariant points. Please pay attention to the meaning of covariant points:

interface Producer<out T> {
   val something: List<T>// Even though T is not a single type, it modifies read-only properties as a generic type, so it is still at the out covariant point
   
   fun produce(a): List<Map<String,T>>// Even though T is not a single type, it modifies the return value as a type argument to a generic type, so it is still at the out covariant point
}
Copy the code

3. Basic features of out covariant points

Basic characteristics of covariant points: If a generic class declares a covariant, out-modified type parameter, the position within the function can only be the type of the read-only property or the return type of the function. Covariance is the role of producing generic parameters relative to the outside, and producers output out

4. Covariant –List<out E>Source code analysis

As we said in the previous article, the List in Kotlin is not the List in Java, because the List in Kotlin is a read-only List with no operations to modify the elements in the collection. A List in Java is actually the equivalent of a MutableList in Kotlin with various read and write operations.

Kotlin’s List

is actually an example of covariation, and it’s a perfect example of analyzing covariation. Remember step 2 of learning generics from the previous article, which is to analyze the source code to verify your understanding and conclusions. Through the following source code can verify the conclusion we said above.

// We can see from the generic class definition that the out modifier is used to modify the generic type parameter E
public interface List<out E> : Collection<E> {
    override val size: Int
    override fun isEmpty(a): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean/ / yi! Yi! Yi! It's not like that. Why is it still in this position? What the hell is @unsafevariance? I'm telling you, hold on, hold on, listen to me as I go along, keep the mystery for a while
    override fun iterator(a): Iterator<E>Iterator
       
        
         
          
           
            
            
           
          
         
        
       
      

    override fun containsAll(elements: CollectionThe < @UnsafeVariance E>): Boolean
    public operator fun get(index: Int): E// The type of the return value of the function E is at the out covariant point, which validates the standard type of the covariant described above.
    public fun indexOf(element: @UnsafeVariance E): Int

    public fun lastIndexOf(element: @UnsafeVariance E): Int

    public fun listIterator(a): ListIterator<E>//(E is the generic type of the type argument), is the out covariant point

    public fun listIterator(index: Int): ListIterator<E>//(E is the generic type of the type argument), is the out covariant point
    public fun subList(fromIndex: Int, toIndex: Int): List<E>//(E is the generic type of the type argument), is the out covariant point
}
Copy the code

Source code analysis finished, is it still a little confused feeling ah? Why is E in any other place, and what the hell is @unsafevariance? But at least it proves that the generic covariant out is the type of the return value and the type of the read-only property (this is not shown in the source code, but in fact it is, check out other examples here).

2. Generic inverter-inverse rotor typing relationship

1. Basic definition and introduction of inverse

  • Basic introduction

Contravariant is actually the opposite of covariant subtyping, which is the inverse rotor typing.

So just to illustrate, we know that String is String, right? Comparable

is contraband, then Comparable

to Comparable

actually reverses String to String? Subtyped relation of, that is, and String to String, right? Comparable

is a subtype of Comparable

. Comparable

can be used anywhere a value of Comparable

type value instead.
?>


?>
?>

In other words: two generic contravariant types with the same base type, if the type argument has a subtyping relationship, then the generic type has a subtyping relationship in the opposite direction

  • The basic definition
interface Consumer<in T>{// Specify the in modifier before the generic type parameter
   fun consume(value: T)
}
Copy the code

2. What is the inverse point of IN

From the basic structure defined above, in fact, the contravariant point is the position T of the consume function that receives the function parameter. Kotlin specifies a generic covariant class, and after adding out modifier in front of the generic parameter, The use of the modifier generic parameter inside a function is limited to the return value of the function or to the property of the modifier read-only permission.

interface Consumer<in T>{// Specify the in modifier before the generic type parameter
   var something: T //T is the type of the variable property, where T is also the inverse point of in
   fun consume(value: T)//T is the function parameter type, where T is the inverse point of in
}
Copy the code

Like covariant, contravariants have generic types that are at contravariant points, which we can think of as contravariant points:

interface Consumer<in T>{
   var something: B<T>// This is a generic type, but the position of T is still modifying the variable property type, so it is still at the inverse point
   fun consume(value: A<T>)// This is a generic type but T is still a function parameter, so it is still at the contravariant point
}
Copy the code

3. Basic characteristics of in contravariant points

If a generic class is declared contravariant, use in to modify the type parameter of the generic class. Invert is the role of consuming generic parameters as opposed to external, where the consumer requests external input in

4. Invert –Comparable<in T>Source code analysis

Comparable

is actually the simplest example of a generic inversion in Kotlin

public interface Comparable<in T> {// Generic inverters use the in keyword modifier
    /** * Compares this object with the specified object for order. Returns zero if this object is equal * to the specified [other] object, a negative number if it's less than [other], or a positive number * if it's greater than [other]. */
    public operator fun compareTo(other: T): Int// Since T is contravariant, the position of T inside the function is the parameter type of the compareTo function, which can be seen as consuming generic parameters
}
Copy the code

3. Generic invariant – no subtyping relationship

Constant basic definition and introduction

  • Basic introduction

For invariance, it is even simpler to remove the covariant and contravariant in the generic variant. In fact, immutable looks like our usual generic type, which has neither in nor out modifier. It’s a generic type, so it obviously doesn’t have as many rules as covariant or contravariant types. It’s free to be both readable and writable. It can be used as a return value type of a function or as a parameter type of a function, and can be declared as read-only or mutable. But note that an immutable type has no subtyping relationship, so it has a limitation that if it’s a function parameter type, it can only be passed in the same type as it, because there’s no such thing as a subtyping relationship, that is, no value of type can replace it. In addition to its own types such as MutableList

and MutableList

is a completely different type, although String is String, right? Subtypes, but the underlying generic MutableList

is invariant, so MutableList

and MutableList
?>


?>

  • The basic definition

interface MutableList<E>{// No in and out modifications
   fun add(element: E)//E can be a function parameter type at the inverse point, input consumption E
   fun subList(fromIndex: Int, toIndex: Int): MutableList<E>// the return type of the function E is covariant, producing the output E
}

Copy the code

Four, by covariant, contravariant, invariable rules to trigger some thinking

Think about one:

Is the generic parameter T of a covariant generic class necessarily only out of the covariant point position? Can we be at the inverse point of in?

Myth 1: It can be at the contravariant point, but the generic parameter T must be guaranteed to have no write operation inside the function, only read operation

A covariant generic class is declared, but sometimes a function parameter of that type needs to be passed in from the outside. Then the parameter type is at the inverse point of in, but inside the function there is no write to the generic parameter. A common example of this is the List

source code, where everyone is confused as to why the generic T defined as a covariant runs to the function parameter type. As shown in the following section of code:

  override fun contains(element: @UnsafeVariance E): Boolean/ / yi! Yi! Yi! It's not like that. Why is it still in this position? What the hell is @unsafevariance? Now the answer is that you might be there, but just make sure the function doesn't write
Copy the code

The contains function parameter in the List above is the generic parameter E, which is covariant and appears at the contravariant point, as long as no writes are made to it inside the function

Think about two:

Does T of a contravariant generic class have to be at the inverse point of in? Student: Can you be at the out covariant point?

Solution 2: Similarly, it can be at the covariant point

Think about three:

Can it be in another seat? Like constructors

Myth # 3: It can be in the constructor function, because this is a special location that is neither in nor out


class ClassMates<out T: Student>(vararg students: T){// This declaration is still valid even though it is defined as covariant, but T is not at the covariant point out. }Copy the code

Note: this is very special scene, so the beginning said if these rules, usage just rote memorization, met the scene began to doubt the life, rules, not so rules defined in the covariance is read-only attribute types and the location of the function return value type, how do you explain this position does not fit on? Therefore, to solve the problem, we need to grasp the key of the problem is the most important.

It’s not hard to explain the problem, but to get back to the original purpose of typing, which is to solve type safety problems and prevent more generic instances from calling some risky operations. Constructors are special in that they can’t be called after you create an instance object, so T is safe to put in there.

Think about the four

To be safe, should I just define all generic classes as covariant, contravariant or invariant?

Solution 4: No, it is not safe. According to the actual requirements of the scene, blindly defining as covariant or contravariant actually limits the possibility that the generic class can use this type of parameter, because out can only be a producer, and the position of covariant point is limited, while in can only be the position of contravariant point of consumer. That is, it loses the subtyping relation, that is, it is used as a function parameter type, the external can only pass in the same type as it, and there is no possible preservation and inversion of the subtyping relation

Five, by thinking to understand the essence of covariant point, contravariant point

From the above thinking to understand that the use of covariant, contravariant is not so dead in accordance with the rules of covariant point, contravariant point, can be more flexible, the key is not to violate the fundamental purpose of covariant, contravariant point. The covariant principle is that there can be no write operations inside the generic class defined, and the contravariant fundamental principle is generally only write. The List

in Kotlin source code is not covariant as the real rule says. The generic parameter E is not always covariant on the point out, but the List

inside can guarantee that there is no dangerous write behavior so this definition is legal. In practice, it is very difficult to make a generic type parameter in a covariant generic class at the out covariant point, because sometimes requirements do require that a function parameter of that type be passed in externally.

Therefore, the final conclusion is that the position rules of covariant point OUT and contravariant point in are generally to be observed, but need to be analyzed in a specific case. According to the specific situation of the generic class designed, the position rules of covariant point and contravariant point should be appropriately changed without violating the fundamental purpose and meeting the needs

6. Understand the application of UnSafeVariance annotation in development by essential difference

By the above analysis of essential differences, strictly in accordance with the rules of covariant point and contravariant point can not fully meet our real development requirements, so sometimes need a back door, that is to use a special way to tell it. That is to use the UnSafeVariance annotation. So the UnSafeVariance annotation is very simple: @unsafevariance tells the compiler that it can control the security and let it pass if the compiler does not consider it illegal. Annotation means unsafe type changes. For example, if a function in a covariant generic class takes a function parameter that takes that generic parameter, shutting down the compiler with the UnSafeVariance annotation and then placing it at the contravariance point actually adds a layer of risk to the developer. Operation is safe as long as the developer can ensure that there is no dangerous inside.

Covariant, contravariant, invariant comparative analysis, use and understanding

1. Analysis and comparison

From the basic structure, untyped relationships (reserved, reversed), typeless variant points (covariant point out, contravariant point in), roles (producer output, consumer input), the location of type parameters (covariant is modifying read-only attributes and function return value types; Contravariant is to modify variable attributes and function parameter types), performance characteristics (read only, write, read and write) and so on

covariance inverter The same
The basic structure Producer<out E> Consumer<in T> MutableList<T>
Subtyping relationships Preserve the subtyping relationship Inverse rotor typing relation No subtyping relationship
There is no type change point Covariance points out Inverter points in No change point
The location where the type parameter exists Modifies read-only attribute types and function return value types Modifies variable attribute types and function parameter types Either way, no constraints
role The producer output is a generic parameter type The consumer input is the generic parameter type Both producer and consumer
Performance characteristics Internal operation read only Internal operations only write Internal operations can be read and written

2. Use comparisons

It’s really about figuring out when to use covariant, when to use contravariant, and when to use invariant. In fact, through the above analysis and comparison of the table, we can draw a conclusion: First of all, the table has many conditional characteristics, which is the best first to judge the condition? There’s actually a choice here.

Suppose a: Like the start conditions for use of subtyping relationship judgment, so it is a bit of a problem, think in the actual development, first, to define a generic class inside some of the methods and properties, it is difficult to know at this time in an external usage under no using subtyping relationship, that is, the value of the deposit does not exist with subtype replacement value of the type of super scenarios, So it’s hard to know when you’re defining a generic class. So it’s clearer to start with the internal characteristics of the generic class definition.

Hypothesis 2: For example, some methods and attributes are defined internally according to the generic class. Since it cannot be determined whether it is covariant OUT or contravariant in at the beginning of the definition, the type-free change point above cannot be used as a judgment condition. When it is not determined at the beginning, it is generally defined as invariant generic class.

, the most straightforward can first look at the type change point, and then based on the type change point basically determine the internal characteristics of the generic class,

  • Step 1: First, make a preliminary determination based on where the type parameter exists
  • Step 2: Then, judging the performance characteristics is defined in a generic class inside is not only involves the generic parameter read-only operations (covariant or the same), or write operation (inverter or the same), or can not only read but also write (constant) here can only judge the two combinations (covariant or the same), one of the (inverter or the same), Because if you only have read operations that’s covariant or invariant, if you only have write operations that’s contravariant or invariant
  • Step 3: in the end, to see whether there is relationship between subtyping, if is is obtained by step 2 (covariant or unchanged) plus a subtyping relationship eventually get use covariance, if is obtained by step 2 is unchanged (inverter or) plus subtyping relationship eventually get using inverter, if there is no subtyping relation with the same.

As a side note, if it turns out to be covariant, but the type parameter is defined in the position of the function parameter by step 1, then you can use the @unsafevariance annotation to tell the compiler to make the compilation pass. The same goes for contravariance.

So let me draw a picture of that

3. Understand the contrast

Remember the example and the cartoon at the beginning of the last article

  • Understanding of covariation:

Example code is as follows:

fun main(args: Array<String>) {
    val stringList: List<String> = listOf("a"."b"."c"."d")
    val intList: List<Int> = listOf(1.2.3.4)
    printList(stringList)// Pass a List
      
        argument to the function, where List
       
         can be substituted for List
        
       
      
    printList(intList)// Pass a List
      
        argument to the function, where List
       
         can be substituted for List
        
       
      
}

fun printList(list: List<Any>) {
List
      
        = List
       
         = List
        
          = List
         
           = List
         
        
       
      
    list.forEach {
        println(it)
    }
}
Copy the code

To understand:

For printList, what it needs is that the List

type is a more general type than the concrete type, and that operations inside the function don’t involve modifying writes, and then passing in a more specific subtype outside of the function is definitely the most basic requirement for the desired generalization type. So the more concrete subtypes List

and List

passed in from the outside are more compatible.


  • Understanding of inverse:

Example code is as follows:

class A<in T>{
    fun doAction(t: T){... }}fun main(args: Array<String>) {

    val intA = A<Int> ()val anyA = A<Any>()

    doSomething(intA)// Invalid,
    doSomething(anyA)/ / legal
}

fun doSomething(a: A<Number>){No more specific type than A
      
        can be passed outside of doSomething because A write operation is involved inside the function.
      . }Copy the code

To understand:

For doSomething, it needs A

that is more specific than the generic type. Since the inverse of the generic class A, the operation inside the function is free to write, try not to pass in A more specific comparator object outside the doSomething function. If it is legal to pass in A

, then the internal function is still A

. If it is written inside the function, it is possible to write A Float into it. Since it is perfectly legal to write A Float to Number, but the external input is actually A

, it would be A mistake to write A Float to A

, so the null hypothesis is not valid. So inverters release write permissions, so the requirements for external incoming types are more stringent.




Which leads to another question, why are contravariant writes safe? Consider also is very simple, for inverter generic types as a function parameter type, and so on the outside of the function of afferent argument type has to be more generalized than function parameter of type cannot be more specific, so the function inside the most specific type of operation is function parameter type, so definitely can bold write operation. For example, the A

type parameter type, in the doSomething function it is explicitly known that the outside cannot be more specific than that, so it is ok to write on the basis of A

inside the function.

  • The code for the invariant understanding example is as follows:
fun main(args: Array<String>) {
    val stringList: MutableList<String> = mutableListOf("a"."b"."c"."d")
    val intList: MutableList<Int> = mutableListOf(1.2.3.4)
    printList(stringList)// This is actually not compiled
    printList(intList)// This is actually not compiled
}

fun printList(list: MutableList<Any>) {
    list.add(3.0f)// Start importing dangerous operations. dangerous! dangerous!
    list.forEach {
        println(it)
    }
}
Copy the code

To understand:

Immutable is actually A little bit easier to understand, because there is no subtyping relationship, there is no rule that A value of subtype A can replace A value of supertype B anywhere and at Any time, so the above example compacts. However, for printList, the type you must accept is MutableList

, Because once you pass in a specific type that is not the same as it, there will be dangerous operations, there will be unsafe problems.

Eight, epilogue

Due to space reasons, so star projection and covariant, contravariant practical examples of the application to the next application, but here Kotlin generic variant focus and difficulties have all been covered, the following is the use of practical development examples. About this article or need to digest, finally according to the next practical example can be more consolidated, the next chapter will focus on the development of the example implementation, will not buckle the concept. Stay tuned for next article ~~~

Welcome to Kotlin’s series of articles:

Original series:

  • How to overcome the difficulties of Generic typing in Kotlin (Part 1)
  • Kotlin’s trick of Reified Type Parameter (Part 2)
  • Everything you need to know about the Kotlin property broker
  • Source code parsing for Kotlin Sequences
  • Complete analysis of Sets and functional apis in Kotlin – Part 1
  • Complete parsing of lambdas compiled into bytecode in Kotlin syntax
  • On the complete resolution of Lambda expressions in Kotlin’s Grammar
  • On extension functions in Kotlin’s Grammar
  • A brief introduction to Kotlin’s Grammar article on top-level functions, infix calls, and destruct declarations
  • How to Make functions call Better
  • On variables and Constants in Kotlin’s Grammar
  • Elementary Grammar in Kotlin’s Grammar Essay

Translation series:

  • Kotlin’s trick of Reified Type Parameter
  • When should type parameter constraints be used in Kotlin generics?
  • An easy way to remember Kotlin’s parameters and arguments
  • Should Kotlin define functions or attributes?
  • How to remove all of them from your Kotlin code! (Non-empty assertion)
  • Master Kotlin’s standard library functions: run, with, let, also, and apply
  • All you need to know about Kotlin type aliases
  • Should Sequences or Lists be used in Kotlin?
  • Kotlin’s turtle (List) rabbit (Sequence) race
  • The Effective Kotlin series considers using static factory methods instead of constructors
  • The Effective Kotlin series considers using a builder when encountering multiple constructor parameters

Actual combat series:

  • Use Kotlin to compress images with ImageSlimming.
  • Use Kotlin to create a picture compression plugin.
  • Use Kotlin to compress images.
  • Simple application of custom View picture fillet in Kotlin practice article

Welcome to the Kotlin Developer Association, where the latest Kotlin technical articles are published, and a weekly Kotlin foreign technical article is translated from time to time. If you like Kotlin, welcome to join us ~~~