此 Codelab 是面向编程人员的 Kotlin 训练营课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。根据您的知识水平,您也许可以浏览某些部分。本课程专为了解面向对象的语言并希望学习 Kotlin 的编程人员而设计。
简介
在此 Codelab 中,您将了解通用类、函数和方法,以及它们在 Kotlin 中的工作方式。
本课程不是构建单个示例应用,而是用来学习知识,但它们是半独立的,以便您能够浏览已熟悉的部分。为了将两者联系起来,许多示例使用了水族馆主题。如果您想看完整的水族馆故事,请查看面向编程人员的 Kotlin 训练营 Udacity 课程。
您应当已掌握的内容
- Kotlin 函数、类和方法的语法
- 如何在 IntelliJ IDEA 中创建新类并运行程序
学习内容
- 如何使用通用类、方法和函数
您将执行的操作
- 创建通用类并添加约束条件
- 创建
in
和out
类型 - 创建通用函数、方法和扩展函数
泛型简介
与许多编程语言一样,Kotlin 也有通用类型。泛型类型可以让您将类变为泛型,从而使得类更加灵活。
假设您要实现一个 MyList
类,用于存储项目列表。如果没有泛型,您需要为每种类型实现新版 MyList
:一个用于 Double
,一个用于 String
,一个用于 Fish
。使用泛型,您可以将列表设为通用,因此它可以包含任何类型的对象。这就类似于将通配符设为适合多种类型的通配符。
要定义泛型类型,请将 T 放置在类名称后加上尖括号 <T>
。(您可以使用其他字母或更长的名称,但通用类型的惯例为 T。)
class MyList<T> {
fun get(pos: Int): T {
TODO("implement")
}
fun addItem(item: T) {}
}
您可以引用 T
作为正常类型。get()
的返回值类型为 T
,addItem()
的参数类型为 T
。当然,通用列表非常有用,因此 List
类内置在 Kotlin 中。
第 1 步:创建类型层次结构
在此步骤中,您将创建一些类,以在下一步中使用。子类化在之前的 Codelab 中有所介绍,下面是简要回顾。
- 为了使示例保持整洁有序,请在 src 下创建一个新软件包,并将其命名为
generics
。 - 在 generics 软件包中,创建一个新的
Aquarium.kt
文件。这样,您就可以使用相同的名称重新定义无冲突的内容,因此此 Codelab 的剩余代码将放入此文件中。 - 为供水类型创建类型层次结构。首先,将
WaterSupply
设为open
类,以便它可以创建子类。 - 添加一个布尔值
var
参数needsProcessing
。这会自动创建一个可变属性以及一个 getter 和 setter。 - 创建一个子类
TapWater
,以便扩展WaterSupply
,并为needsProcessing
传递true
,因为自来水包含对鱼类不良的添加剂。 - 在
TapWater
中,定义一个名为addChemicalCleaners()
的函数,该函数会在净水后将needsProcessing
设置为false
。您可以通过TapWater
设置needsProcessing
属性,因为它默认为public
,并且可供子类访问。以下是完成后的代码。
package generics
open class WaterSupply(var needsProcessing: Boolean)
class TapWater : WaterSupply(true) {
fun addChemicalCleaners() {
needsProcessing = false
}
}
- 为
WaterSupply
创建另外两个子类,分别名为FishStoreWater
和LakeWater
。FishStoreWater
不需要处理,但LakeWater
必须使用filter()
方法进行过滤。过滤后,无需再次处理,因此请在filter()
中设置needsProcessing = false
。
class FishStoreWater : WaterSupply(false)
class LakeWater : WaterSupply(true) {
fun filter() {
needsProcessing = false
}
}
如果您需要更多信息,请参阅有关 Kotlin 继承的上一课。
第 2 步:创建泛型类
在此步骤中,您将修改 Aquarium
类以支持不同类型的供水。
- 在 Aquarium.kt 中,定义
Aquarium
类,类名称后在方括号中<T>
。 - 将类型为
T
的不可变属性waterSupply
添加到Aquarium
。
class Aquarium<T>(val waterSupply: T)
- 编写一个名为
genericsExample()
的函数。它不是类的一部分,因此它可以位于文件的顶级位置,例如main()
函数或类定义。在该函数中,创建Aquarium
并向其传递WaterSupply
。由于waterSupply
参数是通用参数,因此您必须用尖括号<>
指定类型。
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
}
- 在
genericsExample()
中,您的代码将可以访问水族馆的waterSupply
。由于它是TapWater
类型,因此您可以在没有任何类型转换的情况下调用addChemicalCleaners()
。
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}
- 创建
Aquarium
对象时,您可以移除尖括号以及它们之间的相互作用,因为 Kotlin 具有类型推断。因此,您在创建实例时无需说两次TapWater
。该类型可以通过Aquarium
的参数推断出来;它仍然会将Aquarium
的类型设置为TapWater
。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}
- 如需查看发生的情况,请在调用
addChemicalCleaners()
前后输出needsProcessing
。以下是完成后的函数。
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
aquarium.waterSupply.addChemicalCleaners()
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
}
- 添加一个
main()
函数以调用genericsExample()
,然后运行程序并观察结果。
fun main() {
genericsExample()
}
⇒ water needs processing: true water needs processing: false
第 3 步:使其更具体
通用意味着您几乎可以传递任何内容,但有时这又是一个问题。在此步骤中,您将使 Aquarium
类更具体于能够包含哪些内容中。
- 在
genericsExample()
中,创建一个Aquarium
,为waterSupply
传递一个字符串,然后输出水族馆的waterSupply
属性。
fun genericsExample() {
val aquarium2 = Aquarium("string")
println(aquarium2.waterSupply)
}
- 运行程序并观察结果。
⇒ string
得到的结果就是您传递的字符串,因为 Aquarium
不会对T.
(包括 String
等任何类型)进行任何限制。
- 在
genericsExample()
中,创建另一个Aquarium
,为waterSupply
传递null
。如果waterSupply
为 null,就输出"waterSupply is null"
。
fun genericsExample() {
val aquarium3 = Aquarium(null)
if (aquarium3.waterSupply == null) {
println("waterSupply is null")
}
}
- 运行程序并观察结果。
⇒ waterSupply is null
为什么您可以在创建 Aquarium
时传递 null
?之所以能够实现这一点,是因为默认情况下 T
表示可为 null Any?
类型,即类型层次结构顶部的类型。下文内容与您之前输入的内容相同。
class Aquarium<T: Any?>(val waterSupply: T)
- 如需不允许传递
null
,请通过移除Any
之后的?
来明确提供类型为Any
的T
。
class Aquarium<T: Any>(val waterSupply: T)
在这种情况下,Any
称为通用约束条件。这意味着可以为 T
传递任意类型,只要它不是 null
即可。
- 您真正需要的是确保只能为
T
传递WaterSupply
(或其子类之一)。将Any
替换为WaterSupply
可定义更具体的通用限制条件。
class Aquarium<T: WaterSupply>(val waterSupply: T)
第 4 步:添加更多检查
在此步骤中,您将了解 check()
函数,以帮助确保代码的行为符合预期。check()
函数是 Kotlin 中的标准库函数。它充当断言,并在其参数评估结果为 false
时抛出 IllegalStateException
。
- 向
Aquarium
类添加addWater()
方法以添加用水,并使用check()
来确保您不需要先处理水。
class Aquarium<T: WaterSupply>(val waterSupply: T) {
fun addWater() {
check(!waterSupply.needsProcessing) { "water supply needs processing first" }
println("adding water from $waterSupply")
}
}
在这种情况下,如果 needsProcessing
为 true,check()
会抛出异常。
- 在
genericsExample()
中,添加代码以使用LakeWater
创建Aquarium
,然后在其中添加一些水。
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.addWater()
}
- 运行程序,会出现异常情况,因为需要先过滤水。
⇒ Exception in thread "main" java.lang.IllegalStateException: water supply needs processing first at Aquarium.generics.Aquarium.addWater(Aquarium.kt:21)
- 添加用于过滤水的调用,然后将其添加到
Aquarium
。现在,当您运行程序时,不会抛出异常。
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.waterSupply.filter()
aquarium4.addWater()
}
⇒ adding water from generics.LakeWater@880ec60
上文介绍了泛型的基础知识。以下任务涵盖更多内容,但重要的概念是声明和使用具有通用约束的泛型类。
在此任务中,您将了解如何使用泛型实现输入和输出类型。in
类型是一种只能传入类而不返回的类型。out
类型是一种只能从类返回的类型。
查看 Aquarium
类,您会发现只有获取 waterSupply
属性时才会返回通用类型。没有任何方法会将 T
类型的值作为参数(构造函数中的定义除外)。Kotlin 可让您为这种情况精确定义 out
类型,并且可以推断出有关这些类型安全使用位置的额外信息。同样,您可以为仅传入方法而不返回方法的通用类型定义 in
类型。这使 Kotlin 能够对代码进行额外的检查以确保代码安全。
in
和 out
类型是 Kotlin 类型系统的指令。对整个类型系统的说明不在此训练营的讨论范围之内(其中涉及的操作非常复杂);不过,编译器会标记未适当标记为 in
和 out
的类型,因此您需要了解这些类型。
第 1 步:定义输出类型
- 在
Aquarium
类中,将T: WaterSupply
更改为out
类型。
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
...
}
- 在同一文件内的类之外,声明一个函数
addItemTo()
期望Aquarium
为WaterSupply
。
fun addItemTo(aquarium: Aquarium<WaterSupply>) = println("item added")
- 从
genericsExample()
调用addItemTo()
并运行您的程序。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
addItemTo(aquarium)
}
⇒ item added
Kotlin 可以确保 addItemTo()
不会使用泛型 WaterSupply
执行任何不安全的类型,因为它已声明为 out
类型。
- 如果移除
out
关键字,编译器会在调用addItemTo()
时给出错误提示,因为 Kotlin 无法保证您对该类型没有任何安全风险。
第 2 步:定义 in-type
in
类型与 out
类型类似,但适用于仅传入函数而不返回类型的通用类型。如果您尝试返回 in
类型,则会出现编译器错误。在此示例中,您将定义一个 in
类型作为接口的一部分。
- 在 Aquarium.kt 中,定义一个接口
Cleaner
,该接口接受仅限于WaterSupply
的通用T
。由于它仅用作clean()
的参数,因此您可以将其设为in
参数。
interface Cleaner<in T: WaterSupply> {
fun clean(waterSupply: T)
}
- 如需使用
Cleaner
接口,请创建一个TapWaterCleaner
类,通过添加化学品实现Cleaner
以清理TapWater
。
class TapWaterCleaner : Cleaner<TapWater> {
override fun clean(waterSupply: TapWater) = waterSupply.addChemicalCleaners()
}
- 在
Aquarium
类中,更新addWater()
以接受T
类型的Cleaner
,并在添加前对其进行清理。
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
fun addWater(cleaner: Cleaner<T>) {
if (waterSupply.needsProcessing) {
cleaner.clean(waterSupply)
}
println("water added")
}
}
- 更新
genericsExample()
示例代码以创建一个TapWaterCleaner
(使用TapWater
的Aquarium
),然后使用清理器添加一些水。它会根据需要使用清理器。
fun genericsExample() {
val cleaner = TapWaterCleaner()
val aquarium = Aquarium(TapWater())
aquarium.addWater(cleaner)
}
Kotlin 将使用 in
和 out
类型的信息来确保您的代码安全地使用泛型。Out
和 in
很容易记住:out
类型可以作为返回值向外传递,in
类型可以作为参数传入。
如果您希望更深入地了解类型和类型解决的问题,本文档将对此进行详细介绍。
在此任务中,您将了解通用函数及其适用情形。通常,只要函数接受具有通用类型的类的参数,就建议创建通用函数。
第 1 步:创建通用函数
- 在 generics/Aquarium.kt 中,创建一个
isWaterClean()
(接受Aquarium
)。您需要指定参数的通用类型;其中一个选项是使用WaterSupply
。
fun isWaterClean(aquarium: Aquarium<WaterSupply>) {
println("aquarium water is clean: ${aquarium.waterSupply.needsProcessing}")
}
但这意味着 Aquarium
必须具有 out
类型参数才能调用该方法。有时,out
或 in
过于严格,因为您需要为输入和输出都使用类型。您可以通过将函数设为通用来移除 out
要求。
- 如需将函数变为通用函数,请在关键字
fun
后添加尖括号,并传入通用类型T
和任何限制条件,在此示例中为WaterSupply
。将Aquarium
更改为受T
(而非WaterSupply
)约束。
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) {
println("aquarium water is clean: ${!aquarium.waterSupply.needsProcessing}")
}
T
是 isWaterClean()
的类型参数,用于指定水族馆的通用类型。这种模式很常见,建议您花点时间了解一下。
- 调用
isWaterClean()
函数,方法是在函数名之后和尖括号前指定尖括号内指定类型。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean<TapWater>(aquarium)
}
- 由于参数
aquarium
的类型推断,因此该值不是必需的类型,因此请将其移除。运行程序并观察输出结果。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean(aquarium)
}
⇒ aquarium water is clean: false
第 2 步:使用具体化类型创建通用方法
您也可以对方法使用通用函数,即使在具有自己的通用类型的类中也是如此。在此步骤中,您将向 Aquarium
添加一个通用方法,用于检查其类型为 WaterSupply
。
- 在
Aquarium
类中,声明hasWaterSupplyOfType()
方法,该方法接受一个通用参数R
(T
已采用)限制在WaterSupply
中,如果waterSupply
的类型为R
,则返回true
。这与您之前声明的函数类似,但是在Aquarium
类中。
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
- 请注意,最后的
R
带有红色下划线。将鼠标指针悬停在相应标记上,以查看错误是什么。 - 如需执行
is
检查,您需要告知 Kotlin 类型已具体化或真实,并且可以在函数中使用。为此,请将inline
放在fun
关键字前面,将reified
放在通用类型R
之前。
inline fun <reified R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
类型还原后,您可以像普通类型一样使用它,因为它是内嵌之后的真实类型。这意味着您可以使用 类型执行 is
检查。
如果您在此处不使用 reified
,则相应类型不够“真实”,足以让 Kotlin 允许 is
检查。这是因为非具体化的类型只在编译时可用,而不能在运行时用到您的程序中。详细内容将在下一节中介绍。
- 传递
TapWater
作为类型。与调用通用函数一样,调用通用方法时,应该在函数名称后使用尖括号和类型。运行程序并观察结果。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>()) // true
}
⇒ true
第 3 步:创建扩展函数
您还可以对具体函数和扩展函数使用具体化类型。
- 在
Aquarium
类之外,在WaterSupply
上定义一个名为isOfType()
的扩展函数,用于检查传递的WaterSupply
是否属于某个特定类型,例如TapWater
。
inline fun <reified T: WaterSupply> WaterSupply.isOfType() = this is T
- 像调用方法一样调用扩展函数。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.waterSupply.isOfType<TapWater>())
}
⇒ true
对于这些扩展函数,只要是 Aquarium
,即是什么类型(Aquarium
、TowerTank
或一些其他子类)就行了。使用 star-projection 语法可以便捷地指定各种匹配。当您使用星形投影时,Kotlin 会确保您不会执行任何不安全的操作。
- 要使用星形投影,请将
<*>
放在Aquarium
后面。将hasWaterSupplyOfType()
变为扩展函数,因为它实际上不是Aquarium
的核心 API 的一部分。
inline fun <reified R: WaterSupply> Aquarium<*>.hasWaterSupplyOfType() = waterSupply is R
- 更改对
hasWaterSupplyOfType()
的调用并运行您的程序。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>())
}
⇒ true
在前面的示例中,您必须将泛型类型标记为 reified
并将函数设为 inline
,因为 Kotlin 需要在运行时了解它们,而不仅仅是编译时。
所有通用类型都仅在 Kotlin 的编译过程中使用。这样,编译器就可以确保您可以安全地执行所有操作。在运行时,所有泛型类型都会被清空,因此之前关于检查已清空类型的错误消息。
因此,编译器可以创建正确的代码,而不必在运行时保留通用类型。但这的确意味着您有时会执行编译器不支持的一些操作,例如对通用类型执行 is
检查。因此,Kotlin 才增加了具体化或真实类型。
您可以在 Kotlin 文档中详细了解具体化类型和类型清除。
本课程重点介绍泛型,这对于使代码更灵活、更易重复使用至关重要。
- 创建通用类可使代码更灵活。
- 添加通用限制条件以限制泛型所使用的类型。
- 将
in
和out
类型与泛型一起使用可以更好地进行类型检查,从而限制传入或返回类的类型。 - 创建通用函数和方法,以便使用通用类型。例如:
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) { ... }
- 使用通用扩展函数为类添加非核心功能。
- 由于类型清除操作,有时需要使用具体化类型。与泛型类型不同,具体化类型会持续存在。
- 使用
check()
函数验证您的代码是否按预期运行。例如:check(!waterSupply.needsProcessing) { "water supply needs processing first" }
Kotlin 文档
如果您想详细了解本课程中的任何主题,或者遇到问题,最好访问 https://kotlinlang.org。
Kotlin 教程
https://try.kotlinlang.org 网站包含丰富的名为 Kotlin Koans 的教程,一种基于网络的解释器,以及一套完整的示例参考文档。
Udacity 课程
如需查看有关此主题的 Udacity 课程,请参阅面向编程人员的 Kotlin 训练营。
IntelliJ IDEA
您可以在 JetBrains 网站上找到 IntelliJ IDEA 文档。
此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:
- 根据需要布置作业。
- 告知学生如何提交家庭作业。
- 给家庭作业评分。
讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。
如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。
回答这些问题
问题 1
以下哪一项是通用类型的命名惯例?
▢ <Gen>
▢ <Generic>
▢ <T>
▢ <X>
问题 2
对用于泛型类型的允许类型的限制称为:
▢ 通用限制
▢ 通用约束
▢ 歧义
▢ 通用类型限制
问题 3
具体指:
▢ 已计算对象的实际执行影响。
▢ 已针对类设置受限条目索引。
▢ 通用类型参数已转换为实际类型。
▢ 触发了远程错误指示器。
继续学习下一课:
如需大致了解本课程(包括指向其他 Codelab 的链接),请参阅面向编程人员的 Kotlin 训练营:欢迎学习本课程。