我们看一些源码发现,一些源码,在范型前面添加了一些关键字 in 或者 out,这又是什么呢?比如 kotlin内置的 Comparable.kt
![](https://blog.xiunian.wang/wp-content/uploads/2024/07/image-1024x324.png)
再比如,Collections.MutableIterable 接口
![](https://blog.xiunian.wang/wp-content/uploads/2024/07/image-1-1024x294.png)
为什么会有一些加in,有一些加out 呢?了解这个之前我们来看下
范型是什么?
泛型(Generics)是一种允许类、接口和方法操作任意类型(类型参数)的特性。泛型提供了一种类型安全的方式来处理对象集合和其他数据结构,而无需在编译时指定具体的类型。
举一个栗子:
class Box
var value = t
}
为了创建这个Box,可以只需提供类型参数
val box: Box
如果编译器可以推断出来,可以省略参数
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()) }
默认会有什么问题?
默认范型的缺陷
上面我们定义了一个遥控器,我们可以传入不同类型的电视进行遥控,而且两个遥控器互不影响,因为,遥控器不能控制除了自身型号之外的其他电视,否则会报错
![](https://blog.xiunian.wang/wp-content/uploads/2024/07/image-2.png)
这符合我们的日常逻辑。现在问题来了,你妈妈家里的电视机 XiaomiTV1 的遥控器被狗咬坏了,叫你去商场买一个小米1的遥控器,并定义了如下测试接口:
![](https://blog.xiunian.wang/wp-content/uploads/2024/07/image-3.png)
你找了一圈,你在商场里没有找到同样型号的遥控器(得网上买),但是为了让你妈妈立马能看电视,你就买了一个万能遥控器回去,想着既然是万能的,应该是没问题吧。
结果买回来后,发现无法直接使用,让你买小米TV1的遥控器结果买了个万能的遥控器。
![](https://blog.xiunian.wang/wp-content/uploads/2024/07/image-4-1024x301.png)
这并不符合日常逻辑了?万能遥控器不能通过遥控器测试,要么你妈妈早就想到了,需要一个万能遥控器,可以这么交代你(定义测试验证方法)
![](https://blog.xiunian.wang/wp-content/uploads/2024/07/image-5.png)
另外一种这就需要用到范型修饰的关键字,in,告诉编译器这里需要拓展到XiaomiTv1的父类
![](https://blog.xiunian.wang/wp-content/uploads/2024/07/image-6.png)
这样就能通过测试了。
因此,使用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() }
这时候,你找到了一家经营肯德基的店铺,你发现无法让这家肯德基提供点餐服务
![](https://blog.xiunian.wang/wp-content/uploads/2024/07/image-7-1024x567.png)
是不是觉得很荒谬?既然随便找一家饭店就能点单,为什么肯德基不可以呢?由于编译器认死理,我们必须额外提供一些信息给编译器,让它知道我们是在特殊场景使用泛型。这时候我们需要传入 out
![](https://blog.xiunian.wang/wp-content/uploads/2024/07/image-8.png)
或者修改 Restaurant 的源代码定义:
// 变化在这里 // ↓ class Restaurant<out T> { fun orderFood(): T { /*..*/ } }
在做完以上任意一种修改以后,代码就可以通过编译了。这也就意味着,在这种情况下,我们可以使用Restaurant<KFC>替代Restaurant<Food>,也就意味着Restaurant<KFC>可以看作是Restaurant<Food>的子类
总结
当我们抽象方法返回一个范型对象时,我们可以使用out,当我们接受一个范型参数时我们可以用in. 在官方文档中有如下描述:
Consumer in, Producer out!🙂
![](https://blog.xiunian.wang/wp-content/uploads/2024/07/image-9-1024x388.png)
另外,对于翻译,covariance 翻译为协变,contravariance 翻译为逆变, invariance,不变,java中也有对应的概念对照如下。
![](https://blog.xiunian.wang/wp-content/uploads/2024/07/image-10-1024x194.png)
希望看完后对kotlin多一分了解。
参考文档: