Kotlin 编程人员训练营 5.1:扩展程序

此 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 步:创建一些对和三元组

  1. 打开 REPL (Tools > Kotlin > Kotlin REPL)。
  2. 创建一个用于关联设备与其用途的对,然后输出值。您可以通过以下方式创建对:首先使用关键字 to 创建一个用于连接两个值(如两个字符串)的表达式,然后使用 .first.second 来引用每个值。
val equipment = "fish net" to "catching fish"
println("${equipment.first} used for ${equipment.second}")
⇒ fish net used for catching fish
  1. 创建一个三元组并使用 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]

以上示例对所述对或三元组的所有部分使用相同的类型,但这并非强制要求。例如,这些部分可以是字符串、数字或列表 — 甚至可以是其他对或三元组。

  1. 创建一个对,其中该对的第一部分本身就是一个对。
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 将按顺序为每个部分赋值。

  1. 解构一个对,然后输出值。
val equipment = "fish net" to "catching fish"
val (tool, use) = equipment
println("$tool is used for $use")
⇒ fish net is used for catching fish
  1. 解构一个三元组,然后输出值。
val numbers = Triple(6, 9, 42)
val (n1, n2, n3) = numbers
println("$n1 $n2 $n3")
⇒ 6 9 42

请注意,解构对和三元组与数据类的工作原理相同,相关内容已在上一个 Codelab 中进行介绍。

在此任务中,您将详细了解集合(包括列表)以及新的集合类型(哈希映射)。

第 1 步:详细了解列表

  1. 在上一课中,我们介绍了列表和可变列表。它们是非常有用的数据结构,因此 Kotlin 为列表提供了许多内置函数。请查看以下关于列表函数的不完整列表。您可以在 ListMutableList 的 Kotlin 文档中找到完整列表。

功能

用途

add(element: E)

向可变列表中添加项。

remove(element: E)

从可变列表中移除项。

reversed()

返回列表副本,且列表上的元素按倒序排列。

contains(element: E)

如果列表包含相应项,则返回 true

subList(fromIndex: Int, toIndex: Int)

返回列表的一部分,即返回从第一个索引到第二个索引(但不包括第二个索引)的部分。

  1. 仍在 REPL 中执行操作,创建数字列表并对其调用 sum(),这可计算所有元素的总数。
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
  1. 创建字符串列表,并计算该列表中所有字符串的总数。
val list2 = listOf("a", "bbb", "cc")
println(list2.sum())
⇒ error: none of the following functions can be called with the arguments supplied:
  1. 如果元素不是 List 知道如何直接计算总数的事物(如字符串),您可以指定如何通过配合使用 .sumBy() 和 lambda 函数来计算总数,例如,根据每个字符串的长度计算总数。lambda 参数的默认名称为 it;此处的 it 表示在遍历列表时列表中的每个元素。
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
  1. 您可以对列表执行更多操作。查看可用功能的一种方法是在 IntelliJ IDEA 中创建列表,添加点,然后查看提示中的自动补全列表。这适用于任何对象。不妨使用一个列表试试。

  1. 从列表中选择 listIterator(),然后使用 for 语句遍历列表,并输出所有以空格分隔的元素。
val list2 = listOf("a", "bbb", "cc")
for (s in list2.listIterator()) {
    println("$s ")
}
⇒ a bbb cc

第 2 步:尝试哈希映射

在 Kotlin 中,您可以使用 hashMapOf() 将几乎任何内容映射到任何其他内容。哈希映射就像是一对列表,其中第一个值充当键。

  1. 创建一个哈希表,以便匹配鱼的症状、键和疾病以及值。
val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  1. 然后,您可以根据症状键、使用 get() 甚至更短的方括号 [] 来检索疾病值。
println(cures.get("white spots"))
⇒ Ich
println(cures["red sores"])
⇒ hole disease
  1. 请尝试指定地图未显示的症状。
println(cures["scale loss"])
⇒ null

如果某个键不在地图中,则尝试返回匹配的疾病会返回 null。根据映射数据的不同,某个可能键没有匹配项的情况可能很常见。对于此类情况,Kotlin 提供了 getOrDefault() 函数。

  1. 尝试使用 getOrDefault() 查找没有匹配项的键。
println(cures.getOrDefault("bloating", "sorry, I don't know"))
⇒ sorry, I don't know

如果您需要的不仅仅是返回值,Kotlin 会提供 getOrElse() 函数。

  1. 更改代码以使用 getOrElse() 而不是 getOrDefault()
