此 Codelab 是面向编程人员的 Kotlin 训练营课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。根据您的知识水平,您也许可以浏览某些部分。本课程专为了解面向对象的语言并希望学习 Kotlin 的编程人员而设计。
简介
在此 Codelab 中,您将了解 Kotlin 中许多不同的实用功能,包括对、集合和扩展函数。
本课程不是构建单个示例应用,而是用来学习知识,但它们是半独立的,以便您能够浏览已熟悉的部分。为了将两者联系起来,许多示例使用了水族馆主题。如果您想看完整的水族馆故事,请查看面向编程人员的 Kotlin 训练营 Udacity 课程。
您应当已掌握的内容
- Kotlin 函数、类和方法的语法
- 如何使用 IntelliJ IDEA 中的 Kotlin' REPL(读取-求值-输出循环)
- 如何在 IntelliJ IDEA 中创建新类并运行程序
学习内容
- 如何使用双人套和三人套
- 详细了解集合
- 定义和使用常量
- 编写扩展函数
您将执行的操作
- 了解 REPL 中的二元组、三元组和哈希映射
- 了解不同的常量组织方式
- 编写扩展函数和扩展属性
在此任务中,您将了解配对和三元组以及如何解构它们。对和三元组是 2 或 3 个通用项的预创建数据类。例如,在函数返回多个值的情况下,这可能就很有用。
假设您有一个鱼类 List
,还有一个函数 isFreshWater()
,用于检查鱼类是淡水鱼还是咸水鱼。List.partition()
会返回两个列表:一个列表满足条件为 true
的项目,另一个对应于条件为 false
的项目。
val twoLists = fish.partition { isFreshWater(it) }
println("freshwater: ${twoLists.first}")
println("saltwater: ${twoLists.second}")
第 1 步:创建一些对和三元组
- 打开 REPL (Tools > Kotlin > Kotlin REPL)。
- 创建一个用于关联设备与其用途的对,然后输出值。您可以通过以下方式创建对:首先使用关键字
to
创建一个用于连接两个值(如两个字符串)的表达式,然后使用.first
或.second
来引用每个值。
val equipment = "fish net" to "catching fish"
println("${equipment.first} used for ${equipment.second}")
⇒ fish net used for catching fish
- 创建一个三元组并使用
toString()
输出该三元组,然后使用toList()
将其转换为列表。使用带有 3 个值的Triple()
创建一个三元组,然后使用.first
、.second
和.third
来引用每个值。
val numbers = Triple(6, 9, 42)
println(numbers.toString())
println(numbers.toList())
⇒ (6, 9, 42) [6, 9, 42]
以上示例对所述对或三元组的所有部分使用相同的类型,但这并非强制要求。例如,这些部分可以是字符串、数字或列表 — 甚至可以是其他对或三元组。
- 创建一个对,其中该对的第一部分本身就是一个对。
val equipment2 = ("fish net" to "catching fish") to "equipment"
println("${equipment2.first} is ${equipment2.second}\n")
println("${equipment2.first.second}")
⇒ (fish net, catching fish) is equipment ⇒ catching fish
第 2 步:解构一些对和三元组
将对和三元组拆分为各自部分的过程称为“解构”。将对或三元组赋值给适当数量的变量,然后 Kotlin 将按顺序为每个部分赋值。
- 解构一个对,然后输出值。
val equipment = "fish net" to "catching fish"
val (tool, use) = equipment
println("$tool is used for $use")
⇒ fish net is used for catching fish
- 解构一个三元组,然后输出值。
val numbers = Triple(6, 9, 42)
val (n1, n2, n3) = numbers
println("$n1 $n2 $n3")
⇒ 6 9 42
请注意,解构对和三元组与数据类的工作原理相同,相关内容已在上一个 Codelab 中进行介绍。
在此任务中,您将详细了解集合(包括列表)以及新的集合类型(哈希映射)。
第 1 步:详细了解列表
- 在上一课中,我们介绍了列表和可变列表。它们是非常有用的数据结构,因此 Kotlin 为列表提供了许多内置函数。请查看以下关于列表函数的不完整列表。您可以在
List
和MutableList
的 Kotlin 文档中找到完整列表。
功能 | 用途 |
| 向可变列表中添加项。 |
| 从可变列表中移除项。 |
| 返回列表副本,且列表上的元素按倒序排列。 |
| 如果列表包含相应项,则返回 |
| 返回列表的一部分,即返回从第一个索引到第二个索引(但不包括第二个索引)的部分。 |
- 仍在 REPL 中执行操作,创建数字列表并对其调用
sum()
,这可计算所有元素的总数。
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
- 创建字符串列表,并计算该列表中所有字符串的总数。
val list2 = listOf("a", "bbb", "cc")
println(list2.sum())
⇒ error: none of the following functions can be called with the arguments supplied:
- 如果元素不是
List
知道如何直接计算总数的事物(如字符串),您可以指定如何通过配合使用.sumBy()
和 lambda 函数来计算总数,例如,根据每个字符串的长度计算总数。lambda 参数的默认名称为it
;此处的it
表示在遍历列表时列表中的每个元素。
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
- 您可以对列表执行更多操作。查看可用功能的一种方法是在 IntelliJ IDEA 中创建列表,添加点,然后查看提示中的自动补全列表。这适用于任何对象。不妨使用一个列表试试。
- 从列表中选择
listIterator()
,然后使用for
语句遍历列表,并输出所有以空格分隔的元素。
val list2 = listOf("a", "bbb", "cc")
for (s in list2.listIterator()) {
println("$s ")
}
⇒ a bbb cc
第 2 步:尝试哈希映射
在 Kotlin 中,您可以使用 hashMapOf()
将几乎任何内容映射到任何其他内容。哈希映射就像是一对列表,其中第一个值充当键。
- 创建一个哈希表,以便匹配鱼的症状、键和疾病以及值。
val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
- 然后,您可以根据症状键、使用
get()
甚至更短的方括号[]
来检索疾病值。
println(cures.get("white spots"))
⇒ Ich
println(cures["red sores"])
⇒ hole disease
- 请尝试指定地图未显示的症状。
println(cures["scale loss"])
⇒ null
如果某个键不在地图中,则尝试返回匹配的疾病会返回 null
。根据映射数据的不同,某个可能键没有匹配项的情况可能很常见。对于此类情况,Kotlin 提供了 getOrDefault()
函数。
- 尝试使用
getOrDefault()
查找没有匹配项的键。
println(cures.getOrDefault("bloating", "sorry, I don't know"))
⇒ sorry, I don't know
如果您需要的不仅仅是返回值,Kotlin 会提供 getOrElse()
函数。
- 更改代码以使用
getOrElse()
而不是getOrDefault()
。
println(cures.getOrElse("bloating") {"No cure for this"})
⇒ No cure for this
执行大括号 {}
之间的任何代码,而不是返回简单的默认值。在此示例中,else
只是返回一个字符串,而查找包含治愈的网页并返回该结果可能就很复杂。
就像 mutableListOf
一样,您也可以创建 mutableMapOf
。利用可变映射可以添加和移除项。可变意味着可以更改,不可变意味着不可更改。
- 创建可修改的映射,将设备字符串映射到项目数量。制作一张鱼网,然后使用
put()
将 3 个水箱洗碗机添加到商品目录中,然后使用remove()
移除该渔网。
val inventory = mutableMapOf("fish net" to 1)
inventory.put("tank scrubber", 3)
println(inventory.toString())
inventory.remove("fish net")
println(inventory.toString())
⇒ {fish net=1, tank scrubber=3}{tank scrubber=3}
在此任务中,您将了解 Kotlin 中的常量及其不同的整理方式。
第 1 步:了解常量与值
- 在 REPL 中,尝试创建一个数字常量。在 Kotlin 中,您可以创建顶层常量,并在编译时使用
const val
为这些常量赋值。
const val rocks = 3
该值一经赋予便无法更改,这听起来很像声明常规 val
。那么,const val
与 val
有何区别?const val
的值在编译时确定,因为 val
的值在程序执行期间确定,这意味着 val
可以在运行时由函数分配。
这意味着可以使用函数为 val
赋值,但无法为 const val
赋值。
val value1 = complexFunctionCall() // OK
const val CONSTANT1 = complexFunctionCall() // NOT ok
此外,const val
仅适用于顶层,并且仅适用于使用 object
声明的单例类,而不适用于常规类。您可以使用它创建仅包含常量的文件或单例对象,并根据需要导入此类文件或单例对象。
object Constants {
const val CONSTANT2 = "object constant"
}
val foo = Constants.CONSTANT2
第 2 步:创建伴生对象
Kotlin 没有类级别的常量的概念。
如需在类中定义常量,必须将常量封装到使用 companion
关键字声明的伴生对象中。伴生对象基本上是该类中的单例对象。
- 使用包含字符串常量的伴生对象创建一个类。
class MyClass {
companion object {
const val CONSTANT3 = "constant in companion"
}
}
伴生对象与常规对象之间的根本区别在于:
- 伴生对象是从包含类的静态构造函数初始化的,也就是说,创建对象时会创建伴生对象。
- 常规对象会在首次访问该对象时(即首次使用是)延迟进行初始化。
还有更多需要了解的信息,但目前您有必要知道的是将常量封装在伴生对象中的类中。
在此任务中,您将了解如何扩展类的行为。编写实用函数来扩展类的行为是一种很常见的现象。Kotlin 为声明以下实用函数提供了便捷语法:扩展函数。
利用扩展函数,您无需访问源代码即可向现有类添加函数。例如,您可以在软件包中的 Extensions.kt 文件中声明这些函数。这实际上不会修改该类,但使您能够在对该类的对象调用函数时使用点分表示法。
第 1 步:编写扩展函数
- 仍在 REPL 中,编写一个简单的扩展函数
hasSpaces()
,以检查字符串是否包含空格。函数名称的前缀是函数要对其执行操作的类。在函数内,this
是指调用它的对象,而it
是指find()
调用中的迭代器。
fun String.hasSpaces(): Boolean {
val found = this.find { it == ' ' }
return found != null
}
println("Does it have spaces?".hasSpaces())
⇒ true
- 您可以简化
hasSpaces()
函数。没有明确需要this
,该函数可以简化为一个表达式并返回,因此也不需要用大括号{}
括住函数。
fun String.hasSpaces() = find { it == ' ' } != null
第 2 步:了解扩展函数的限制
扩展函数只能访问要扩展的类的公共 API。无法访问值为 private
的变量。
- 尝试向标记为
private
的属性添加扩展函数。
class AquariumPlant(val color: String, private val size: Int)
fun AquariumPlant.isRed() = color == "red" // OK
fun AquariumPlant.isBig() = size > 50 // gives error
⇒ error: cannot access 'size': it is private in 'AquariumPlant'
- 检查下面的代码,并确定其将输出的内容。
open class AquariumPlant(val color: String, private val size: Int)
class GreenLeafyPlant(size: Int) : AquariumPlant("green", size)
fun AquariumPlant.print() = println("AquariumPlant")
fun GreenLeafyPlant.print() = println("GreenLeafyPlant")
val plant = GreenLeafyPlant(size = 10)
plant.print()
println("\n")
val aquariumPlant: AquariumPlant = plant
aquariumPlant.print() // what will it print?
⇒ GreenLeafyPlant AquariumPlant
plant.print()
会输出 GreenLeafyPlant
。aquariumPlant.print()
可能会输出 GreenLeafyPlant
,因为其已被赋予值 plant
。不过,类型会在编译时进行解析,因此系统会输出 AquariumPlant
。
第 3 步:添加扩展属性
除扩展函数外,利用 Kotlin 还可以添加扩展属性。与扩展函数一样,您需要指定要扩展的类,后跟一个点,再跟属性名称。
- 仍在 REPL 中执行操作,向
AquariumPlant
添加扩展属性isGreen
,如果此扩展属性呈绿色,该参数为true
。
val AquariumPlant.isGreen: Boolean
get() = color == "green"
isGreen
属性的访问方式与常规属性相同;访问时,系统会调用 isGreen
的 getter 来获取该值。
- 输出
aquariumPlant
变量的isGreen
属性并观察结果。
aquariumPlant.isGreen
⇒ res4: kotlin.Boolean = true
第 4 步:了解可为 null 的接收器
您扩展的类称为接收器,此类可以设为可为 null。如果您这么做了,正文中使用的 this
变量可以为 null
,因此请务必进行测试。如果预期调用方想要针对可为 null 的变量调用扩展方法,或者您想要在将函数应用于 null
时提供默认行为,则需要采用可为 null 的接收器。
- 仍在 REPL 中执行操作,定义一种采用可为 null 的接收器的
pull()
方法。这会以问号?
表示,后跟点,再跟类型。在 body 中,您可以使用 issuemark-dot-apply?.apply.
来测试this
是否不是null
fun AquariumPlant?.pull() {
this?.apply {
println("removing $this")
}
}
val plant: AquariumPlant? = null
plant.pull()
- 在这种情况下,运行该程序不会产生任何输出。由于
plant
为null
,因此系统不会调用内部println()
。
扩展函数功能非常强大,而且 Kotlin 标准库大多以扩展函数的形式实现。
在本课中,您已详细了解集合、了解常量并体验扩展函数和属性的强大功能。
- 对和三元组可用于从函数返回多个值。例如:
val twoLists = fish.partition { isFreshWater(it) }
- Kotlin 包含许多适用于
List
的有用函数,如reversed()
、contains()
和subList()
。 HashMap
可用于将键映射到值。例如:val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
- 使用
const
关键字声明编译时常量。您可以将它们放在顶层、整理到单例对象中或放在伴生对象中。 - 伴生对象是使用
companion
关键字定义的类定义中的单例对象。 - 扩展函数和属性可以向类中添加功能。例如:
fun String.hasSpaces() = find { it == ' ' } != null
- 利用可为 null 的接收器可以在类(可以是
null
)上创建扩展函数。?.
运算符可以与apply
配对,以在执行代码前检查null
。例如:this?.apply { println("removing $this") }
Kotlin 文档
如果您想详细了解本课程中的任何主题,或者遇到问题,最好访问 https://kotlinlang.org。
Kotlin 教程
https://try.kotlinlang.org 网站包含丰富的名为 Kotlin Koans 的教程,一种基于网络的解释器,以及一套完整的示例参考文档。
Udacity 课程
如需查看有关此主题的 Udacity 课程,请参阅面向编程人员的 Kotlin 训练营。
IntelliJ IDEA
您可以在 JetBrains 网站上找到 IntelliJ IDEA 文档。
此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:
- 根据需要布置作业。
- 告知学生如何提交家庭作业。
- 给家庭作业评分。
讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。
如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。
回答以下问题
问题 1
以下哪一项会返回列表的副本?
▢ add()
▢ remove()
▢ reversed()
▢ contains()
问题 2
class AquariumPlant(val color: String, val size: Int, private val cost: Double, val leafy: Boolean)
上的以下哪个扩展函数会引发编译器错误?
▢ fun AquariumPlant.isRed() = color == "red"
▢ fun AquariumPlant.isBig() = size > 45
▢ fun AquariumPlant.isExpensive() = cost > 10.00
▢ fun AquariumPlant.isNotLeafy() = leafy == false
问题 3
在使用 const val
时,您不应在以下哪个位置定义常量?
▢,位于文件顶层
▢ 常规类
单例对象中的 ▢
随播广告素材中的对象
继续学习下一课:
如需大致了解本课程(包括指向其他 Codelab 的链接),请参阅面向编程人员的 Kotlin 训练营:欢迎学习本课程。