此 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。 - 创建一个扩展
WaterSupply的子类TapWater,并为needsProcessing传递true,因为自来水含有对鱼有害的添加剂。 - 在
TapWater中,定义一个名为addChemicalCleaners()的函数,该函数在清洁水后将needsProcessing设置为false。needsProcessing属性可以从TapWater设置,因为该属性默认情况下为public,并且可供子类访问。以下是完成后的代码。
package generics
open class WaterSupply(var needsProcessing: Boolean)
class TapWater : WaterSupply(true) {
fun addChemicalCleaners() {
needsProcessing = false
}
}- 再创建两个
WaterSupply的子类,分别命名为FishStoreWater和LakeWater。FishStoreWater不需要处理,但必须使用filter()方法过滤LakeWater。过滤后,无需再次处理,因此在filter()中,将needsProcessing = false设置为 1。
class FishStoreWater : WaterSupply(false)
class LakeWater : WaterSupply(true) {
fun filter() {
needsProcessing = false
}
}如果您需要更多信息,请回顾之前关于 Kotlin 中继承的课程。
第 2 步:创建泛型类
在此步骤中,您将修改 Aquarium 类以支持不同类型的水源。
- 在 Aquarium.kt 中,定义一个
Aquarium类,并在类名后用方括号添加<T>。 - 向
Aquarium添加一个类型为T的不可变属性waterSupply。
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的实参推断出来;它仍会生成TapWater类型的Aquarium。
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后的?,将T显式设为Any类型。
class Aquarium<T: Any>(val waterSupply: T)在此上下文中,Any 称为泛型约束。这意味着,只要不是 null,任何类型都可以作为 T 传递。
- 您真正想要的是确保只有
WaterSupply(或其子类之一)可以作为T的实参传递。将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(),该函数需要一个WaterSupply的Aquarium。
fun addItemTo(aquarium: Aquarium<WaterSupply>) = println("item added")- 从
genericsExample()调用addItemTo()并运行程序。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
addItemTo(aquarium)
}⇒ item added
Kotlin 可以确保 addItemTo() 不会对泛型 WaterSupply 执行任何类型不安全的操作,因为 addItemTo() 被声明为 out 类型。
- 如果您移除
out关键字,编译器会在调用addItemTo()时抛出错误,因为 Kotlin 无法确保您不会对该类型执行任何不安全的操作。
第 2 步:定义 in 类型
in 类型与 out 类型类似,但适用于仅传递到函数中而不返回的泛型。如果您尝试返回 in 类型,则会收到编译器错误。在此示例中,您将定义一个 in 类型作为接口的一部分。
- 在 Aquarium.kt 中,定义一个接口
Cleaner,该接口采用受限于WaterSupply的泛型T。由于它仅用作clean()的实参,因此您可以将其设为in形参。
interface Cleaner<in T: WaterSupply> {
fun clean(waterSupply: T)
}- 如需使用
Cleaner接口,请创建一个实现Cleaner的类TapWaterCleaner,用于通过添加化学物质来清洁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 中,创建一个接受
Aquarium的函数isWaterClean()。您需要指定形参的泛型类型;一种方法是使用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(),该方法接受一个约束为WaterSupply的泛型形参R(T已使用),如果waterSupply的类型为R,则返回true。这与您之前声明的函数类似,但位于Aquarium类内。
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R- 请注意,最后的
R带红色下划线。将指针悬停在相应图标上,即可查看错误内容。
- 如需执行
is检查,您需要告知 Kotlin 该类型是具体化的(或真实的),并且可以在函数中使用。为此,请在fun关键字前面放置inline,并在泛型类型R前面放置reified。
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 或其他某个子类),只要它是 Aquarium 即可。使用星号投影语法可以方便地指定各种匹配项。使用星投影时,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 训练营:欢迎来到本课程”。