阅读 1006

Kotlin教程(九)泛型

写在开头:本人打算开始写一个Kotlin系列的教程,一是使自己记忆和理解的更加深刻,二是可以分享给同样想学习Kotlin的同学。系列文章的知识点会以《Kotlin实战》这本书中顺序编写,在将书中知识点展示出来同时,我也会添加对应的Java代码用于对比学习和更好的理解。

Kotlin教程(一)基础
Kotlin教程(二)函数
Kotlin教程(三)类、对象和接口
Kotlin教程(四)可空性
Kotlin教程(五)类型
Kotlin教程(六)Lambda编程
Kotlin教程(七)运算符重载及其他约定
Kotlin教程(八)高阶函数
Kotlin教程(九)泛型


泛型类型参数

泛型允许你定义带类型形参的类型,当这种类型的实例被创建出来的时候,类型形参被替换成称为类型实参的具体类型。例如:

List<String>
Map<String, Person>
复制代码

和一般类型一样,Kotlin编译器也常常能推导出类型实参:

val authors = listOf("Dimtry", "Sevelana")
复制代码

如果你想创建一个空的列表,这样就没有任何可以推导出类型实参的线索,你就得显式地指定它(类型形参)。

val readers: MutableList<String> = mutableListOf()

val readers = mutableListOf<String>()
复制代码

和Java不同,Kotlin始终要求类型实参要么被显式地说明,要么能被编译器推导出来。因为泛型是1.5版本才引入到Java的,它必须保证和基于老版本的兼容,所以它允许使用没有类型参数的泛型类型——所谓的原生态类型。而Kotlin从一开始就有泛型,所以它不支持原生态类型,类型实参必须定义。

泛型函数和属性

如果要编写一个使用列表的函数,希望它可以在任何列表上使用,而不是某个具体类型的元素的列表,需要编写一个泛型函数。

fun <T> List<T>.slice(indices: IntReange): List<T>
复制代码

基本上是和Java的声明类似的,在方法名前声明,即可在函数中使用。

还可以给类或接口的方法,顶层函数,扩展属性以及扩展函数声明类型参数。例如下面你这个返回列表倒数第二个元素的扩展属性:

val <T> List<T>.penultimate: T
    get() = this[size -2]
复制代码

不能声明泛型非扩展属性

普通(非扩展)属性不能拥有类型参数,不能再一个类的属性中存储多个不同类型的值,因此声明泛型非扩展函数函数没有任何意义。

声明泛型类

和Java一样,Kotlin通过在类名称后面加上一对尖括号,并把类型参数放在尖括号内来声明泛型类及泛型接口。一旦声明之后,就可以在类的主体内像其他类型一样使用类型参数。

interface List<T> {
    operator fun get(index: Int): T
}
复制代码

如果你的类继承了泛型(或者实现了泛型接口),你就得为基础类型的泛型形参提供一个类型实参。

class StringList: List<String> {
    override fun get(index: Int): String = ...
}
复制代码

类型参数约束

类型参数约束可以限制作为(泛型)类和(泛型)函数的类型实参的类型。 如果你把一个类型指定为泛型类型形参的上界约束,在泛型类型具体的初始化中,其对应的类型实参就必须是这个具体类型或其子类型。你是这样定义约束:把冒号放在类型参数名称之后,作为类型形参上界的类型紧随其后:

fun <T : Number> List<T>.sum(): T
复制代码

相当于Java中的:

<T extends Number> T sum(List<T> list)
复制代码

一旦指定了类型形参T的上界,你就可以把类型T的值当做它的上界的值使用:

fun <T : Number> oneHalf(value: T): Double {
    return value.toDouble() //调用Number的方法
}
复制代码

极少数情况下,需要在一个类型参数上指定多个约束,这时你需要使用不同的语法:

