一文了解 kotlin 中的泛型

1,143 阅读4分钟

Kotlin 泛型作用和其他语言一样,作用都是在不同类型之间复用相似的逻辑代码。不过 Kotlin 泛型还是有些特别的概念,比如协变和逆变。这篇文章就介绍 kotlin 中的泛型。

泛型的使用

我们以图书为例,先定义三个类,分别为 BookComputeBookAndroidBook,代码如下:

// 图书
open class Book {  
  ...
}  
// 计算机相关的图书
open class ComputeBook: Book() {  
  ...
}  
// Android相关的图书  
class AndroidBook: ComputeBook() {  
  ...
}

对于不同的图书,分别由不同的出版社来处理。由于出版社的出版流程是相似的,这时候就可以使用泛型来共有逻辑代码。代码如下所示:

// 通过 : 来声明泛型的上界
class PublishingHouse<T: Book> {
    ...
}
// 计算机图书出版社
val computePublishingHouse = PublishingHouse<ComputeBook>()
// android 图书出版社
val androidPublishingHouse = PublishingHouse<AndroidBook>()

其中我们可以使用 T: Book 来设置泛型的上界,即泛型只能是 Book 以及子类。如果你想要限制多个上界,需要使用关键字 where。代码示例如下:

// 如果需要限制多个上界,需要使用关键字 where
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String> where T : CharSequence, T : Comparable<T> {
    return list.filter { it > threshold }.map { it.toString() }
}

协变和逆变

在介绍泛型的协变和逆变之前,我们需要了解泛型的不变性。泛型的不变性是指,PublishingHouse<Book>PublishingHouse<ComputeBook>PublishingHouse<AndroidBook> 这三者之间没有任何关系,即使这三个类之间存在继承关系。假设有一个程序员类,它需要通过出版社购买计算机相关的图书,代码示例如下:

class Programmer {
    // 计算机图书出版社购买图书
    fun buyBook(publishingHouse: PublishingHouse<ComputeBook>) {  
        ...
    }  
}
// AndroidBook 为 ComputeBook 子类。
// 这时 buyBook 传入 PublishingHouse<AndroidBook> 会报错,
val computePublishingHouse = PublishingHouse<AndroidBook>()
buyBook(computePublishingHouse)
// Book 为 ComputeBook 父类,也会报错
val publishingHouse = PublishingHouse<Book>()
buyBook(publishingHouse)

可以看到,无论泛型之间是子类还是父类,kotlin 都认为认为 PublishingHouse<Book>PublishingHouse<ComputeBook>PublishingHouse<AndroidBook> 这三者之间没有任何关系,因此提示报错。默认情况下,你就只能传入 PublishingHouse<ComputeBook> 对象。

父类 Book 的情况先不讨论,按照我们一般的理解,子类 AndroidBook 应该可以传入才对啊。如果你想要正常传入,解决方案其实很简单,就是在 buyBook 或者 PublishingHouse 的定义处使用 out。代码示例如下:

// 在 buyBook 使用 out 关键字,这种方式叫做 使用处协变
class Programmer {   
    fun buyBook(publishingHouse: PublishingHouse<out ComputeBook>) {  
        ...
    }  
}
// 在 PublishingHouse 的定义处使用 out 关键字,这种方式叫做 声明处协变
class PublishingHouse<out T: Book> {
    ...
}

但是需要注意的是,使用 out 关键字其实是有代价的,你只能传出值,而不能传入值。这里以声明处协变为例,代码示例如下:

class PublishingHouse<out T: Book> {
    // 不能传入值,会报错
    fun addBook(book: T) {
        ...
    }
    // 可以传出值
    fun sellBook(): T {
        ...
    }
}

为什么 kotlin 编译器要搞这种限制呢?我们先假设编译器不报错,addBook 可以正常使用,这时如果我们在 Programmer 类中调用 addBook 方法就可能会出现错误传值的情况。代码如下所示:

class Programmer {
    // 传入的是 PublishingHouse<AndroidBook> 对象
    fun buyBook(publishingHouse: PublishingHouse<out ComputeBook>) {  
        publishingHouse.add(Book()) // 错误传值,应该传入 ComputeBook
    }  
}

说完了协变,再来看看逆变 。假设程序员打算出书,需要联系出版社,有专门的计算机图书出版社,还有大型的图书出版社,什么书都出版。这时的代码示例如下:

class Programmer {

    ...
    // 打算出版一本计算机相关的图书
    fun contact(PublishingHouse: PublishingHouse<ComputeBook>) {
        ...
    }
}

val computePublishingHouse = PublishingHouse<AndroidBook>()
val programmer = Programmer() 
// 联系计算机出版社
programmer.contact(computePublishingHouse)
// 联系大型的图书出版社,什么书都出版
val bigPublishingHouse = PublishingHouse<Book>()
// 这里会报错
programmer.contact(bigPublishingHouse)

如果我们想要父类 Book 的图书出版社正常的传入,这时就需要 in 关键字。和 out 一样,它可以分别在使用处或者声明处使用,代码示例如下:

// 在 contact 使用 in 关键字,这种方式叫做 使用处逆变
class Programmer {   
    fun contact(PublishingHouse: PublishingHouse<in ComputeBook>) {  
        ...
    }  
}
// 在 PublishingHouse 的定义处使用 in 关键字,这种方式叫做 声明处逆变
class PublishingHouse<in T: Book> {
    ...
}

out 关键字正好相反,使用 in 关键字后,你只能传入值,而不能传出值。代码示例如下:

class PublishingHouse<out T: Book> {
    // 可以传入值
    fun addBook(book: T) {
        ...
    }
    // 不可以传出值,会报错
    fun sellBook(): T {
        ...
    }
}

之所以 Kotlin 编译器要限制传出值,这是因为如果不限制传出值,那么可能会获取的是错误数据类型,代码示例如下:

class Programmer {
    // 传入的是 PublishingHouse<Book> 对象
    fun contact(publishingHouse: PublishingHouse<in ComputeBook>) {  
        ...
        val book = publishingHouse.sellBook() // 返回的是 Book 对象,但要求的是 ComputeBook
    }  
}

如果代码中可以确保不会出现上面的问题,我们可以使用 @UnsafeVariance 注解,这样编译器就会忽略这些问题。

星投影

星投影是指在代码中用 * 作为泛型的实参,表示我们不关心传递的泛型实参到底是什么。代码示例如下:

class Programmer {
    // 当 PublishingHouse 泛型为 <out T: Book> 等同于 PublishingHouse<out Book>
    // 当 PublishingHouse 泛型为 <in T: Book> 等同于 PublishingHouse<in Nothing>
    // 当 PublishingHouse 泛型为 <T: Book> ,读取时等同于 PublishingHouse<out Book>;
    // 写入时等同于 PublishingHouse<in Nothing>
    fun buyBook(publishingHouse: PublishingHouse<*>)
}

参考