Kotlin generic <in, out, where> concepts and examples

Kotlin generic <in, out, where> concepts and examples

In Kotlin, generics are used to specify the type of objects that a class, interface or method can operate on.
insert image description here
constant

in

inThe keyword is used to specify that the generic type is an "input" type, meaning that it will only be used as an argument to a function or class.

interface ReadOnly {
    
    
    fun read(): Any
}

class ReadWrite<in T>(private var value: T) : ReadOnly {
    
    
    override fun read(): Any = value

    // 'in' keyword allows to use T as an input only
    // so, the following line will give a compile error
    // fun write(value: T) { this.value = value }
}

Another example:

interface Consumer<in T> {
    
    
    fun consume(item: T)
}

class StringConsumer : Consumer<String> {
    
    
    override fun consume(item: String) {
    
    
        println("Consuming string: $item")
    }
}

class AnyConsumer : Consumer<Any> {
    
    
    override fun consume(item: Any) {
    
    
        println("Consuming any type: $item")
    }
}

fun main() {
    
    
    val stringConsumer = StringConsumer()
    stringConsumer.consume("Hello") // prints "Consuming string: Hello"

    val anyConsumer: Consumer<Any> = AnyConsumer()
    anyConsumer.consume("Hello") // prints "Consuming any type: Hello"
    anyConsumer.consume(123) // prints "Consuming any type: 123"
}

In the above example, Consumerit is an interface with only one method consumeaccepting a parameter of type T. The type parameter T is indeclared with a keyword indicating that it is only to be used as an input type. StringConsumerand AnyConsumerare two classes that implement Consumerthe interface, both of which can be used to consume instances of their respective types.

out

covariant

outThe keyword is used to specify that the generic type is an "output" type, meaning that it will only be used as the return type of a function or class.

interface WriteOnly {
    
    
    fun write(value: Any)
}

class ReadWrite<out T>(private var value: T) : WriteOnly {
    
    
    // 'out' keyword allows to use T as an output only
    // so, the following line will give a compile error
    // fun read(): T = value

    override fun write(value: Any) {
    
    
        // this.value = value  
    }
}

another example:

interface Producer<out T> {
    
    
    fun produce(): T
}

class StringProducer : Producer<String> {
    
    
    override fun produce(): String = "Hello"
}

class AnyProducer : Producer<Any> {
    
    
    override fun produce(): Any = "Hello"
}

fun main() {
    
    
    val stringProducer = StringProducer()
    println(stringProducer.produce()) // prints "Hello"

    val anyProducer: Producer<Any> = AnyProducer()
    println(anyProducer.produce()) // prints "Hello"
}

In the above example, Produceris an interface which has a single method producewhich returns a Tvalue of a type. A type parameter is declared Twith outa keyword indicating that it is only to be used as an output type. StringProducerand AnyProducerare two classes that implement Producerthe interface, both of which can be used to generate instances of their respective types.

where

whereKeywords are used to specify constraints on types that can be used as parameters or return types.

interface Processor<T> where T : CharSequence, T : Comparable<T> {
    
    
    fun process(value: T): Int
}

class StringProcessor : Processor<String> {
    
    
    override fun process(value: String): Int = value.length
}

Another example:

interface Processor<T> where T : CharSequence, T : Comparable<T> {
    
    
    fun process(value: T): Int
}

class StringProcessor : Processor<String> {
    
    
    override fun process(value: String): Int = value.length
}

fun main() {
    
    
    val stringProcessor = StringProcessor()
    println(stringProcessor.process("Hello")) // prints "5"
}

In the above example, Processoris an interface with just one method that processtakes Ta parameter of a type and returns one Int. A type parameter is declared Tusing wherethe keyword and specifies two constraints: T must implement CharSequencethe interface, and it must be comparable to itself. StringProcessorIs a class that implements a Stringtype's Processorinterface that can be used to manipulate Stringvalues.

Consider the following class:

class Box<T>(val item: T)

This class defines a generic type T that can be used to specify the type of items stored in the Box. This class can be used to create any type of Box without any additional constraints:

val intBox = Box(1)
val stringBox = Box("hello")

Now consider the following class:

class InOut<in T, out R>(val item: T) {
    
    
    fun get(): R {
    
    
        return item as R
    }
}

Here, Tis defined as an "input" type (using inthe keyword), Rand is defined as an "output" type (using outthe keyword). This means that it Tcan only be used as an argument to a function, and Ronly as a return type. This will allow us to define a InOutfunction that takes a type and returns the inner item:

fun test(input: InOut<String, Any>): Any {
    
    
    return input.get()
}

Finally consider the following class:

class MyClass<T> where T : Number, T : Comparable<T> {
    
    
    fun compare(item1: T, item2: T): Int {
    
    
        return item1.compareTo(item2)
    }
}

Here, Tis defined as a generic type, limited to types that are both Numberand Comparable<T>. This means that comparison functions can only be called with arguments of type Numberand .Comparable<T>

val myClass = MyClass<Int>()
val result = myClass.compare(1,2)

In this example, we can see that the class only accepts type Int since it is one Numberand is comparable.

By using in and out, Kotlin provides support for declaration-point variants, which allows us to define subtyping relationships for generic types at the point of declaration, rather than at the point of use. This allows us to use more generic types more safely and concisely, and prevents some possible type errors.

in conclusion

covariant relationship

Here are some things you can do with in and out in Kotlin that would be difficult or impossible without them:

  1. Define covariant and contravariant generic types: out allows us to define covariant generic types, which means that subtype relationships are preserved (eg, subtypes of yes) List<Child>. List<Parent>On the other hand, in allows us to define contravariant generic types, which means that the direction of the subtyping relationship is reversed (for example, Comparator<Parent>is Comparator<Child>a subtype of ).
  2. Using generic types in function parameters and return types: Using in and out allows us to use generic types in function parameters and return types to preserve subtyping relationships. For example, we can define a List<out Parent>function that takes as an argument, meaning it accepts List<Child>an or List<Parent>, but not List<Grandparent>. Similarly, we can define a Comparator<in Child>function that returns , which means it can return Comparator<Child>or Comparator<Parent>, but not Comparator<Grandparent>.
  3. Avoid casts and type checks: Using in and out can avoid casts and type checks in some cases because the compiler can infer subtyping relationships between different generic types. For example, if we have a List<out Any>, we can safely access the elements of the list as Any, since we know that all elements of the list are at least of type Any.

In summary, in and out are powerful tools in Kotlin, without them we would have to use casting, type checking and other workarounds to achieve the same level of expressiveness and safety.

reference

[Kotlin Generics] http://www.enmalvi.com/2021/01/31/kotlin-28/

Guess you like

Origin blog.csdn.net/u011897062/article/details/130832411