Generics and delegates

Basic usage of generics

Java introduced generics as early as version 1.5, so Kotlin naturally supports generics. But there are some similarities and differences between generics in Kotlin and generics in Java, so let’s look at the similarities in Java first.

Generics, meaning that under normal programming mode, we need to specify a specific type for any variable. Generics allow us to write code without specifying a specific type, so that the code will have more extensibility. List, for example, is implemented using generics.

There are two main ways to define generics, one is to define generic classes, the other is to define generic methods, the use of the syntax structure is

, this T is just a convention of generic writing.

fun main(a){
    // How generic classes are called
    val myClass = MyClass<Int> ()val result = myClass.method(123)
    println("$result")

    // How generic methods are called
    val myClass2 = MyClass2()
    // Type inference mechanism, so generic specification can be omitted.
// val result = myClass.method
      
       (123)
      
    val result2 = myClass2.method(123)
    println("$result2")}/** * defines the generic class */
class MyClass<T>{

    The /** * method allows arguments of type T and return values */
    fun method(param: T):T{
        return param
    }
}

/** * defines the generic method */
class MyClass2{

    /**
     * 泛型方法
     */
    fun <T> method(param: T):T{
        return param
    }

    /** * restrict the type of a generic by specifying an upper bound. * This means that only method generics can be specified as numeric types, such as Int, Float, Double, and so on. * * Also, by default, all generics can be specified as nullable types. This is because the default upper bound of generics is Any? . * If you want the type of a generic to be non-null, manually specify the upper bound of the generic to be Any. * /
    fun <T:Number> method2(param: T):T{
        return param
    }
}
Copy the code
fun main(a){
    contentResolver.query(uri,null.null.null.null)? .apply {while (moveToNext()){
             ... 
         }
         close()
    }

    contentResolver.query(uri,null.null.null.null)? .build {while (moveToNext()){
            ...
        }
        close()
    }
}

/** * As we learned earlier, this function is basically the same as the apply function, except that it only works on SB. * /
fun StringBuilder.build(block:StringBuilder. () - >Unit):StringBuilder{
    block()
    return this
}

/** * Use generics to extend the above function to perform exactly the same functions as apply. * /
fun <T> T.build(block: T. () - >Unit):T {
    block()
    return this
}
Copy the code

Class delegates and delegate properties

Delegation is a design pattern based on the basic idea that an operating object does not process a certain piece of logic itself, but rather delegates work to another auxiliary object. Java has no language-level implementation of delegates, whereas languages like C# have native support for delegates. Delegation is also supported in Kotlin and is divided into two types: class delegates and delegate attributes.

The core idea of class delegate is to delegate the implementation of a class to another class. A Set is a data structure that is an interface that needs to be implemented using its concrete implementation class, such as HashSet. With the delegate pattern, we can easily implement an implementation class of our own:

The /** * constructor takes a HashSet argument, which acts as a helper object. * Then, in all method implementations of the Set interface, there is no implementation of its own, * but the corresponding method implementation in the auxiliary object, which is actually a delegation mode. The significance of the delegate pattern is that MySet will become a new data structure class by allowing most of the method implementations to call the methods in the auxiliary object, and rewriting a few of the methods themselves, or even adding some of their own methods. The downside of this approach is that if there are too many methods to implement in the interface, each of them will have to call the corresponding method implementation in the auxiliary object. * This problem, which has no solution in Java, can be solved in Kotlin through the class delegate functionality. * /
//class MySet<T>(val helperSet: HashSet<T>):Set<T> {
//
// override val size: Int
// get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
//
// override fun contains(element: T): Boolean {
// TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
/ /}
//
// override fun containsAll(elements: Collection
      
       ): Boolean {
      
// TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
/ /}
//
// override fun isEmpty(): Boolean {
// TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
/ /}
//
// override fun iterator(): Iterator
      
        {
      
// TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
/ /}
/ /}

/** * The delegate keyword in Kotlin is by, and you can save yourself a lot of template code by using the by keyword at the end of the interface declaration and attaching the delegate helper object. * * These two pieces of code achieve the same effect. * /
//class MySet<T>(val helperSet: HashSet<T>):Set<T> by helperSet{
//
/ /}

/** * If you want to reimplement a method, just rewrite it separately. * Other Set interface functions, same as HashSet. * /
class MySet<T>(val helperSet: HashSet<T>):Set<T> by helperSet{

    /** * New method */
    fun helloWorld(a) = println("Hello World")

