Kotlin学习之泛型

作者: 程序员丶星霖 | 来源:发表于2017-12-05 08:43 被阅读134次

    Kotlin学习之泛型

    Kotlin的泛型与Java一样,都是一种语法糖,只在源代码里出现,编译时会进行简单的字符替换。

    泛型其实就是类型参数,它给强类型编程语言加入了更强的灵活性。

    在Java中,只要是有类型的元素,都可以泛型化。泛型类、泛型接口、泛型方法和泛型属性。泛型类和泛型接口统称为泛型类型。最重要的是泛型类型和泛型方法。

    在Kotlin,类也可以有类型参数:

    class Box<T>(t: T) {
        var value = t
    }
    

    一般来说,要创建如上这样类的实例,我们需要提供类型参数:

    val box: Box<Int> = Box<Int>(1)
    

    如果类型参数是可以推断出来的,也可以将其省略掉:

    val box = Box(1)
    
    • 定义泛型类型,是在类型名之后、主构造函数之前用尖括号(<>)括起的大写字母类型参数指定。
    • 定义泛型类型变量,可以完整的写明类型参数;如果编译器可以自动推定类型参数,也可以省略类型参数。

    在泛型方法的类型参数里可以用冒号指定上界。

    fun <T : Comparable<T>> sort(list: List<T>) {/*...*/}
    

    对于多个上界约束条件,可以用where子句:

    fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>
        where T : Comparable, Cloneable {
          return list.filter(it > threshold).map(it.clone())
        }
    

    一、型变

    在Kotlin中,没有通配符类型。但是它有:声明处型变和类型投影。

    在Java中的泛型是不型变的。通配符类型参数 ? extends E表示此方法接受E或者E的一些子类型对象的集合,而不只是E自身。也就是说可以安全地从中读取E,但不能写入,因为我们不知道什么对象符合未知的E的子类型。带extends限定的通配符类型使得类型是协变的。

    只能从中读取的对象为生产者,只能写入的对象为消费者。通配符保证的唯一的事情就是类型安全。

    1.1声明处型变

    如果有一个泛型接口Source<T>,这个接口中不存在任何以T为参数的方法,只是方法返回T类性值:

    // Java
    interface Source<T> {
      T nextT();
    }
    
    void demo(Source<String> str) {
      // Java 中这种写法是不允许的
      Source<Object> obj = str;
      /*...*/
    }
    

    因为Java中泛型是不型变的,Source<String> 不是Source<Object>的子类型,所以不能把Source<String>类型变量赋值给Source<Object>类型变量。

    在Kotlin中,有一种方法向编译器解释这种情况。称为声明处型变:可以标注Source的类型参数T来确保它仅从Source<T>成员中返回,并从不被消费。Kotlin提供了out修饰符:

    abstract class Source<out T> {
        abstract fun nextT(): T
    }
    
    fun demo(strs: Source<String>) {
        val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数
        // ……
    }
    
    • 当一个类C的类型参数T被声明为out时,就只能出现在C的成员的输出位置,但是C<Base>可以安全地作为C<Derived>的超类。

    out修饰符称为型变注解,并且由于它在类型参数声明处提供,所以叫声明处型变

    除了out,Kotlin又补充了一个型变注释:in。它使得一个类型参数逆变:只可以被消费而不可以被生产。逆变类的一个很好的例子是Comparable:

    abstract class Comparable<in T> {
        abstract fun compareTo(other: T): Int
    }
    
    fun demo(x: Comparable<Number>) {
        x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
        // 因此,我们可以将 x 赋给类型为 Comparable <Double> 的变量
        val y: Comparable<Double> = x // OK!
    }
    

    1.2类型投影

    使用处型变:类型投影
    将类型参数T声明为out很方便,而且可以避免使用处子类型化的麻烦,但是有些类实际上不能限制为只返回T 。例如Array:

    class Array<T>(val size: Int) {
        fun get(index: Int): T { ///* …… */ }
        fun set(index: Int, value: T) { ///* …… */ }
    }
    

    这个类在T上既不可以是型变,也不可以是逆变。从而造成了一些不灵活性。

    fun copy(from: Array<Any>, to: Array<Any>) {
        assert(from.size == to.size)
        for (i in from.indices)
            to[i] = from[i]
    }
    

    要确保的是copy()不会做任何坏事。所以如下:

    fun copy(from: Array<out Any>, to: Array<Any>) {
     // ……
    }
    

    这就发生了类型投影。这就是使用处型变的用法,并且是对应Java中的Array<? extends Object>,但是使用更简单。

    也可以使用in投影一个类型:

    fun fill(dest: Array<in String>, value: String) {
        // ……
    }
    
    1.2.1星投影

    Kotlin为此提供了所谓的星投影语法:

    • 对于Foo<out T>,其中T是一个具有上界TUpper的协变类型参数,Foo<*>等价于Foo<out TUpper>。意味着当T未知时,可以安全地从Foo<*>读取TUpper的值。
    • 对于Foo<in T>,其中T是一个逆变类型参数,Foo<*>等价于Foo<in Nothing>。意味着当T未知时,没有什么可以以安全的方式写入Foo<*>。
    • 对于Foo<T>,其中T是一个具有上界TUpper的不型变类型参数,Foo<*>对于读取值时等价于Foo<out TUpper>而对于写值时等价于Foo<in Nothing>

    如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。 例如,如果类型被声明为 interface Function <in T, out U>,我们可以想象以下星投影:

    • Function<*, String> 表示 Function<in Nothing, String>;
    • Function<Int, *> 表示 Function<Int, out Any?>;
    • Function<*, *> 表示 Function<in Nothing, out Any?>。

    学海无涯苦作舟

    我的微信公众号.jpg

    相关文章

      网友评论

        本文标题:Kotlin学习之泛型

        本文链接:https://www.haomeiwen.com/subject/filqixtx.html