fun <T> ensureTrailingPeriod(seq: T) 
    where T : CharSequence, T : Appendable {
    if(!seq.endWith('.') { //调用CharSequence的方法
        seq.append('.')//调用Appendable的方法
    }
}
复制代码

这种情况下,可以说明作为类型实参的类型必须同时实现CharSequence和Appendable两个接口。

让类型形参非空

如果你声明的时泛型类或者泛型函数,任何类型实参,包括哪些可空的类型实参,都可以替换她的类型形参。事实上没有指定上界的类型形参将会使用Any? 这个默认上界:

class Processor<T> {
    fun process(value: T) {
        value?.hashCode()
    }
}
复制代码

process函数中,参数value是可空的,尽管T并没有使用问号标记。

如果你想保证替换类型形参的始终是非空类型,可以通过制定一个约束来实现。如果你除了可空性之外没有任何限制,可以使用Any代替默认的Any?作为上界。

class Processor<T : Any> {
    fun process(value: T) {
        value.hashCode()
    }
}
复制代码

运行时的泛型:擦除和实化类型参数

你可能知道,JVM上的泛型一般是通过类型擦除实现的,就是说泛型类实例的类型实参在运行时是不保留的。

运行时的泛型:类型检查和转换

和Java一样,Kotlin的泛型在运行时也被擦除了。这意味着泛型类实例不会携带用于创建它的类型实参的信息。例如,如果你创建一个List<String>并将一堆字符串放到其中,在运行时你只能看到它是一个List,不能识别出列表本打算包含的时哪种类型的元素。
随着擦除类型信息也带来了约束。因为类型实参没有被存储下来,你不能检查他们。例如,你不能判断一个列表是一个包含字符串的列表还是包含其他对象的列表:

>>> if (value is List<String>)
ERROR: Canot check for instance of erased type
复制代码

那么如何检查一个值是否是列表,而不是set或者其他对象。可以使用特殊的*投影语法来做这样的检查:

if (value is List<*>)
复制代码

这种表示拥有未知类型实参的泛型类型,类似于Java中的List<?>

注意,在asas?转换中仍然可以使用一般的泛型类型。但是如果该类有正确的基础类型但类型实参是错误的,转换也不会失败,因为在运行时转换发生的时候类型实参是未知的。因此,这样的转换会导致编译器发出“unchecked cast”的警告。这仅仅是一个警告,你仍然可以继续使用这个值。

fun printSum(c: Collection<*>) {
    //这里会有警告:Unchecked cast:List<*> to List<Int>
    val intList = c as? List<Int> 
            ?: throw IllegalArgumentException("List is expected")
    println(intList.sum())
}

>>> printSum(listOf(1, 2, 3))
6
复制代码

编译一切正常:编译器只是发出了一个警告,这意味着代码是合法的。如果在一个整型的列表或者set上调用该函数,一切都会如预期发生:第一种情况会打印元素之和,第二种情况会抛出IllegalArgumentException异常。但如果你传递了一个错误类型的值,如List<String>,运行时会得到一个ClassCastException。

声明带实化类型参数的函数

前面说过,Kotlin泛型在运行时会被擦除,泛型函数的类型实参也是这样。在调用泛型函数的时候,在函数体中你不能决定调用它用的类型实参:

>>> fun <T> isA(value: Any) = value is T
Error: Cannot check for instance of erased type: T
复制代码

通常情况下都是这样的,只有一种例外可以避免这种限制:内联函数。内联函数的类型形参能够被实化,意味着你可以在运行时引用实际的类型实参。
在之前章节中,我们知道如果用inline关键字标记一个函数,编译器会把每一次函数调用都换成函数实际的代码实现。使用内联函数还可以提升性能,如果该函数使用了lambda实参:lambda的代码也会内联,所以不会创建任何匿名类。基于这种实现原理,应该也可以想象到,根据嵌入的上下文,泛型在class文件中已经被确定了。
如果把前面例子中的isA函数声明成inline并且用reified标记类型参数,你就能够用该函数检查value是不是T的实例了。

inline fun <reified T> isA(value: Any) = value is T

>>> println(isA<String>("abc"))
true
>>> println(isA<String>(123))
false
复制代码

一个实化类型参数能发挥作用的最简单的例子就是标准库函数filterIsInstance 。这个函数接收一个集合,选择其中哪些指定类的实例,然后返回这些被选中的实例。

>>> val items = listOf("one", 2, "three")
>>> println(items.filterIsInstance<String>())
[one, three]
复制代码

通过指定<String>作为函数的类型实参,你表明感兴趣的只是字符串。因此函数的返回类型是List<String>。这种情况下,类型实参在运行时是已知的,函数filterIsInstance使用它来检查列表中的值是不是指定为该类型实参的类的实例。
下面是Kotlin标准库函数filterIsInstance声明的简化版本:

inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
    val destination = mutableListOf<T>()
    for (element in this) {
        if (element is T) {
            destination.add(element)
        }
    }
    return destination
}
复制代码

