Kotlin中的out和in

说到Kotlin中的out和in关键字,可能有些人都是what表情?最开始的我也见到这两个关键字的时候也是一脸懵逼,其实如果熟悉了Java的泛型之后,这两个关键字就秒懂了,其实无非还是PECS那一套,无非在kotlin中换了个名字而已,out对应Java的? extend,in对应kotlin的? super。本篇文章我结合kotlin看看这个两个关键字的具体用法(本文是看郭霖大神的第一行代码第三版而作,推荐大家看看这本书,就当是查缺补漏,而且还能学习很多kotlin的知识)。

out的使用
首先我们声明了三个类,如下:

open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)
class Teacher(name: String, age: Int) : Person(name, age)


我们知道虽然Student是Person的子类,但是List<Student>并不是List<Person>的子类,因为这么做会导致类型转换异常,那么为什么这么做会导致类型转换异常呢?我们下面通过一个例子验证一下, 先声明一个SampleData类,封装了一个泛型data字段;

class Sample<T> {
    var t: T? = null

    fun set(t: T) {
        this.t = t
    }

    fun get(): T? {
        return t
    }
}

fun main() {
    val student = Student("tom", 11)
    val sample = Sample<Student>()
    sample.set(student)
    handle(sample)//这里会编译报错
    val stu: Student? = sample.get()
}

fun handle(sample: Sample<Person>) {
    sample.set(Teacher("teacher1", 1))
}


假如这里允许将Sample<Student>赋值给Sample<Person>的话,那么上面的代码就是合法的,在handle内部就会出现再次赋值的情况,那么再次通过sample.get获取的时候期望获取一个Student的对象,但是实际上获取的是Teacher对象,就会出现了类型转换异常。

这里就好比List<String>中添加了一些String后,将List<String>赋值给List<Object>, 那我List<Object>中可以添加Integer吧,那么将来从List<Object>中获取的值是String还是Integer呢?就会出现类型转换异常,所以Java干脆就不允许这么做。
但是如果这里只是只读的,不能通过set设置值的话,就不会出现问题,那么这个时候Sample<Student>可不可以成为Sample<Person>的子类呢?

这个时候就要引出协变的定义了:定义如下:假如定义了一个MyClass<T>的泛型类,其中A是B的子类,同时MyClass<A>是MyClass<B>的子类的话,那么我们就可以说MyClass在T这个泛型上是协变的。

这个时候out就要派上用场了,out的意思就是泛型T只能书写在返回的位置,不能写在参数位置,我们重新书写上面的代码如下,就能将Sample<Student>赋值给Sample<Person>,编译不会报错,而且运行成功。

open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)
class Teacher(name: String, age: Int) : Person(name, age)

fun main() {
    val student = Student("tom", 11)
    val sample2 = Sample2(student)
    handle2(sample2)
    val result  = sample2.get()
    println(result)
}

fun handle2(sample2: Sample2<Person>) {
    sample2.get()
}

/**
 * 使用了out之后,如果想将T放入in位置会报错,不允许放在in位置; 虽然可以使用@UnsafeVariance将其放入in位置,但是运行时候会报错运行时异常
 */
class Sample2<out T>(private var t: T?) {
    fun get(): T? {
        return t
    }
}


假如我们想将T放在set的位置,是会报错的, 错误内容是Type parameter T is declared as 'out' but occurs in 'in' position in type T?, 我们可以模仿kotlin中List的处理方式,加上@UnsafeVariance关键字,就不会报错了,但是注意这里源码虽然将其放在了in位置上, 但是实际上只是将其用作比较是否contains某一个元素,并不会造成类型转换异常。

加上了@UnsafeVariance的代码如下,

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>
}

fun handle2(sample2: Sample2<Person>) {
    sample2.get()
    sample2.set(Teacher("tea",18))
}

class Sample2<out T>(private var t: T?) {
    fun get(): T? {
        return t
    }

    //Type parameter T is declared as 'out' but occurs in 'in' position in type T?
    fun set(t: @UnsafeVariance T?) {
        this.t = t
    }
}


此时运行代码就会报错:由此可见使用了out的话,就不允许将泛型放在in位置上,就不会造成类型转换异常的风险。如果想要放的话可以使用@UnsafeVariance注解,但是有可能造成类型转换异常。

Exception in thread "main" java.lang.ClassCastException: com.oman.lib.structure.Teacher cannot be cast to com.oman.lib.structure.Student at com.oman.lib.structure.Sample1Kt.main(Sample1.kt:11) at com.oman.lib.structure.Sample1Kt.main(Sample1.kt)

in的使用
in的使用自然就是逆变了,有的协变的底子,逆变相对来讲就好理解多了。
逆变的概念:假如定义了一个MyClass<T>的泛型类,其中A是B的子类,同时MyClass<B>是MyClass<A>的子类的话,那么我们就可以说MyClass在T这个泛型上是逆变的。我们下面通过一段代码来演示in的用法:

open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)
class Teacher(name: String, age: Int) : Person(name, age)

interface Transformer<T> {
    fun transform(t: T): String
}


fun handleTransformer(trans: Transformer<Student>) {
    val student = Student("Tom", 19)
    val result = trans.transform(student)
}

fun main() {
    val trans = object : Transformer<Person> {
        override fun transform(t: Person): String {
            return "${t.name} ${t.age}"
        }
    }
    handleTransformer(trans)//这行代码会报错
}


在上面的代码最后一行会提示需要输入Transformer<Student>对象,但是输入的却是Transformer<Person>对象, 这个时候in就派上用场了,在泛型声明的地方在T前面加上in即可解决问题,这意味着现在T只能出现在in位置,不能出现在out位置。

那么为什么in的时候泛型T不能出现在out位置呢?我们假设允许出现在out位置上,代码如下所示:

fun main() {
    val trans = object : Transformer<Person> {
        override fun transform(name: String, age: Int): Person {
            return Teacher("tea", 12)
        }
    }
    handleTransformer(trans)
}


interface Transformer<in T> {
    fun transform(name: String, age: Int): @UnsafeVariance T
}

fun handleTransformer(trans: Transformer<Student>) {
    val result = trans.transform("Tom", 19)
}


这里就会出现类型转换的异常: Exception in thread "main" java.lang.ClassCastException: com.oman.lib.structure.Teacher cannot be cast to com.oman.lib.structure.Student at com.oman.lib.structure.TestKt.handleTransformer(Test.kt:40) at com.oman.lib.structure.TestKt.main(Test.kt:31) at com.oman.lib.structure.TestKt.main(Test.kt)
这里因为Transformer的匿名实现类中返回的是一个Teacher对象,但是在handleTransformer方法中却想得到的是一个Student对象,这里就会类型转换异常。

总结
Kotlin和Java在提供协变逆变功能的时候就已经考虑了很多潜在的风险,只要我们按照语法规则的话,让泛型在协变的时候只出现在out位置上,在逆变的时候只出现在in位置上,就不会出现类型转换的异常了。另外@UnsafeVariance虽然可以打破这个限制,但是希望大家谨慎使用,有类型转换的风险。
————————————————
版权声明:本文为CSDN博主「Zhou Jiang」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/oman001/article/details/108556934

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值