kotlin修炼指南7之泛型
发布时间:2023-03-23 13:41:46 所属栏目:教程 来源:
导读:Kotlin在Java的基础上,同样对泛型语法进行了拓展,所以很多Kotlin开发者,看着源码中的一堆in、out和*,感觉非常不知所措。其实,只要了解了Java泛型,那么Kotlin泛型就迎刃而解了。
首先,我们来想想,我们为什
首先,我们来想想,我们为什
Kotlin在Java的基础上,同样对泛型语法进行了拓展,所以很多Kotlin开发者,看着源码中的一堆in、out和*,感觉非常不知所措。其实,只要了解了Java泛型,那么Kotlin泛型就迎刃而解了。 首先,我们来想想,我们为什么需要泛型。 泛型是面向对象编程的一个非常重要的方面,它的出现,是多态的核心实现,简单的说,就是可以在不同的对象类型之间,使用相同的代码逻辑,从而实现复用。 为了充分了解泛型,以及泛型的实例场景,我们下面来构建一个面向对象的例子。 abstract class Person(open val name: String) { abstract fun talk(): String } class Parent(override val name: String) : Person(name) { override fun talk(): String = name } class Son : Person("ryan") { override fun talk(): String = "hahaha" } fun doTalk(family: MutableList<Person>) { family.forEach { println(it.talk()) } } 这里定义了一个Person类,作为基类,他的子类——Father、Son,就是具体的实例,新建一个方法doTalk,用来输出具体的实现。 fun main() { val family = arraylistof<Person>() family.add(Parent("Father")) family.add(Son()) doTalk(family) } 这样,我们就可以构建一个family的List,指定List的类型为Person,这样Father、Son,这些子类就都可以加入到这个List。 泛型不变性 Father和Son都可以作为子类,加入到Person的List中,这就是泛型,但是让我们再看下下面的代码。 val parents = mutablelistof<Parent>() parents.add(Parent("Father")) parents.add(Parent("Mother")) doTalk(parents) 创建一个类型为Parent的List,再传入doTalk函数,这时候,编译器报错了。 为什么呢?从编译器来看,doTalk需要的是一个List类型的参数,但是传入的是List类型,确实类型不一致,但是,Parent是Person的子类,从语义上来说,doTalk函数也是可以接受Parent类型的List的。 这就是泛型的不变性。即使参数中的类型是父子关系,但是编译器依然不能识别,它只能识别具体的类型。 泛型的型变 正是由于存在泛型的不变性,所以我们在支持某些场景的泛型参数时,就需要通过「泛型的型变」来拓展「泛型的不变性」。 Kotlin,或者说Java的泛型,实际上是一种伪泛型,即泛型只在申明时检查泛型是否有效,在编译时,泛型类型会被擦除,这是因为Java的历史原因所导致的,由于它为了兼容没有泛型的老Java版本,从而做出的妥协。 不管是如何型变,它们的作用都是扩大泛型参数的类型范围。 协变 泛型的协变,是泛型型变的一种方式。 协变的使用很简单,我们给参数加上out前缀即可,代码如下。 fun doTalk(family: MutableList<out Person>) { family.forEach { println(it.talk()) } } 加上out关键字之后,参数类型就变成了「Person类及其子类」,也就是说,只要是Person的子类,都可以作为参数传进来。 那么这样处理之后,上面的方法就可以执行了。但是,协变之后的泛型,就变成可读而不可写类型了。 例如我们在协变泛型参数上进行写操作,代码如下。 fun doTalk(family: MutableList<out Person>) { family.add(Son()) // Error family.forEach { println(it.talk()) } } 这样就会报错,因为被out修饰之后,参数失去了写属性,变为只读属性了,这就是协变的副作用。 那么原因是什么呢? 我们来思考下,为什么它是可读的,通过out修饰之后,我们能保证,加入List的数据都是Person的子类,所以,List读取出来的实例类型,不管是哪个子类,都可以转为Person,也就是基类,所以可以通过它来调用基类的函数。 如果把参数写成Java的方式,可能更好理解一些。 void doTalk(List<? extends Person> family) {} 可以发现,泛型的协变,实际上是控制了类型的上限,但返回的具体类型,是不确定的(?代表未知类型),这就是为什么在协变后的参数中,无法执行写指令的原因,因为参数的类型,可能是List,也可能是List,所以无法确定是哪一种类型,自然无法写入。 逆变 逆变是泛型型变的第二种方式,与协变类似,逆变也是将某一个泛型类型,拓展了其父类类型,例如下面这个方法。 fun work(worker: MutableList<Son>) { worker.forEach { println(it.talk()) } } 这个方法接收一个List类型的参数,那么假如我们要传递一个List类型的参数,就会报错,原因跟协变是一样的。 这个时候,就需要使用逆变关键字in,将参数类型拓展为「Son类及其父类」。 fun work(family: MutableList<in Son>) { family.forEach { println(it.talk()) } } val family = arraylistof<Person>() family.add(Parent("Father")) family.add(Son()) work(family) 这样参数就可以传进去了,但是,逆变的副作用,是会导致泛型参数失去读属性,而只能使用写属性。 fun work(family: MutableList<in Son>) { family.add(Son()) family.forEach { println(it.talk()) } // Error } 同样的,我们将它转化为Java中的代码,这样更好理解一些。 void work(List<? super Son> family) {} 泛型的逆变,实际上是控制了类型的下限,即Son及其父类。对List进行add操作时,新实例son一定符合条件,但是get时,只会获取到Any或者Object类型,所以,拿到Object类型后,你可以根据业务来进行强转。 星型投影 星型投影,其实就是Java中的「?」通配符,用于在泛型的使用中,去除泛型的依赖,这么说有点抽象,简单的说,就是当你不关心具体的泛型类型时,就可以使用「?」或者「*」来忽略泛型的约束。下面举个例子。 class Push<T> { fun pushMsg(msg: String): T {} } fun <T> getPush(): Push<T> {} 这是泛型版本的方法,我们可以获取指定泛型的Push,同时,你也可以用out来做泛型协变,让它可以返回子类。 那么这个时候,如果我不关心泛型的类型呢? fun getPush(): Push<*> {} fun main() { val push = getPush() val pushMsg = push.pushMsg("xys") } 通过「*」,我们就可以不用指定泛型的具体类型,因为我不关心泛型类型,不过要注意的是,星型投影之后返回的类型,就成了「Any?」或者「Object」,因为泛型类型已经没有了。 但是我们依然可以使用协变来限制投影的上限,当我们加上上限后,就可以限制返回数据的上限类型了——out T : CommonPush 实际使用 我们在设计泛型API时,通常会有两种使用方式,一种是将泛型作为参数,另一种是将泛型作为返回值,这两种模式,实际上就对应「生产者-消费者」模型。下面我们就借助这个模型,来完整的演示下。 官方文档中的说法是——Consumer in,Producer out ! 生产者 首先,设计一个生产者。 class Producer<T> { fun produceSth(): T { // Todo } } fun main() { val producer: Producer<Person> = Producer() val sth: Person = producer.produceSth() } 这是我们泛型最基本的使用,创建Person类型的生产者,它生产出来的东西,全是Person类型。 下面,我们来对泛型协变,这样就可以创建Son类型的生产者。 fun main() { val producer: Producer<out Person> = Producer<Son>() val sth: Person = producer.produceSth() } 但是协变之后,生产出来的类型,依然是Person类型。 那么在Kotlin中,可以将这种在使用时的协变,变为申明时的协变,代码如下。 class Producer<out T> { fun produceSth(): T { // Todo } } fun main() { val producer = Producer<Son>() val sth: Person = producer.produceSth() } 在申明时标记协变,这样后续在使用时,就不用再标记了,你可以创建子类的生产者,生产基类的对象。 在Kotlin中,集合类大量使用了协变,如下所示。 public interface List<out E> : Collection<E> { // Query Operations override val size: Int override fun isEmpty(): Boolean override fun contains(element: @UnsafeVariance E): Boolean override fun iterator(): Iterator<E> // Bulk Operations override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean // Positional Access Operations /** * Returns the element at the specified index in the list. */ public operator fun get(index: Int): E 这里泛型就是作为返回值传入。 消费者 同样的,我们创建一个消费者。 class Consumer<T> { fun consumeSth(t: T) { // Todo } } fun main() { val consumer: Consumer<Son> = Consumer<Person>() consumer.consumeSth(Son()) } 同理,创建一个Person类型的消费者,它只能消费Son类型的参数。 我们再给它增加逆变,让它可以接受Son的基类。 fun main() { val consumer: Consumer<in Son> = Consumer<Person>() consumer.consumeSth(Son()) } 但是逆变之后,同样只能接受Son类型的参数,但是可以创建Person类型的消费者。 类似的,逆变也可以在申明处标记。 fun main() { val consumer = Consumer<Person>() consumer.consumeSth(Son()) } class Consumer<in T> { fun consumeSth(t: T) { // Todo } } 那么逆变,在实际代码中的例子,我们可以参考下Comparable接口的设计。 public interface Comparable<in T> { public operator fun compareto(other: T): Int } 这里的泛型就是作为参数传递,所以使用了逆变。 泛型的实例化 由于Java会在编译期进行泛型擦除,所以我们无法对泛型来做类型判断,比如下面的代码。 fun <T> test(param: Int) { if (param is T) {// Error } } T是无法进行类型判断的,因为它已经被擦除了,这和在Java中使用instanceof判断是一样的,在Java中,我们通常会再传入一个Class类型的参数来处理这个问题。而在Kotlin中,有更简单的方法来处理,那就是通过inline配合reified关键字来处理。 inline fun <reified T> test(param: Int) { if (param is T) { } } 这样T就可以当做正常的类型来处理了,不过这种实例化的方式是有限制的。 函数必须是内联函数,因为只有内联函数才会在编译时进行替换 加上reified关键字让编译器在该泛型使用时进行实例化 在实战中,我们就可以利用泛型来进一步简化代码,例如: inline fun <reified T> startActivity(context: Context) { context.startActivity(Intent(context, T::class.java)) } (编辑:汽车网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
推荐文章
站长推荐