在之前章节,我们提到把函数标记成内联只有在一种情况下有性能优势,即函数拥有函数类型的形参并且其对应的实参lambda和函数一起被内联的时候。而现在我们是为了能够使用实化参数而把函数标记成内联。

为什么实化只对内联函数有效

编译器把实现内联函数的字节码插入每一次调用发生的地方。每次你调用带实化类型参数的函数时,编译器都知道这次特定调用中用作类型实参的切确类型。因此,编译器可以生成引用作为类型实参的具体类的字节码。实际对filterIsInstance<String>掉用来说,生成的代码和下面这段代码是等价的:

for (element in this) {
    if (element is String) {
        destination.add(element)
    }
}
复制代码

因为生成的字节码引用了具体类,而不是类型参数,它不会被运行时发生的类型参数擦除影响。
注意,带reified类型参数的inline函数不能再Java代码中调用。 普通内联函数可以像常规函数那样在Java中调用——他们可以被调用而不能被内联。带实化参数类型的函数需要额外的处理,来把类型参数的值替换到字节码中,所以他们必须永远是内联的。这样他们不可能用Java那样的普通方式调用。

使用实化类型参数代替类引用

如果你是Android开发者,显示Activity是一个最常用的方法。也可以使用实化类型参数来代替传递作为java.lang.Class的Activity类:

inline fun <reified T : Activity> Context.startActivity() {
    val intent = Intent(this, T::class.java)
    startActivity(intent)
}

>>> startActivity
复制代码

::class.java的语法展现了如何获取java.lang.Class对应的Kotlin类。这和Java中的Service.class是完全等同的。

实化类型参数的限制

