有时,您需要与类而不是实例关联的单例函数或属性。在其他语言(如 Java)中,您可以使用 static
成员。为此,Kotlin 提供了 companion
object
。伴生对象不是实例,不能单独使用。
- 在
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 步:创建一些对和三元组
- 打开 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 中进行介绍。
在此任务中,您将详细了解集合(包括列表)和全新的集合类型 HashMap
第 1 步:详细了解列表
- 在上一课中,我们介绍了列表和可变列表。列表和可变列表是十分常用的数据结构,因此 Kotlin 为它们提供了许多内置函数。请查看以下关于列表函数的不完整列表。您可以在 Kotlin 文档中找到
List
和MutableList
的完整列表。
函数 | 用途 |
| 向可变列表中添加项。 |
| 从可变列表中移除项。 |
| 返回列表副本,且列表上的元素按倒序排列。 |
| 如果列表包含相应项,则返回 |
| 返回列表的一部分,即返回从第一个索引到第二个索引(但不包括第二个索引)的部分。 |
- 仍在 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 函数来计算总数,例如,根据每个字符串的长度计算总数。请注意,在上一个 Codelab 中,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 步:尝试哈希映射
哈希映射是另一种有用的数据结构,利用它们可以存储值和可用于引用所存储值的辅助对象。例如,如果您想存储班级或城镇中所有人的身高,而没有必要知道他们的身份,您可以将身高存储在 List
中。如果您曾想存储某个人的姓名,您可以将此人的姓名存储为键,并将身高存储为值。在 Kotlin 中,您可以使用 hashMapOf()
创建将几乎任何内容关联(或映射)到其他任何内容的哈希映射。哈希映射是对列表,其中第一个值充当第二个值的查询键。
- 创建与鱼类通用名(键)和这些鱼的学名(值)匹配的哈希映射。
val scientific = hashMapOf("guppy" to "poecilia reticulata", "catfish" to "corydoras", "zebra fish" to "danio rerio" )
- 然后,您可以使用
get()
甚至更短的方括号[]
来根据鱼类通用名键检索学名值。
println (scientific.get("guppy"))
⇒ poecilia reticulata
println(scientific.get("zebra fish"))
⇒ danio rerio
- 尝试指定映射中未包含的鱼名。
println("scientific.get("swordtail"")
⇒ null
如果映射中未包含某个键,尝试返回匹配的学名会返回 null
。根据映射数据的不同,某个可能键没有匹配项的情况可能很常见。对于此类情况,Kotlin 提供了 getOrDefault()
函数。
- 尝试使用
getOrDefault()
查找没有匹配项的键。
println(scientific.getOrDefault("swordtail", "sorry, I don't know"))
⇒ sorry, I don't know
如果您需要的不仅仅是返回值,Kotlin 会提供 getOrElse()
函数。
- 更改代码以使用
getOrElse()
而不是getOrDefault()
。
println(scientific.getOrElse("swordtail") {"sorry, I don't know"})
⇒ sorry, I don't know
执行大括号 {}
之间的任何代码,而不是返回简单的默认值。在此示例中,else
仅返回字符串,但可能就像查找并返回包含详细科学描述的网页一样奇妙。
就像 mutableListOf
一样,您也可以创建 mutableMapOf
。利用可变映射可以添加和移除项。可变意味着可以更改,不可变意味着不可更改。
在此任务中,您将了解 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 步:编写扩展函数
String
是 Kotlin 中的一种重要数据类型,具有许多有用的函数。但是,如果我们需要其他无法直接获取的String
函数,该怎么办?例如,我们可能需要确定String
是否有任何嵌入空格。
仍在 REPL 中执行操作,将一个简单的扩展函数写入 String
类 hasSpaces()
,以检查字符串是否包含空格。函数名称的前缀是函数要对其执行操作的类。
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
}
- 您可以简化
hasSpaces()
函数。我们并未明确要求使用this
,而且该函数可以缩减为一个表达式并返回。
fun String.hasSpaces() = indexOf(" ") != -1
第 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()
方法。这会以问号?
表示,后跟点,再跟类型。在正文中,您可以使用?.apply.
测试this
是否不为null
。
fun AquariumPlant?.pull() {
this?.apply {
println("removing $this")
}
}
val plant: AquariumPlant? = null
plant.pull()
- 在这种情况下,运行该程序不会产生任何输出。由于
plant
为null
,因此系统不会调用内部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") }