翻译说明:
原标题: When (and when not) to Use Type Parameter Constraints in Kotlin
之前的 Kotlin 文章, 欢迎查看:
翻译系列:
[译] 一个简单方式教你记住 Kotlin 的形参和实参
[译]Kotlin 中是应该定义函数还是定义属性?
[译]如何在你的 Kotlin 代码中移除所有的!!(非空断言)
[译]掌握 Kotlin 中的标准库函数: run,with,let,also 和 apply
[译]有关 Kotlin 类型别名 (typealias) 你需要知道的一切
[译]Kotlin 中是应该使用序列 (Sequences) 还是集合(Lists)?
[译]Kotlin 中的龟 (List) 兔(Sequence)赛跑
[译]Effective Kotlin 系列之考虑使用静态工厂方法替代构造器
[译]Effective Kotlin 系列之遇到多个构造器参数要考虑使用构建器
原创系列:
有关 Kotlin 属性代理你需要知道的一切
浅谈 Kotlin 中的 Sequences 源码解析
浅谈 Kotlin 中集合和函数式 API 完全解析 - 上篇
浅谈 Kotlin 语法篇之 lambda 编译成字节码过程完全解析
浅谈 Kotlin 语法篇之 Lambda 表达式完全解析
浅谈 Kotlin 语法篇之扩展函数
浅谈 Kotlin 语法篇之顶层函数, 中缀调用, 解构声明
浅谈 Kotlin 语法篇之如何让函数更好地调用
浅谈 Kotlin 语法篇之变量和常量
浅谈 Kotlin 语法篇之基础语法
实战系列:
用 Kotlin 撸一个图片压缩插件 ImageSlimming - 导学篇(一)
用 Kotlin 撸一个图片压缩插件 - 插件基础篇(二)
用 Kotlin 撸一个图片压缩插件 - 实战篇(三)
浅谈 Kotlin 实战篇之自定义 View 图片圆角简单应用
简述(又来皮一下):
今天这篇文章依旧很简单, 只要搞懂一个东西就可以了. 那就是泛型中的类型形参的约束, 这个概念在 Java 中也有的. 但是我们有个疑惑是什么情况下使用泛型类型形参呢?
进入正题(开始表演...)
当你在声明一个泛型时, Kotlin 允许你给这个泛型的类型形参增加约束条件, 换言之就是把类型形参可接受的类型实参限制在一个类型范围内. 那这个有什么作用呢? 让我们一起来看下面这个例子:
想像下有这么个需求场景: 假设你家里有几只宠物, 你想选择一个最喜欢的:
- fun <T> chooseFavorite(pets: List<T>): T {
- val favorite = pets[random.nextInt(pets.size)]
- // This next line won't compile - because `name` can't be resolved
- println("My favorite pet is ${favorite.name}")
- return favorite
- }
println()函数声明不会通过编译, 因为你无法通过 T 来引用 name 属性. 因为 T 可以是调用者指定的任何内容. 例如, 像以下代码实现, T 就是一个 Int 类型, 很明显它就没有 name 属性:
chooseFavorite(listOf(1, 2, 3))
解决方法一 - 放弃使用泛型
你可以尝试改造一下这个函数, 把泛型去掉:
- fun chooseFavorite(pets: List<Pet>): Pet {
- val favorite = pets[random.nextInt(pets.size)]
- println("My favorite pet is ${favorite.name}")
- return favorite
- }
这个处理看起来貌似没有什么问题, 但是我们会遇到一种不想要的返回值类型. 以下是我们调用该函数时会发生的情况:
- val pets: List<Dog> = listOf(Dog("Rover"), Dog("Sheba"))
- val favorite: Pet = chooseFavorite(pets)
尽管我们声明定义的是 List<Dog>, 但是通过 chooseFavorite 返回的是一个 Pet 类型, 除非这里我们使用强制类型转换.
解决办法二 - 使用类型形参约束
我们可以通过指定上限来限制类型形参 - 换句话说就是指定你想要接收的超类型. 在我们的例子中, 我们希望此函数作为 List <Pet > 处理 List <Dog>,List <Cat > 也就是既包含 Cat 类型也包含 Dog 类型的混合 Pet 类型的列表中.
- fun <T : Pet> chooseFavorite(pets: List<T>): T {
- val favorite = pets[random.nextInt(pets.size)]
- println("My favorite pet is ${favorite.name}")
- return favorite
- }
这里的类型形参的声明是 < T:Pet>, 这个 Pet 就是上界约束. 现在我们已经指定了这个, 调用代码只能传递 Pet 类型以及它的子类型.
- val pets: List<Dog> = listOf(Dog("Rover"), Dog("Sheba"))
- val favorite: Dog = chooseFavorite(pets)
使用建议
下面两种是你需要使用类型形参约束情况:
当你在某个类型上调用特定的函数或属性(即某个类型的类独有的函数和属性)
当你希望在函数返回时保留某个特定类型
这是一个快速的 "备忘单" 表, 可帮助您决定哪种情况使用什么?
需要调用成员 (类的成员函数或属性) | 不需要调用成员 (类的成员函数或属性) | |
---|---|---|
需要保留类型 | 使用带有类型参数约束的泛型 | 使用不带类型参数约束的泛型 |
不需要保留类型 | 使用非泛型和适当的抽象 | 使用 Java 中的原生态类型 |
如何指定约束
约束还有很多 - 您可以指定多个参数的约束, 以及对同一参数的多个约束. 对于所有细节, 请查看概念文章 Type Parameter Constraint. 您还可以在 Kotlin 的官方参考文档中快速查看常用约束.
读者有话说
本篇文章核心点在于什么情况下该使用泛型约束, 作者总结的很好就是那种表格, 理解和掌握了那张表格, 那么你在使用泛型形参约束上就会胸有成竹. 关于那个表格可能有点难理解, 我这里再补充解释一下:
首先, 解释下是否需要调用成员, 意思是在定义函数声明内部是否使用了泛型形参约束上界类型中对应类的特定成员包括函数或属性, 比如说 chooseFavorite 方法中的 name 属性就是 Pet 这个类特定的成员属性. 总之一句话: 在函数定义内部是否需要调用特定类型的类的成员或属性, 这也就直接决定了是否需要带类型参数约束, 如果不需要调用成员, 那么就不需要带类型参数约束
然后, 解释下是否需要保留类型, 就是在定义一个函数的时候, 返回值的类型是否要保留泛型形参约束上界类型, 主要作用就是避免强制类型转换, 例如例子中解决办法一就是去除泛型, 而是用了抽象的父类 Pet, 即使传入的是 List<Dog>, 但是 chooseFavorite 方法返回依然是父类 Pet, 外部接收类型 Dog 所以避免不了强制类型转换. 总之一句话: 是否需要保留类型, 也就直接决定了是否使用泛型, 如果不保留类型的话可以不使用泛型, 函数外部接收者可以使用抽象的父类来接收; 如果保留类型, 函数外部接收者就必须是明确一个类型, 那么此时如果还用抽象父类就避免不了类型转换, 那么此时就应该使用泛型
最后, 得出简单的总结:
1, 是否需要调用成员决定了在使用泛型前提下, 是否使用泛型类型参数约束; 否则直接可以使用抽象
2, 是否保留类型决定了是否使用泛型
3, 既保留类型又调用成员, 则就是使用泛型且带形参约束条件
4, 不保留类型又调用成员, 则就是不使用泛型和适当的抽象
5, 保留类型不调用成员, 则就是使用泛型不带形参约束条件
6, 不保留类型不调用成员, 则就是使用 java 中原生态类型, 例如 Java 中的 List 类型
来源: https://juejin.im/post/5bceb5915188255c6b654cec