尽管实化类型参数是方便的工具,但它们也有一些限制。有一些事实化与生俱来的,而另外一些则是现有的实现决定的,而且可能在未来的Kotlin版本中放松这些限制。
具体来说,可以按下面的方式使用实化类型参数:

  • 用在类型检查和类型转换中(is!isasas?
  • 使用Kotlin反射API(::class
  • 获取相应的java.lang.Class::class.java
  • 作为调用其他函数的类型实参

不能做下面的这些事情:

  • 创建指定为类型参数的类的实例
  • 调用类型参数类的伴生对象的方法
  • 调用带实化类型参数函数的时候使用非实化类型形参作为类型实参
  • 把类、属性或者非内联函数的类型参数标记成reified

变型:泛型和子类型化

变型的概念描述了拥有相同基础类型和不同类型实参的(泛型)类型之间是如何关联的:例如,List<String>List<Any>之间如何关联。

为什么存在变型:给函数传递实参

假如你有一个接收List<Any>作为实参的函数。把List<String>类型的变量传给这个函数时候安全?毫无疑问,把一个字符串传给一个期望Any的函数是安全的,因为String继承了Any。但当String和Any变成List接口的类型实参之后,情况就没有这么简单了。

fun printContents(list: List<Any>) {
    println(list.joinToString())
}

>>> printContents(listOf("abc", "bac"))
abc, bac
复制代码

这看上去没什么问题,我们来看另一个例子:

fun addAnswer(list: MutableList<Any>) {
    list.add(42)
}

>>> val strings = mutableListOf("abc", "bac")
>>> addAnswer(strings)
Type mismatch. Required: MutableList<Any> Found: MutableList<String>
复制代码

这个例子和上面的例子中,区别仅仅是将List<Any>变成了MutableList<Any>,就无法将泛型为String的list传递给函数。
现在可以回答刚才那个问题了,把一个字符串列表传给期望Any对象列表的函数是否安全。如果函数添加或者替换了列表中的元素就是不安全的,因为这样会产生类型不一致的可能性。在Kotlin中,可以通过根据列表是否可变选择合适的接口来轻松的控制。如果函数接收的是只读列表,可以传递具有更具体的元素类型的列表。如果列表是可变的,就不能这么做。

类、类型和子类型

为了讨论类型之间的关系,需要熟悉子类型这个术语。任何时候如果需要的时类型A的值,你都能够使用类型B的值(当做A的值),类型B就称为类型A的子类型。举例来说,Int是Number的子类型,但Int不是String的子类型。这个定义还标明了任何类型都可以被认为是它自己的子类型。
术语超类型是子类型的反义词。如果A是B的子类型,那么B就是A的超类型。
为什么一个类型是否是另一个的子类型这么重要?编译器在每一次给变量赋值或者给函数传递实参的时候都要做这项检查。

fun test(i: Int) {
    val n: Number = i  //可以编译
    fun f(s: String) {/*...*/}
    f(i)  //不能编译
}
复制代码

只有值得类型是变量类型的子类型时,才允许变量存储该值。例如,变量n的初始化器i的类型Int是变量的类型Number的子类型,所以n的声明是合法的。只有当表达式的类型是函数参数的类型的子类型时,才允许把该表达式传给函数。这个例子中i的类型Int不是函数参数的类型String的子类型,所以函数f的调用会编译失败。
你可能认为子类型就是子类的概念,但是为什么在Kotlin中称之为子类型呢?因为,Kotlin存在可空类型。一个非空类型是它的可空版本的子类型,但它们都对应着同一个类。你始终能在可空类型的变量中存储非空类型的值,但反过来却不行。

var s: String = "abc"
val t: String? = s //编译通过
s = t  //编译不通过
复制代码

前面,我们把List<String>类型的变量传给期望List<Any>的函数是否安全,现在可以使用子类型化术语来重新组织:List<String>List<Any>的子类型吗?你已经了解了为什么把MutableList<String>当成MutableList<Any>的子类型对待是不安全的。显然,返回来也是不成立的:MutableList<Any>肯定不是MutableList<String>的子类型。
一个泛型类(例如MutableList)如果对于任意两种类型A和B,MutableList<A>既不是MutableList<B>的子类型也不是他的超类型,他就是被称为在该类型参数上是不变型的。Java中所有的类都是不变型的(尽管哪些类具体的使用可以标记成可变型的,稍后你就会看到)。

List类的类型化规则不一样,Kotlin中的List接口表示的是只读集合,如果A是B的子类型,那么List<A>就是List<B>的子类型。这样的类或者接口被称为协变的。

协变:保留子类型化关系

一个协变类是一个泛型类(我们以Producer<T>为例),对这种类来说,下面的描述是成立的:如果A是B的子类型,那么Producer<A>就是Producer<B>的子类型。我们说子类型化被保留了。
在Kotlin中,要声明类在某个类型参数上是可以协变的,在该类型参数的名称前面加上out关键字即可:

interface Producer<out T> {
    fun produce(): T
}
复制代码

将一个类的类型参数标记为协变得,在该类型实参没有精确匹配到函数中定义的类型形参时,可以让该类的值作为这些函数的实参传递,也可以作为这些函数的返回值。例如,想象一下有这样一个函数,它负责喂养用类Herd代表的一群动物,Herd类的类型参数确定了畜群中动物的类型。

open class Animal {
    fun feed() {...}
}

class Herd<T : Animal> {
    val size: Int
        get() = ...

    operator fun get(i: Int): T {...}
}

fun feeAll(animals: Herd<Animal>) {
    for (i in 0 until animals.size) {
        animals[i].feed()
    }
}
复制代码

假设这段代码的用户有一群猫需要照顾:

class Cat : Animal() {
    fun cleanLitter() {...}
}

fun takeCareOfCats(cats: Herd<Cat>) {
    for(i in 0 until cats.size) {
        cats[i].cleanLitter()
        // feedAll(cats)  //错误:类型不匹配
    }
}
复制代码

如果尝试把猫群传给feedAll函数,在编译期你就会得到类型不匹配的错误。因为Herd类中的类型参数T没有用任何变型修饰符,猫群不是畜群的子类。可以使用显示得类型转换来绕过这个问题,但是这种方法啰嗦、易出错,而且几乎从来不是解决类型不匹配问题的正确方式。
因为Herd类有一个类似List的API,并且不允许它的调用者添加和改变畜群中的动物,可以把它变成协变并相应地修改调用代码。

class Herd<out T: Animal> {
    ...
}
复制代码

你不能把任何类都变成协变得:这样不安全。让类在某个类型参数变为协变,限制了该类中对该类型参数使用的可能性。要保证类型安全,它只能用在所谓的out位置,意味着这个类只能生产类型T的值而不能消费它们。
在类成员的声明中类型参数的使用可以分为in位置和out位置。考虑这样一个类,它声明了一个类型参数T并包含了一个使用T的函数。如果函数是把T当成返回类型,我们说它在out位置。这种情况下,该函数生产类型为T的值。如果T用作函数参数的类型,它就在in位置,这样的函数消费类型为T的值。

interface Transformer<T> {
                //in位置 //out位置
    fun transform(t: T): T
}
复制代码

类的类型参数前的out关键字要求所有使用T的方法只能把T放在out位置而不能放在in位置。这个关键字约束了使用T的可能性,这保证了对应子类型关系的安全性。

重申一下,类型参数T上的关键字out有两层含义:

  • 子类型化被保留
  • T只能用在out位置

现在我们看看List<Interface>接口。Kotlin的List是只读的,所以它只有一个返回类型为T的元素的方法get,而没有定义任何把类型为T的元素存储到列表中的方法。因此,它也是协变的。

interface List<out T> : Collection<T> {
    operator fun get(index: Int): T
}
复制代码

注意,类型形参不光可以直接当作参数类型或者返回类型使用,还可以当作另一个类型的类型实参。例如,List接口就包含了一个返回List<T>的subList方法:

interface List<out T> : Collection<T> {
    fun subList(fromIndex: Int, toIndex: Int): List<T>
}
复制代码

在这个例子中,函数subList中的T也用在out位置。
注意,不能把MutableList<T>在它的类型参数上声明成协变的,因为它既含有接收类型为T的值作为参数的方法,也含有返回这种值得方法(因此,T出现在in和out两种位置上)。

interface MutableList<T>
        : List<T>, MultableCollection<T> {
    override fun add(element: T): Boolean   
}
复制代码

编译器强制实施了这种限制。如果这个类被声明成协变得,编译器会报错:Type parameter T is declared as 'out' but occurs in 'in' position(类型参数T声明为out但出现在in位置)。
注意,构造方法的参数即不在in位置,也不在out位置。即使类型参数声明成了out,仍然可以在构造方法参数的声明中使用它:

class Herd<out T: Animal>(vararg animals: T) {...}
复制代码

如果把类的实例当成一个更泛化的类型的实例使用,变型会防止该实例被误用:不能调用存在潜在危险的方法。构造方法不是那种在实例创建之后还能调用的方法,因此它不会有潜在危险。
然后,如果你在构造方法的参数上使用了关键字val和var,同时就会声明一个getter和一个setter(如果属性是可变的)。因此,对只读属性来说,类型参数用在了out位置,而可变属性在out位置和in位置都使用了它:

class Herd<T: Animal>(var leadAnimal: T, vararg animals: T) {...}
复制代码

上面这个例子中,T不能用out标记,因为类包含属性leadAnimal的setter,它在in位置用到了T。
还需要注意的是,位置规则只覆盖了类外部可见的(public、protected和internal)API。私有方法的参数即不在in位置也不在out位置。变型规则只会防止外部使用者对类的误用但不会对类自己的实现起作用:

class Herd<out T: Animal>(private var leadAnimal: T, vararg animals: T) {...}
复制代码

现在可以安全地让Herd在T上协变,因为属性leadAnimal变成了私有的。

逆变:反转子类型化关系

逆变的概念可以被看成是协变的镜像:对一个逆变来说,它的子类型化关系与用作类型实参的类的子类型化关系是相反的。我们从Comparator接口的例子开始,这个接口定义了一个方法compare类,用于比较两个给定的对象:

interface Comparator<in T> {
    fun compare(e1: T, e2: T): Int {...}
}
复制代码

一个为特定类型的值定义的比较器显然可以比较该类型任意子类型的值。例如,如果有一个Comparator<Any>,可以用它比较任意具体类型的值。

interface Comparator<in T> {
    fun compare(e1: T, e2: T): Int
}

fun main(args: Array<String>) {
    val anyComparator = Comparator<Any> { e1, e2 -> e1.hashCode() - e2.hashCode() }
    val strings = listOf("a", "b", "c")
    strings.sortedWith(anyComparator)
}
复制代码

sortedWith函数期望一个Comparator<String>(一个可以比较字符串的比较器),传给它一个能比较更一般的类型的比较器是安全的。如果你要在特定类型的对象上执行比较,可以使用能处理该类型或者它的超类型的比较器。这说明Comparator<Any>Comparator<String>的子类型,其中Any是String的超类型。不同类型之间的子类型关系和这些类型的比较器之间的子类型化关系截然相反。
现在你已经为完整的逆变定义做好了准备。一个在类型参数上逆变的类是这样的一个泛型类(我们以Consumer<T>为例),对这种类来说,下面的描述是成立的:如果B是A的子类,那么Consumer<A>就是Consumer<B>的子类型,类型参数A和B交换了位置,所以我们说子类型化被反转了。

in关键字的意思是,对应类型的值是传递进来给这个类的方法的,并且被这些方法消费。和协变得情况类似,约束类型参数的使用将导致特定的子类型化关系。在类型参数T上的in关键字意味着子类型化被反转了,而且T只能用在in位置。

协变得,逆变的和不变型的类

协变 逆变 不变型
Producer<out T> Consumer<in T> MutableList<T>
类的子类型化保留了:Producer<Cat>Producer<Animal>的子类型 子类型化反转了:Consumer<Animal>Consumer<Cat>的子类型 没有子类型化
T 只能在out位置 T只能在in位置 T可以在任何位置

一个类可以在一个类型参数上协变,同时在另外一个类型参数上逆变。Function接口就是一个经典的例子。下面是一个单个参数的Function的声明:

interface Function1<in P, out R> {
    operator fun invoke(p: P): R
}
复制代码

Kotlin的表达发(P) -> R是表达Function<P, R>的另一种更具可读性的形式。可以发现用in关键字标记的P(参数类型)只用在in位置,而用out关键字标记的R(返回类型)只用在out位置。这意味着对这个函数类型的第一个类型参数来说,子类型化反转了,而对于第二个类型参数来说,子类型化保留了。

fun enumerateCats(f: (Cat) -> Number) {...}
fun Animal.getIndex(): Int = ...
>>> enumerateCats(Animal::getIndex)
复制代码

在Kotlin中这点代码是合法的。Animal是Cat的超类型,而Int是Number的子类型。

使用点变型:在类型出现的地方指定变型

在类声明的时候就能够指定变型修饰符是很方便的,因为这些修饰符会应用到所有类被使用的地方。这被称作声明点变型。如果你熟悉Java的通配符类型(? extends 和 ? super),你会意识到Java用完全不同的方式处理变型。在Java中,每一次使用带类型参数的类型的时候,还可以指定这个类型参数是否可以用他的子类型或者超类型替换。这叫做使用点变型。

Kotlin的声明点变型 vs. Java通配符

声明点变形带来了更简洁的代码,因为只用指定一次变型修饰符,所有这个类的使用者都不用再考虑这些了,在Java中,库作者不得不一直使用通配符:Function<? super T, ? extends R>,来创建按照用户期望的运行的API。如果你查看Java 8标准库的源码,你会在每次用到Function接口的地方发现通配符。例如,下面是Stream.map方法的声明:

/* Java */
public interface Stream<T> {
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
}
复制代码

Kotlin也支持使用点变型,允许在类型参数出现的具体位置指定变型,即使在类型声明时它不能被声明成协变或逆变的。
你已经见过许多像MutableList这样的接口,通常情况下即不是协变也不是逆变的,因为它同时生产和消费指定为它们类型参数的类型的值。但对于这个类型的变量来说,在某个特定函数中只被当成其中一种角色使用的情况挺常见的:要么是生产者要么是消费者。例如下面这个简单的函数:

fun <T> copyData(source: MutableList<T>, 
        destination: MutableList<T>) {
    for (item in source) {
        destination.add(item)
    }
}
复制代码

这个函数从一个集合把元素拷贝到另一个集合中。尽管两个集合都拥有不变型的类型,来源集合只是用于读取,而目标集合只是用于写入。这种情况下,集合元素的类型不需要精确匹配。例如,把一个字符串的集合拷贝到可以包含任意对象的集合中一点儿问题也没有。
要让这个函数支持不同类型的列表,可以引入第二个泛型参数。

fun <T : R, R> copyData(source: MutableList<T>,
                destination: MutableList<R>) {
    for (item in source) {
        destination.add(item)
    }
}

>>> val ints = mutableListOf(1, 2, 3)
>>> val anyItems = mutableListOf<Any>()
>>> copyData(ints, anyItems)
>>> println(anyItems)
[1, 2, 3]
复制代码

你声明了两个泛型参数代表来源列表和目标列表中的元素类型。为了能够把一个列表中的元素拷贝到另一个列表中,来源元素类型应该是目标列表中的元素的子类型(Int是Any的子类型)。
但是Kotlin提供了一种更优雅的表达方式。当函数的实现调用了那些类型参数只出现在out位置(或只出现在in位置)的方法时,可以充分利用这一点,在函数定义中给特定用途的类型参数加上变型修饰符。

fun <T> copyData(source: MutableList<out T>,
                 destination: MutableList<T>) {
    for (item in source) {
        destination.add(item)
    }
}
复制代码

可以为类型声明中类型参数任意的用法指定变型修饰符,这些用法包括:形参类型、局部变量类型、函数返回类型,等等。这里发生的一切被称作类型投影:我们说source不是一个常规的MutableList,而是一个投影(受限)的MutableList。只能调用返回类型是泛型类型参数的那些方法,或者严格的讲,只在out位置使用它的方法。编译器禁止调用使用类型参数做实参的那些方法(在in位置使用类型参数):

>>> val list: MutableList<out Number> = ...
>>> list.add(42)
Error: Out-projected type 'MutableList<out Number>' prohibits the use of 'fun add (element: E): Boolean'
复制代码

不要为使用投影类型后不能调用某些方法而吃惊,如果需要调用那些方法,你要用的时常规类型而不是投影。这可能要求你声明第二个类型参数,它依赖的原本要进行投影的类型。

当然,实现copyData函数的正确方式应该是使用List<T>作为source实参的类型,因为我们只用了声明在List中的方法,并没有用到MutableList中的方法,而且List类型参数的变型在声明时就指定了。但这个例子对展示这个概念依然十分重要,尤其是要记住大多数的类并没有像List和MutableList这样分开的两个接口,一个是协变的读取接口,另一个是不变型的读取/写入接口。
如果类型参数已经有out变型,获取它的out投影没有任何意义。就像List<out T>这样。它和List<T>是一个意思,因为List已经声明成了class List<out T>。编译器会发出警告,标明这样的投影是多余的。

同理,可以对类型参数的用法使用in修饰符,来表明在这个特定的地方,相应的值担当的时消费者,而且类型参数可以使用它的任意子类型替换。

fun <T> copyData(source: MutableList<T>,
                 destination: MutableList<in T>) {
    for (item in source) {
        destination.add(item)
    }
}
复制代码

Kotlin的使用点变型直接对应Java的限界通配符。Kotlin中的MutableList<out T>和Java中的MutableList<? extends T>是一个意思。in投影的MutableList<in T>对应到Java的MutableList<? super T>

星号投影:使用*代替类型参数

本章前面提到类型检查和转换的时候,我们提到了一种特殊的星号投影语法,可以用它来标明你不知道关于泛型实参的任何信息。例如,一个包含未知类型的元素的列表用这种语法表示为List<*>。现在我们深入探讨星号投影的语义。

首先需要注意的是MutableList<*>MutableList<Any?>不一样。你确信MutableList<Any?>这种列表包含的时任意类型的元素。而另一方面,MutableList<*>是包含某种特定类型元素的列表,但是你不知道是哪个类型。这种列表被创建成一个包含某种特定类型元素的列表,比如String,而且创建它的代码期望只包含那种类型的元素。因为不知道是哪个类型,你不能像列表中写入任何东西,因为你写入的任何值都可能会违反调用代码的期望。但是从列表中读取元素是可行的,因为你心里有数,所有的存储在列表中的值都能匹配所有Kotlin类型的超类型Any?:

fun main(args: Array<String>) {
    val list: MutableList<Any?> = mutableListOf('a', 1, "qwe")
    val chars = mutableListOf('a', 'b', 'c')
    val unknownElements: MutableList<*> = if (Random().nextBoolean()) list else chars
//    unknownElements.add(42) //编译器禁止调用这个方法
    println(unknownElements.first()) //读取元素是安全的
}

//输出
a
复制代码

为什么编译器会把MutableList<*>当成out投影的类型?在这个例子的上下文中,MutableList<*>投影成了MutableList<out Any?>,当你没有任何元素类型信息的时候,读取Any?类型的元素任然是安全的,但是向列表中写入元素是不安全的。
Kotlin的MyType<*>相当于Java中的MyType<?>

对像Consumer<in T>这样的逆变类型的参数来说,星号投影等价于<in Nothing>。实际上,在这种星号投影中无法调用任何签名中有T的方法。如果类型参数是逆变的,它就只能表现为一个消费者,而且,我们之前讨论过,你不知道它可以消费的到底是什么。因此,不能让它消费任何东西。

当类型实参的信息并不重要的时候,可以使用星号投影的语法,不需要使用任何在签名中引用类型参数的方法,或者只是读取数据额不关心它的具体类型。例如,可以实现一个接收List<*>做参数的printFirst函数:

fun printFirst(list: List<*>) {
    if (list.isNotEmpty()) {
        println(list.first())
    }
}
复制代码
关注下面的标签,发现更多相似文章
评论