对/三元组、集合、常量和编写扩展函数

有时,您需要与类而不是实例关联的单例函数或属性。在其他语言(如 Java)中,您可以使用 static 成员。为此,Kotlin 提供了 companion object。伴生对象不是实例,不能单独使用。

  1. Decoration.kt 中,尝试伴生对象示例。
class Choice {
   companion object {
       var name: String = "lyric"
       fun showDescription(name:String) = println("My favorite $name")
   }
}

fun main() {
   println(Choice.name)
   Choice.showDescription("pick")
   Choice.showDescription("selection")
}
⇒
lyric
My favorite pick
My favorite selection

伴生对象是真正的 Kotlin 对象,可以实现接口并扩展类,确保其功能丰富,同时使用单例节省内存。

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

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

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

在此任务中,您将了解对和三元组及其使用方法。对和三元组是 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 中进行介绍。

在此任务中,您将详细了解集合(包括列表)和全新的集合类型 HashMap

第 1 步:详细了解列表

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

函数

用途

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 函数来计算总数,例如,根据每个字符串的长度计算总数。请注意,在上一个 Codelab 中,lambda 参数的默认名称为 it.。在这里,it 指的是系统遍历列表时该列表中的每个元素。
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
  1. 您可以对列表执行更多操作。查看可用功能的一种方法是在 IntelliJ IDEA 中创建列表,添加点,然后查看提示中的自动补全列表。这适用于任何对象。不妨使用一个列表试试。

7accafeefe61a724.png

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

第 2 步:尝试哈希映射

哈希映射是另一种有用的数据结构,利用它们可以存储值和可用于引用所存储值的辅助对象。例如,如果您想存储班级或城镇中所有人的身高,而没有必要知道他们的身份,您可以将身高存储在 List 中。如果您曾想存储某个人的姓名,您可以将此人的姓名存储为键,并将身高存储为值。在 Kotlin 中,您可以使用 hashMapOf() 创建将几乎任何内容关联(或映射)到其他任何内容的哈希映射。哈希映射是对列表,其中第一个值充当第二个值的查询键。

  1. 创建与鱼类通用名(键)和这些鱼的学名(值)匹配的哈希映射。
val scientific = hashMapOf("guppy" to "poecilia reticulata", "catfish" to "corydoras", "zebra fish" to "danio rerio" )

  1. 然后,您可以使用 get() 甚至更短的方括号 [] 来根据鱼类通用名键检索学名值。
println (scientific.get("guppy"))
⇒ poecilia reticulata
println(scientific.get("zebra fish"))
⇒ danio rerio
  1. 尝试指定映射中未包含的鱼名。
println("scientific.get("swordtail"")
⇒ null

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

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

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

  1. 更改代码以使用 getOrElse() 而不是 getOrDefault()
println(scientific.getOrElse("swordtail") {"sorry, I don't know"})
⇒ sorry, I don't know

执行大括号 {} 之间的任何代码,而不是返回简单的默认值。在此示例中,else 仅返回字符串,但可能就像查找并返回包含详细科学描述的网页一样奇妙。

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

在此任务中,您将了解 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. String 是 Kotlin 中的一种重要数据类型,具有许多有用的函数。但是,如果我们需要其他无法直接获取的 String 函数,该怎么办?例如,我们可能需要确定 String 是否有任何嵌入空格。

仍在 REPL 中执行操作,将一个简单的扩展函数写入 StringhasSpaces(),以检查字符串是否包含空格。函数名称的前缀是函数要对其执行操作的类。

fun String.hasSpaces(): Boolean {
    val found = this.indexOf(' ')
    // also valid: this.indexOf(" ")
    // returns positive number index in String or -1 if not found
    return found != -1
}
  1. 您可以简化 hasSpaces() 函数。我们并未明确要求使用 this,而且该函数可以缩减为一个表达式并返回。
fun String.hasSpaces() = indexOf(" ") != -1

第 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() 方法。这会以问号 ? 表示,后跟点,再跟类型。在正文中,您可以使用 ?.apply. 测试 this 是否不为 null
fun AquariumPlant?.pull() {
   this?.apply {
       println("removing $this")
   }
}

val plant: AquariumPlant? = null
plant.pull()
  1. 在这种情况下,运行该程序不会产生任何输出。由于 plantnull,因此系统不会调用内部 println()

扩展函数功能非常强大,而且 Kotlin 标准库大多以扩展函数的形式实现。

在本课中,您已详细了解集合、了解常量并体验扩展函数和属性的强大功能。

  • 创建一个与类(而非实例)关联的 companion object
  • 对和三元组可用于从函数返回多个值。例如:val twoLists = fish.partition { isFreshWater(it) }
  • Kotlin 包含许多适用于 List 的有用函数,如 reversed()contains()subList()
  • HashMap 可用于将键映射到值。例如:val scientific = hashMapOf("guppy" to "poecilia reticulata", "catfish" to "corydoras", "zebra fish" to "danio rerio" )
  • 使用 const 关键字声明编译时常量。您可以将它们放在顶层、整理到单例对象中或放在伴生对象中。
  • 扩展函数和属性可以向类中添加功能。例如:fun String.hasSpaces() = indexOf(" ") != -1
  • 利用可为 null 的接收器可以在类(可以是 null)上创建扩展函数。?. 运算符可以与 apply 配对,以在执行代码前检查 null。例如:this?.apply { println("removing $this") }