println(cures.getOrElse("bloating") {"No cure for this"})
⇒ No cure for this

执行大括号 {} 之间的任何代码,而不是返回简单的默认值。在此示例中,else 只是返回一个字符串,而查找包含治愈的网页并返回该结果可能就很复杂。

就像 mutableListOf 一样,您也可以创建 mutableMapOf。利用可变映射可以添加和移除项。可变意味着可以更改,不可变意味着不可更改。

  1. 创建可修改的映射,将设备字符串映射到项目数量。制作一张鱼网,然后使用 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 步:了解常量与值

  1. 在 REPL 中,尝试创建一个数字常量。在 Kotlin 中,您可以创建顶层常量,并在编译时使用 const val 为这些常量赋值。
const val rocks = 3

该值一经赋予便无法更改,这听起来很像声明常规 val。那么,const valval 有何区别?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 关键字声明的伴生对象中。伴生对象基本上是该类中的单例对象。

  1. 使用包含字符串常量的伴生对象创建一个类。
class MyClass {
    companion object {
        const val CONSTANT3 = "constant in companion"
    }
}

伴生对象与常规对象之间的根本区别在于:

  • 伴生对象是从包含类的静态构造函数初始化的,也就是说,创建对象时会创建伴生对象。
  • 常规对象会在首次访问该对象时(即首次使用是)延迟进行初始化。

还有更多需要了解的信息,但目前您有必要知道的是将常量封装在伴生对象中的类中。

在此任务中,您将了解如何扩展类的行为。编写实用函数来扩展类的行为是一种很常见的现象。Kotlin 为声明以下实用函数提供了便捷语法:扩展函数。

利用扩展函数,您无需访问源代码即可向现有类添加函数。例如,您可以在软件包中的 Extensions.kt 文件中声明这些函数。这实际上不会修改该类,但使您能够在对该类的对象调用函数时使用点分表示法。

第 1 步:编写扩展函数

  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
  1. 您可以简化 hasSpaces() 函数。没有明确需要 this,该函数可以简化为一个表达式并返回,因此也不需要用大括号 {} 括住函数。
fun String.hasSpaces() = find { it == ' ' } != null

第 2 步:了解扩展函数的限制

扩展函数只能访问要扩展的类的公共 API。无法访问值为 private 的变量。

  1. 尝试向标记为 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'
  1. 检查下面的代码,并确定其将输出的内容。
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() 会输出 GreenLeafyPlantaquariumPlant.print() 可能会输出 GreenLeafyPlant,因为其已被赋予值 plant。不过,类型会在编译时进行解析,因此系统会输出 AquariumPlant

第 3 步:添加扩展属性

除扩展函数外,利用 Kotlin 还可以添加扩展属性。与扩展函数一样,您需要指定要扩展的类,后跟一个点,再跟属性名称。

  1. 仍在 REPL 中执行操作,向 AquariumPlant 添加扩展属性 isGreen,如果此扩展属性呈绿色,该参数为 true
val AquariumPlant.isGreen: Boolean
   get() = color == "green"

isGreen 属性的访问方式与常规属性相同;访问时,系统会调用 isGreen 的 getter 来获取该值。

  1. 输出 aquariumPlant 变量的 isGreen 属性并观察结果。
aquariumPlant.isGreen
⇒ res4: kotlin.Boolean = true

第 4 步:了解可为 null 的接收器

您扩展的类称为接收器,此类可以设为可为 null。如果您这么做了,正文中使用的 this 变量可以为 null,因此请务必进行测试。如果预期调用方想要针对可为 null 的变量调用扩展方法,或者您想要在将函数应用于 null 时提供默认行为,则需要采用可为 null 的接收器。

  1. 仍在 REPL 中执行操作,定义一种采用可为 null 的接收器的 pull() 方法。这会以问号 ? 表示,后跟点,再跟类型。在 body 中,您可以使用 issuemark-dot-apply ?.apply. 来测试 this 是否不是 null
fun AquariumPlant?.pull() {
   this?.apply {
       println("removing $this")
   }
}

val plant: AquariumPlant? = null
plant.pull()
  1. 在这种情况下,运行该程序不会产生任何输出。由于 plantnull,因此系统不会调用内部 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 时,您不应在以下哪个位置定义常量?

▢,位于文件顶层

▢ 常规类

单例对象中的 ▢

随播广告素材中的对象

继续学习下一课:5.2 泛型

如需大致了解本课程(包括指向其他 Codelab 的链接),请参阅面向编程人员的 Kotlin 训练营:欢迎学习本课程