    /** * override method */
    override fun isEmpty(a) = false
}
Copy the code

The core idea of delegating attributes is to delegate the implementation of an attribute (field) to another class.

class MyClass {

    The /** * by keyword connects the p property on the left to the Delegate() instance on the right. * This means that the implementation of the p property is delegated to the Delegate class. * The Delegate class getValue() is automatically called when the p property is called, and the Delegate class setValue() is automatically called when assigning a value to the p property. * * Assuming the val keyword is used here, you just need to implement getValue(), * since the p attribute cannot be reassigned after initialization. * /
    var p by Delegate()
}

/** * Implements the Delegate class */
class Delegate{

    var propValue: Any? = null

    / * * *@paramMyClass is used to declare what classes the delegate function of that class can be used in, which is only used in myClass. *@paramProperty is a property manipulation class in Kotlin that can be used to get various property-related values. * In addition, <*> indicates that you do not know or care about the specific type of the generic, just for syntactic compilation, similar to the <? > < p style = "max-width: 100%; clear: both; * The return value can be declared as any type, depending on the implementation logic. This is just a standard example. * /
    operator fun getValue(myClass: MyClass, property: KProperty< * >): Any? {
        return propValue
    }

    / * * *@paramAny indicates the value to be assigned to the delegate property. The type of this parameter must be the same as that returned by getValue(). * /
    operator fun setValue(myClass: MyClass, property: KProperty<*>, any: Any?). {
        propValue = any
    }
}
Copy the code

Implement a lazy function of its own (some aspects such as synchronization, null-value handling, etc., are not implemented rigorously)

/** * Decrypt by lazy: * Lazy loading technique, the code in the code block is executed only when the variable is first called. * Here, by is the key word in Kotlin, and lazy is just a higher-order function. * In the lazy function, a Delegate object is created and returned. When the p property is called, the Delegate object's getValue() is called, and the Lambda expression passed in by the lazy function is then called from getValue(). This allows the code in the expression to be executed, * and the resulting value from calling the p attribute is the return value of the last line of code in the Lambda expression. * /
val p by lazy {  }

/** * uses the self-implemented lazy function */
val p by later {
        // Verify that lazy loading of the later function takes effect.
        // Place this code in any Activity and call the p property in the button click event.
        // When the Activity starts, the log line in the later function is not printed. It prints only when the button is called for the first time.
        // Also, the code in the code block is executed only once, and clicking the button again does not print a log.
        Log.d("TAG"."run codes inside later block")
        "test later"
}
Copy the code
/** * define the Later class and specify it as generic. * The constructor takes a function type argument, which takes no arguments and returns the generic type specified by Later. * * Since lazy loading techniques do not assign values to attributes, setValue() is not implemented here. * /
class Later<T>(val block:() -> T){

    /** * Caches the value using the value variable, calling the function type argument passed in the constructor to get the value if it is null, or returning it if it is not. * /
    var value: Any? = null

    / * * *@paramAny any? Type indicates that you want Later's delegate functionality to be available to all classes. * /
    operator fun getValue(any: Any? ,prop:KProperty< * >): T{
        if (value == null){
            value = block()
        }
        return value as T
    }
}

/** * To make the use of Later more similar to lazy, define a top-level function. * And define it outside of the Later class, because only functions that are not defined in any class are top-level functions. * * Replaces lazy with later when used. * /
fun <T> later(block: () -> T) = Later(block)
Copy the code

Infix function

Use the Infix function to build a more readable syntax

To construct key-value pairs for syntactic constructs such as A to B, including Kotlin’s built-in mapOf() function, is not because to is A keyword in Kotlin, but because Kotlin provides an advanced syntactic sugar feature: the infix function.

The infix function tweaks the syntax of programming language function calls, such as A to B, to be equivalent to a.dot (B).

Example 1:

fun main(a){
    // Checks if a string starts with a specified argument
    if ("Hello Kotlin".startsWith("Hello")) {// Handle concrete logic
        println("true")}else {
        println("false")}// Implement a more readable syntax with infix to express the above code. A special syntax sugar format.
    // ------------------------------------------------------------
    // The syntax of infix is:
    // Call "Hello Kotlin" beginsWith() and pass in a string argument.
    // The infix function allows you to remove the decimal point, parentheses, and other computer-specific syntax from function calls.
    // Write the program in a more English style to make the code more readable.
    // ------------------------------------------------------------
    // Infix syntax limits:
    A function cannot be defined as a top-level function. It must be a member function of a class. It can be defined as an extension function to a class.
    // Only one parameter must be received. There is no limit on the type of the parameter.
    // The syntactic sugar of the infix function is available only if both of these conditions are met.
    // ------------------------------------------------------------
    if ("Hello Kotlin" beginsWith "Hello") {// Handle concrete logic
        println("true")}else {
        println("false")}}/** * This is a String extension function that becomes an infix function by adding the infix keyword. * beginsWith() is also used to determine if a String startsWith a specified argument, and its internal implementation is the startsWith() of the calling String class. * /
infix fun String.beginsWith(prefix: String) = startsWith(prefix)
Copy the code

Example 2:

fun main(a){
    val list = listOf("Apple"."Banana"."Pear")
    if (list.contains("Apple")){
        println("true")}else {
        println("false")}val list1 = listOf("Apple"."Banana"."Pear")
    if (list has "Apple"){
        println("true")}else {
        println("false")}}Contains (); /** * contains(); /** * contains(); /** * contains(); /** * contains(); /** * contains(); * * Collection is the general interface to all Java and Kotlin collections, so has() can be used by all subclasses of collections. * /
infix fun <T> Collection<T>.has(element: T) = contains(element)
Copy the code

Example 3:

fun main(a){
    val map = mapOf("Apple" to 1."Banana" to 2."Pear" to 3)
    for (i in map){
        println("i is $i, key is ${i.key}, value is ${i.value}")}// Copy the source code for the to() function to write your own key-value pair builder.
    val map1 = mapOf("Apple" with 1."Banana" with 2."Pear" with 3)
    for (i in map1){
        println("i is $i, key is ${i.key}, value is ${i.value}")}}/ * * * imitation to () function of the source code to write your own key values for the construction of function * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- * to () * public infix fun 
      
        a.too (that: B): Pair (A, B > = Pair (this, that) * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- * first of all, Defines to() to type A by defining A generic function and takes A parameter of type B. * Thus A and B can be two different types of generics, which allows us to construct key-value pairs such as string to integer. * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- * to the realization of a () function: MapOf () actually receives A list of variables of type Pair. MapOf () creates and returns A Pair. * /
      ,>
infix fun <A,B> A.with(that:B): Pair<A,B> = Pair(this,that)
Copy the code

Advanced features of generics

Kotlin’s unique capabilities in generics

Implement generic types

Before JDK 1.5, Java didn’t have generics, and data structures like Lists could store arbitrary types of data, requiring manual downtransitions to retrieve data that were cumbersome and dangerous. For example, if you store string and integer data in the same List, but cannot distinguish the specific data type when fetching data, if you manually cast them to the same type, you will throw a cast exception.

So in JDK 1.5, Java introduced generics. This not only makes data structures like lists easier to use, but also makes code more secure. But in reality, Java’s generics capabilities are implemented through type erasure mechanisms.

The constraints of generics on types only exist at compile time, and the runtime is still run under the pre-jdk 1.5 mechanism, and the JVM does not recognize generic types specified in code. For example, the List

collection, while only String elements can be added at compile time, at run time the JVM does not know which type of element it was intended to contain, only that it is a List.

So jVM-based languages implement generics through type erasure mechanisms that make it impossible to use syntax such as a is T or T::class.java because the actual type of T is removed at runtime.

However, Kotlin provides the concept of an inline function where the code in an inline function is automatically replaced at compile time to the place where it was called. In this way, there is no problem with generic erasing because the code directly replaces the generic declaration in the inline function with the actual type after compilation.

Foo () calls bar(), an inline function with a generic type. After the code is compiled, the code in bar() will get the actual type of the generic. * /
fun foo(a){
    bar<String>()
}

inline fun <T> bar(a){
    // do something with T type
}

/** * Replace the code */
fun foo(a){
    // do something with T type
}
Copy the code

The conditions for generic realizations: First, the function must be an inline function. Second, you must add the reified keyword wherever you declare a generic to indicate that the generic is to be implemented.

fun main(a){
    val result1 = getGenericType<String>()
    val result2= getGenericType<Int>()
    println(result1)
    println(result2)
    
    // Print the result:
    // class java.lang.String
    // class java.lang.Integer
}

/** * Gets the ability to generalize the actual type, which is impossible in Java. * This function directly returns the actual type of the currently specified generic. Syntax such as t. CLSS is not legal in Java, whereas Kotlin uses generics to implement syntax such as T::class.java. * /
inline fun <reified T> getGenericType(a) = T::class.java
Copy the code

Application of generic realizations

/** * The second parameter to the Intent should be the Class type of a specific Activity, * but since T is already an implemented generic, it can be passed directly to T::class.java. * /
inline fun <reified T> startActivity(context: Context){
    val intent = Intent(context,T::class.java)
    context.startActivity(intent)
}

/** * Adds a function type parameter when the Activity is started, and its function type is defined in the Intent class */
inline fun <reified T> startActivity(context: Context,block:Intent. () - >Unit){
    val intent = Intent(context,T::class.java)
    intent.block()
    context.startActivity(intent)
}
Copy the code
    companion object{
        fun actionStart(context: Context){
            // Use generic realizations to start the Activity
            startActivity<ServiceActivity>(context)
          
            // Start the Activity with parameters
            startActivity<Main2Activity>(context){
                putExtra("param1",data1)
                putExtra("param2",data2)
            }
          
          // Other code like Service is basically similar.}}Copy the code

Covariant of generics

The covariant and contravariant functions of generics are less common and difficult to understand. But Kotlin’s built-in API uses a lot of covariant and contravariant features.

Start with a convention. A method in a generic class or generic interface whose argument list is the place where the data is received can be called the in position, and whose return value is the place where the data is output can be called the out position.

If a method accepts a Persion argument, it is legal to pass in an instance of Student, but if a method accepts a List argument, it is not allowed in Java to pass in an instance of List

. Because List cannot be a subclass of List, there may be a security risk of type conversion.

fun main(a){
    val student = Student("Tom".19)
    val data = SimpleData<Student>()
    data.set(student)
    handleSimpleData(data)  // In fact this line of code will report an error, assuming it will compile
    val studentData = data.get()}fun handleSimpleData(data: SimpleData<Person>){
    // The SimpleData
      
        is passed in, but the Teacher instance is created to replace the original data in the SimpleData
       
         parameter.
       
      
    // SimpleData
       
        
         
          
           
            
            
           
          
         
        
       
      
    // A cast exception must be generated. So, Java does not allow you to pass parameters in this way.
    // In other words, SimpleData
      
        is not a subclass of SimpleData
       
         even though Student is a subclass.
       
      
    val teacher = Teacher("Jack".35)
    data.set(teacher)
}

// Define three classes
open class Person(val name: String,val age:Int)
class Student(name:String,age: Int):Person(name,age)
class Teacher(name:String,age: Int):Person(name,age)

class SimpleData<T>{
    private var data:T? = null

    fun set(t:T?).{
        data = t
    }
    fun get(a):T? {return data}}Copy the code

Definition of generic covariant: If we define A generic class of MyClass

, where A is A subtype of B and MyClass
is A subtype of MyClass, then we can say that MyClass is agreed on T.

How to make MyClass A subtype of MyClass : IF A generic class is read-only on the data of its generic type, it has no conversion safety concerns. To do this, you need to make all methods in the MyClass

class unable to accept arguments of type T. In other words, T can only appear in the out position, not in position.

fun main(a){
    val student = Student("Tom".19)
    SimpleData
      
        is a subclass of SimpleData
       
         because the SimpleData class is covariant declared.
       
      
    val data = SimpleData<Student>(student)
    handleSimpleData(data)  
    val studentData = data.get()}fun handleSimpleData(data: SimpleData<Person>){
    // Get the data wrapped by SimpleData, even though the generic declaration is of type Person, it will actually get a Student instance,
    // However, since Persion is the parent of Student, upconversion is completely safe.
    val personData = data.get()}open class Person(val name: String,val age:Int)
class Student(name:String,age: Int):Person(name,age)
class Teacher(name:String,age: Int):Person(name,age)

The /** * out keyword indicates that T can only appear in the out position. It also means that SimpleData is covariant on the generic T. * Since T cannot be in, we cannot use set() to assign data, so we use the constructor instead. The constructor uses the val keyword, so T is still read-only. Even the var keyword is legal, as long as it is private to ensure that the generic T is not modifiable externally. * /
class SimpleData<out T>(val data:T?) {fun get(a):T? {return data}}Copy the code

Above, if a method receives a List type parameter, passing in an instance of List

is not allowed in Java, but is legal in Kotlin because covariant declarations are added to many of the built-in apis by default. This includes classes and interfaces for various collections. For example, if you want to add data to a List that is read-only, you need to use a MutableList.

/** * List is covariant on the generic E. Contaion (), however, still appears in the IN position. * This is because the purpose of contains() is very clear. It is only to determine if the current collection contains the element passed in as an argument. The contents of the current collection are not modified, so this operation is actually safe again. In order for the compiler to understand that this operation is safe, use@UnsafeVarianceAnnotation, so that the compiler will allow the generic E to appear in position. * /
public interface List<out E> : Collection<E> {
  
    override val size: Int
    override fun isEmpty(a): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(a): Iterator<E>
    public operator fun get(index: Int): E
}
Copy the code

The inverse of generics

Definition of contravariant: If we define A generic class MyClass

where A is A subtype of B and MyClass
is A subtype of MyClass, then we can say that MyClass is contravariant on T.

fun main(a){
    // write a Transformer
      
        anonymous class implementation,
      
    val trans = object :Transformer<Person>{
        Transform () converts the passed Person object to a string
        override fun transform(t: Person): String {
            return "${t.name} ${t.age}"}}// Because Transformer
      
        is not a subtype of Transformer
       
      
    handleTransformer(trans)  // This line of code will report an error
}

fun handleTransformer(trans:Transformer<Student>){
    val student = Student("Tom".19)
    val result = trans.transform(student)
}

/** * is used to perform some conversion operations */
interface Transformer<T>{
    // The argument T will be converted to a string, and the conversion logic is implemented by the subclass.
    fun transform(t:T):String
}


open class Person(val name: String,val age:Int)
class Student(name:String,age: Int):Person(name,age)
class Teacher(name:String,age: Int):Person(name,age)
Copy the code
/** * use invert */
fun main(a){
    val trans = object :Transformer<Person>{
        override fun transform(t: Person): String {
            return "${t.name} ${t.age}"}}At this point, Transformer
      
        is already a subtype of Transformer
       
      
    handleTransformer(trans)

    // If the generic T appears in the out position
    val trans1 = object :Transformer1<Person>{
        // Constructs a Teacher object and returns it directly.
        override fun transform(name: String,age: Int): Person {
            // The return value of transform() requires a Person object, and Teacher is a subclass of Person.
            return Teacher(name,age)
        }
    }
    handleTransformer1(trans1)
}

fun handleTransformer(trans:Transformer<Student>){
    val student = Student("Tom".19)
    val result = trans.transform(student)
    println(result)
    // Print the result:
    // Tom 19
}

fun handleTransformer1(trans:Transformer1<Student>){
    // Expecting a Student object, but actually getting a Teacher object causes a cast exception.
    val result = trans.transform("Tom".19)
    println(result)
    // Print the result
    // Exception in thread "main" java.lang.ClassCastException:
    // com.example.myapplication.test.Teacher cannot be cast to com.example.myapplication.test.Student
}

The in keyword indicates that T can only appear in the in position, and Transformer is contravariant on generic T
interface Transformer<in T>{
    fun transform(t:T):String
}

/** * The danger of making the generic T appear in the out position */
interface Transformer1<in T>{
    fun transform(name:String,age: Int):@UnsafeVariance T
}

open class Person(val name: String,val age:Int)
class Student(name:String,age: Int):Person(name,age)
class Teacher(name:String,age: Int):Person(name,age)
Copy the code

The use of Comparable, an interface to compare the size of two objects, is a good example of the use of Comparable in Kotlin’s built-in API.

/** * Comparable source code: Comparable
      
        is Comparable to two Person objects. If we use Comparable
       
         to compare the size of two Person objects, then it must be true to compare the size of two Student objects. It therefore makes sense to make Comparable
        
          a subclass of Comparable
         
          , which is a very typical application of contravariant. * /
         
        
       
      
public interface Comparable<in T> {
    // Implement concrete comparison logic
    public operator fun compareTo(other: T): Int
}
Copy the code

Comparable is a subclass of Person. Comparable

is a subclass of Person. This is impossible because Person is never a subclass of Student.


note

References:

Line 1 of code (Version 3)

The official documentation

Official Chinese translation site

Welcome to follow wechat official account:No reason also