Kotlin 范型中的in 和out 是什么?

我们看一些源码发现,一些源码,在范型前面添加了一些关键字 in 或者 out,这又是什么呢?比如 kotlin内置的 Comparable.kt

再比如,Collections.MutableIterable 接口

为什么会有一些加in,有一些加out 呢?了解这个之前我们来看下

范型是什么?

泛型(Generics)是一种允许类、接口和方法操作任意类型(类型参数)的特性。泛型提供了一种类型安全的方式来处理对象集合和其他数据结构,而无需在编译时指定具体的类型。

举一个栗子:

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

 为了创建这个Box,可以只需提供类型参数

val box: Box = Box(1)

 如果编译器可以推断出来,可以省略参数

val box = Box(1) 

这很好理解 。

范型,是对我们程序的抽象

在现实生活中,我们能看到各式各样的电视机遥控器,比如小米就有 1S、2S、3S、4S 电视遥控器。那么,如果我们将遥控器的概念迁移到程序的世界,我们就需要定义各种各样的“遥控器类”,如下:

// 小米1S电视机遥控
class TvMi1SController {
    fun turnOn() {}
    fun turnOff() {}
}

// 小米2S电视机遥控
class TvMi2SController {
    fun turnOn() {}
    fun turnOff() {}
}

// 小米3S电视机遥控
class TvMi3SController {
    fun turnOn() {}
    fun turnOff() {}
}

// 小米4S电视机遥控
class TvMi4SController {
    fun turnOn() {}
    fun turnOff() {}
}

...
省略几千种不同的遥控器

从上面的代码我们可以看到,如果我们为每一个型号的电视机都创建一个对应的遥控器类,然后在里面重复编写“开机”“关机”的方法,我们的工作量会很大,而且没有意义。这个时候,我们其实需要一个万能遥控器,而借助 Kotlin 的泛型,我们就可以很容易地实现

//          T代表泛型的形参
//               ↓
class Controller<T:TV> {
    fun turnOn(tv: T) {}
    fun turnOff(tv: T) {}
}

fun main() {
//                                泛型的实参
//                                   ↓
    val mi1Controller = Controller<XiaoMiTV1>()
    mi1Controller.turnOn(XiaoMiTV1())

//                                  泛型的实参
//                                     ↓
    val mi2Controller = Controller<XiaoMiTV2>()
    mi2Controller.turnOn(XiaoMiTV2()) 
  }

默认会有什么问题?

默认范型的缺陷

上面我们定义了一个遥控器,我们可以传入不同类型的电视进行遥控,而且两个遥控器互不影响,因为,遥控器不能控制除了自身型号之外的其他电视,否则会报错

这符合我们的日常逻辑。现在问题来了,你妈妈家里的电视机 XiaomiTV1 的遥控器被狗咬坏了,叫你去商场买一个小米1的遥控器,并定义了如下测试接口:

你找了一圈,你在商场里没有找到同样型号的遥控器(得网上买),但是为了让你妈妈立马能看电视,你就买了一个万能遥控器回去,想着既然是万能的,应该是没问题吧。

结果买回来后,发现无法直接使用,让你买小米TV1的遥控器结果买了个万能的遥控器。

这并不符合日常逻辑了?万能遥控器不能通过遥控器测试,要么你妈妈早就想到了,需要一个万能遥控器,可以这么交代你(定义测试验证方法)

另外一种这就需要用到范型修饰的关键字,in,告诉编译器这里需要拓展到XiaomiTv1的父类

这样就能通过测试了。

因此,使用in的时候,我们可以接受这个类型的父类作为实例。

Out 又是什么鬼?

再来看一个栗子🌰:

我们假定KFC是一种食物(不是一个品牌或者店铺)有如下继承关系

open class Food {}
class KFC : Food() {}

除此之外呢,我们还有一个饭店的角色,可以提供点单服务

class Restaurant<T : Food> {
    fun orderFood(): T {
        throw NotImplementedError()
    }
}

 这里需要一家普通的饭店,随便什么饭店都行

//                      这里需要一家普通的饭店,随便什么饭店都行
//                                     ↓
fun orderFood(restaurant: Restaurant<Food>) {
    // 从这家店点餐
    val food = restaurant.orderFood()
}

这时候,你找到了一家经营肯德基的店铺,你发现无法让这家肯德基提供点餐服务

是不是觉得很荒谬?既然随便找一家饭店就能点单,为什么肯德基不可以呢?由于编译器认死理,我们必须额外提供一些信息给编译器,让它知道我们是在特殊场景使用泛型。这时候我们需要传入 out

或者修改 Restaurant 的源代码定义:

//            变化在这里
//                ↓
class Restaurant<out T> {
    fun orderFood(): T { /*..*/ }
}

在做完以上任意一种修改以后,代码就可以通过编译了。这也就意味着,在这种情况下,我们可以使用Restaurant<KFC>替代Restaurant<Food>,也就意味着Restaurant<KFC>可以看作是Restaurant<Food>的子类

总结

当我们抽象方法返回一个范型对象时,我们可以使用out,当我们接受一个范型参数时我们可以用in. 在官方文档中有如下描述:

Consumer in, Producer out!🙂

另外,对于翻译,covariance 翻译为协变,contravariance 翻译为逆变, invariance,不变,java中也有对应的概念对照如下。

希望看完后对kotlin多一分了解。

参考文档: 

https://kotlinlang.org/docs/generics.html

https://time.geekbang.org/column/intro/100103401

Leave a Reply

Your email address will not be published. Required fields are marked *