# Kotlin 语言参考文档
Kotlin 语言参考文档
最新稳定版本: 2.4.0
# 关于翻译
本文是 Kotlin 语言参考文档的中文翻译版.
## 原文
网址: [https://kotlinlang.org/docs/](https://kotlinlang.org/docs/)
代码库: [https://github.com/JetBrains/kotlin-web-site](https://github.com/JetBrains/kotlin-web-site)
## 中文翻译版
网址: [https://kotlin.liying-cn.net/](https://kotlin.liying-cn.net/)
代码库: [https://github.com/LiYing2010/kotlin-web-site](https://github.com/LiYing2010/kotlin-web-site)
翻译者: 李 颖 [liying.cn.2010@gmail.com](mailto:liying.cn.2010@gmail.com)
关于本文档的任何问题, 欢迎与译者联系.
### 更新历史
* 2026 年 08 月: 第 18 次更新
* 2025 年 07 月: 第 17 次更新
* 2025 年 02 月: 第 16 次更新
* 2024 年 10 月: 第 15 次更新
* 2024 年 03 月: 第 14 次更新
* 2023 年 04 月: 第 13 次更新
* 2022 年 09 月: 第 12 次更新
* 2022 年 01 月: 第 11 次更新
* 2020 年 12 月: 第 10 次更新
* 2020 年 09 月: 第 9 次更新
* 2019 年 03 月: 第 8 次更新
* 2018 年 12 月: 第 7 次更新
* 2018 年 09 月: 第 6 次更新
* 2018 年 02 月: 第 5 次更新
* 2017 年 10 月: 第 4 次更新
* 2017 年 02 月: 第 3 次更新
* 2016 年 09 月: 第 2 次更新
* 2016 年 04 月: 初版翻译
# Kotlin 入门
Kotlin 的最新发布版本: [2.4.0](whatsnew24.html)
Kotlin 是一门现代编程语言, 它简洁, 跨平台, 而且能够与 Java 及其他语言交互.
你是刚刚开始学习 Kotlin 吗? 请参加我们的 Kotlin 之旅, 直接在浏览器内学习它的基础知识.
[欢迎参加我们的 Kotlin 观光之旅!](kotlin-tour-welcome.html)
## 安装 Kotlin
Kotlin 包含在 [IntelliJ IDEA](https://www.jetbrains.com/idea/download/) 和 [Android Studio](https://developer.android.com/studio) 每个发行版之内.
下载并安装这些 IDE 中的一个, 就可以开始使用 Kotlin 了.
## 选择你的 Kotlin 使用场景
控制台:
在这里你将会学习如何使用 Kotlin 开发一个控制台应用程序, 并创建单元测试.
1. [使用 IntelliJ IDEA 项目向导创建一个基本的 JVM 应用程序](jvm-get-started.html).
2. [编写你的第一个单元测试](jvm-test-using-junit.html).
后端:
在这里你将会学习如何使用 Kotlin 服务端技术开发后端应用程序.
* 将 Kotlin 引入你的 Java 项目: * [配置 Java 项目, 引入 Kotlin](mixing-java-kotlin-intellij.html) * [向你的 Java Maven 项目添加 Kotlin 测试](jvm-test-using-junit.html)
* 使用 Kotlin, 从头创建一个后端应用程序 : * [使用 Spring Boot 创建一个 RESTful Web 服务](jvm-get-started-spring-boot.html) * [使用 Ktor 创建 HTTP API](https://ktor.io/docs/creating-http-apis.html)
跨平台:
在这里你将会学习如何使用 [Kotlin Multiplatform](get-started.html) 来开发一个跨平台应用程序.
1. [为跨平台开发设置环境](quickstart.html).
2. 创建你的第一个 iOS 和 Android 应用程序:
* 从零开始创建一个跨平台应用程序, 并且: * [共用业务逻辑, 同时使用原生 UI](https://kotlinlang.org/docs/multiplatform/multiplatform-create-first-app.html) * [共用业务逻辑和 UI](https://kotlinlang.org/docs/multiplatform/compose-multiplatform-create-first-app.html)
* [让你的既有的 Android 应用程序在 iOS 上运行](https://kotlinlang.org/docs/multiplatform/compose-multiplatform-create-first-app.html)
* [使用 Ktor 和 SQLDelight 创建跨平台应用程序](https://kotlinlang.org/docs/multiplatform/multiplatform-ktor-sqldelight.html)
3. 查看 [示例项目](https://kotlinlang.org/docs/multiplatform/multiplatform-samples.html).
Android:
要使用 Kotlin 进行 Android 开发, 请阅读 [Google 的 Kotlin Android 开发入门教程](https://developer.android.com/kotlin/get-started).
数据分析:
从创建数据管道(Data Pipeline), 到真实生产环境的机器学习模型, Kotlin 都是用于处理数据并充分利用数据的很好的选择.
1. 在 IDE 中无缝的创建并编辑 Notebook:
* [Kotlin Notebook 入门](get-started-with-kotlin-notebooks.html)
2. 浏览和实验你的数据:
* [DataFrame](https://kotlin.github.io/dataframe/overview.html) – 一个用于数据分析和操作的库.
* [Kandy](https://kotlin.github.io/kandy/welcome.html) – 一个用于数据可视化的绘图工具.
3. 关注 Kotlin for Data Analysis 的 Twitter 官方帐号: [KotlinForData](http://twitter.com/KotlinForData).
## 获取支持
如果你遇到任何困难和问题,
可以到  Slack 寻求帮助: [获取邀请](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up),
或者到我们的 [问题追踪系统](https://youtrack.jetbrains.com/issues/KT) 提交报告.
## 没有找到需要的资料吗?
如果你没有找到需要的资料, 或对本页面内容感到疑惑, 请向我们 [反馈你的意见](https://surveys.hotjar.com/d82e82b0-00d9-44a7-b793-0611bf6189df).
# 欢迎参加我们的 Kotlin 观光之旅!
Note:
这些教程全部可以在你的浏览器中完成. 不需要安装任何软件.
通过我们的 Kotlin 观光之旅, 你将快速学习 Kotlin 编程语言的基础知识.
通过初学者教程, 可以掌握基础知识.
通过中级教程, 可以加深你的理解.
* [Hello world](kotlin-tour-hello-world.html)
* [中级教程: 扩展函数](kotlin-tour-intermediate-extension-functions.html)
*  [Hello world](kotlin-tour-hello-world.html)  [基本类型](kotlin-tour-basic-types.html)  [集合(Collection)](kotlin-tour-collections.html)  [控制流](kotlin-tour-control-flow.html)  [函数](kotlin-tour-functions.html)  [类](kotlin-tour-classes.html)  [Null 值安全性](kotlin-tour-null-safety.html)
*  [扩展函数](kotlin-tour-intermediate-extension-functions.html)  [作用域函数(Scope Function)](kotlin-tour-intermediate-scope-functions.html)  [带接受者的 Lambda 表达式](kotlin-tour-intermediate-lambdas-receiver.html)  [类与接口](kotlin-tour-intermediate-classes-interfaces.html)  [对象](kotlin-tour-intermediate-objects.html)  [开放类与特殊类](kotlin-tour-intermediate-open-special-classes.html)  [属性](kotlin-tour-intermediate-properties.html)  [Null 值安全性](kotlin-tour-intermediate-null-safety.html)  [库与 API](kotlin-tour-intermediate-libraries-and-apis.html)
# Hello world
 Hello world
 [基本类型](kotlin-tour-basic-types.html)
 [集合(Collection)](kotlin-tour-collections.html)
 [控制流](kotlin-tour-control-flow.html)
 [函数](kotlin-tour-functions.html)
 [类](kotlin-tour-classes.html)
 [Null 值安全性](kotlin-tour-null-safety.html)
下面是一个简单的程序, 输出 "Hello, world!":
```KOTLIN
fun main() {
println("Hello, world!")
// 输出结果为 Hello, world!
}
```
在 Kotlin 中:
* `fun` 用来声明一个函数
* `main()` 函数是你的程序开始的位置
* 函数体写在大括号 `{}` 之内
* [println()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/println.html) 和 [print()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/print.html) 函数将它们的参数打印到标准输出
函数是一组指令, 执行特定的任务. 创建一个函数后, 你就可以在需要执行这个任务时使用这个函数, 而不必反复编写这些指令.
函数会在后面的各章中详细介绍. 在此之前, 所有的示例程序都使用 `main()` 函数.
## 变量
所有的程序都需要存储数据, 变量可以帮助你实现这个目的. 在 Kotlin 中, 你可以:
* 使用 `val`, 声明只读的变量
* 使用 `var`, 声明可变的变量
Note:
在为只读变量初次赋值之后, 就不能再修改它的值.
要为变量赋值, 请使用赋值操作符 `=`.
例如:
```KOTLIN
fun main() {
//sampleStart
val popcorn = 5 // 有 5 盒爆米花
val hotdog = 7 // 有 7 个热狗
var customers = 10 // 队列中有 10 个客户
// 有些客户离开了队列
customers = 8
println(customers)
// 输出结果为 8
//sampleEnd
}
```
Tip:
变量可以声明在 `main()` 函数之外, 在你的程序开始的地方.
使用这种方式声明的变量, 我们称之为声明在 顶级(top level) 范围中.
由于 `customers` 是可变的变量, 可以在变量声明之后对它重新赋值.
Note:
我们建议默认将所有变量都声明为只读(`val`)变量.
只有在真正需要的时候才使用可变的(`var`)变量.
通过这种方式, 可以尽量避免意外的修改本来不应该修改的内容.
## 字符串模板
确定的知道变量内容如何打印到标准输出将会很有用处. 你可以使用 字符串模板 做到这一点.
你可以使用模板表达式来访问存储在变量和其它对象中的数据, 并将它们转换为字符串.
字符串值是包含在双引号 `"` 中的一串字符. 模板表达式总是以美元符号 `$` 作为起始.
要在模板表达式中计算一段代码的值, 请在美元符号 `$` 之后放置一对大括号 `{}`, 然后将代码放在大括号之内.
例如:
```KOTLIN
fun main() {
//sampleStart
val customers = 10
println("There are $customers customers")
// 输出结果为 There are 10 customers
println("There are ${customers + 1} customers")
// 输出结果为 There are 11 customers
//sampleEnd
}
```
更多详情请参见 [字符串模板](strings.html#string-templates).
你会注意到, 上面的示例中没有为变量声明类型. Kotlin 自己会推断它的类型: `Int`.
这个教程会在 [下一章](kotlin-tour-basic-types.html) 中解释 Kotlin 各种不同的基本类型, 以及如何声明这些类型.
## 实际练习
### 习题
完成以下代码, 让程序打印 `"Mary is 20 years old"` 到标准输出:
```KOTLIN
fun main() {
val name = "Mary"
val age = 20
// 在这里编写你的代码
}
```
```KOTLIN
fun main() {
val name = "Mary"
val age = 20
println("$name is $age years old")
}
```
## 下一步
[基本类型](kotlin-tour-basic-types.html)
# 基本类型
 [Hello world](kotlin-tour-hello-world.html)
 基本类型
 [集合(Collection)](kotlin-tour-collections.html)
 [控制流](kotlin-tour-control-flow.html)
 [函数](kotlin-tour-functions.html)
 [类](kotlin-tour-classes.html)
 [Null 值安全性](kotlin-tour-null-safety.html)
在 Kotlin 中, 每个变量和数据结构都有一个类型.
类型很重要, 因为它告诉编译器你可以对这个变量或数据结构做什么样的操作.
也就是说, 这个变量或数据结构有什么函数和属性.
在上一章中, Kotlin 能够知道上一个示例程序中的 `customers` 的类型是 [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/).
Kotlin 推断 类型的能力称为 类型推断.
`customers` 被赋值了一个整数值. 根据这一点, Kotlin 推断 `customers` 拥有数值类型 `Int`.
结果是, 编译器知道你可以对 `customers` 执行算数操作:
```KOTLIN
fun main() {
//sampleStart
var customers = 10
// 有些客户离开了队列
customers = 8
customers = customers + 3 // 加法示例, 结果为: 11
customers += 7 // 加法示例, 结果为: 18
customers -= 3 // 减法示例, 结果为: 15
customers *= 2 // 乘法示例, 结果为: 30
customers /= 3 // 除法示例, 结果为: 10
println(customers) // 输出结果为 10
//sampleEnd
}
```
Tip:
`+=`, `-=`, `*=`, `/=`, 和 `%=` 是计算并赋值操作符(Augmented Assignment Operator).
详情请参见 [计算并赋值](operator-overloading.html#augmented-assignments).
总的来说, Kotlin 有以下数据类型:
| 类别 |基本类型 |示例代码 |
------------------
| 整数 |`Byte`, `Short`, `Int`, `Long` |`val year: Int = 2020` |
| 无符号整数 |`UByte`, `UShort`, `UInt`, `ULong` |`val score: UInt = 100u` |
| 浮点数 |`Float`, `Double` |`val currentTemp: Float = 24.5f`, `val price: Double = 19.99` |
| 布尔值 |`Boolean` |`val isEnabled: Boolean = true` |
| 字符 |`Char` |`val separator: Char = ','` |
| 字符串 |`String` |`val message: String = "Hello, world!"` |
关于基本类型和它们的属性, 详情请参见 [类型概述](types-overview.html).
有了这些知识之后, 你可以声明变量, 并初始化这些变量.
只要变量在第一次读取之前初始化, Kotlin 就能够正确处理这些变量.
要声明一个变量但不初始化, 请使用 `:` 来指定它的类型.
例如:
```KOTLIN
fun main() {
//sampleStart
// 声明变量, 但不初始化
val d: Int
// 变量被初始化
d = 3
// 明确指定了变量类型, 而且初始化
val e: String = "hello"
// 可以读取变量, 因为已经它们初始化了
println(d) // 输出结果为 3
println(e) // 输出结果为 hello
//sampleEnd
}
```
如果一个变量在读取之前没有初始化, 会发生错误:
```KOTLIN
fun main() {
//sampleStart
// 声明变量, 但没有初始化
val d: Int
// 这里会发生错误
println(d)
// 错误: Variable 'd' must be initialized
//sampleEnd
}
```
现在你已经知道了如何声明基本类型, 下面我们来学习 [集合(Collection)](kotlin-tour-collections.html).
## 实际练习
### 习题
为每个变量明确声明正确的类型:
```KOTLIN
fun main() {
val a: Int = 1000
val b = "log message"
val c = 3.14
val d = 100_000_000_000_000
val e = false
val f = '\n'
}
```
```KOTLIN
fun main() {
val a: Int = 1000
val b: String = "log message"
val c: Double = 3.14
val d: Long = 100_000_000_000_000
val e: Boolean = false
val f: Char = '\n'
}
```
## 下一步
[集合(Collection)](kotlin-tour-collections.html)
# 集合(Collection)
 [Hello world](kotlin-tour-hello-world.html)
 [基本类型](kotlin-tour-basic-types.html)
 集合(Collection)
 [控制流](kotlin-tour-control-flow.html)
 [函数](kotlin-tour-functions.html)
 [类](kotlin-tour-classes.html)
 [Null 值安全性](kotlin-tour-null-safety.html)
在程序开发中, 能够将数据组织到数据结构中以供后续的处理, 这样的能力非常有用.
为了这样的目的, Kotlin 提供了集合.
Kotlin 有以下集合来组织数据元素:
| 集合类型 |描述 |
------------
| List |有顺序的元素组成的集合 |
| Set |唯一的、无顺序的元素组成的集合 |
| Map |一组键值对(key-value pair), 其中键是唯一, 并且每个键对应到唯一的值 |
每个集合类型都可以是可变的, 或只读的.
## List
列表按照元素添加的顺序保存它们, 而且允许重复的元素.
要创建一个只读的 List ([List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/)),
请使用 [listOf()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/list-of.html) 函数.
要创建一个可变的 List ([MutableList](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-mutable-list.html)),
请使用 [mutableListOf()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/mutable-list-of.html) 函数.
创建 List 时, Kotlin 可以推断它存储的元素类型.
如果要明确声明元素类型,
请在 List 的声明之后的尖括号 `<>` 中添加类型:
```KOTLIN
fun main() {
//sampleStart
// 只读 List
val readOnlyShapes = listOf("triangle", "square", "circle")
println(readOnlyShapes)
// 输出结果为 [triangle, square, circle]
// 可变的 List, 带有明确的类型声明
val shapes: MutableList = mutableListOf("triangle", "square", "circle")
println(shapes)
// 输出结果为 [triangle, square, circle]
//sampleEnd
}
```
Tip:
为了防止无意中修改 List 的内容, 你可以将可变的 List 赋值给一个 `List`, 来创建它的一个只读的视图:
```KOTLIN
val shapes: MutableList = mutableListOf("triangle", "square", "circle")
val shapesLocked: List = shapes
```
这种操作也叫做 类型变换(casting).
List 是有顺序的, 因此要访问 List 内的元素, 请使用 [下标访问操作符](operator-overloading.html#indexed-access-operator) `[]`:
```KOTLIN
fun main() {
//sampleStart
val readOnlyShapes = listOf("triangle", "square", "circle")
println("The first item in the list is: ${readOnlyShapes[0]}")
// 输出结果为 The first item in the list is: triangle
//sampleEnd
}
```
要获取 List 中的第一个或最后一个元素, 请分别使用 [.first()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/first.html)
和 [.last()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/last.html) 函数:
```KOTLIN
fun main() {
//sampleStart
val readOnlyShapes = listOf("triangle", "square", "circle")
println("The first item in the list is: ${readOnlyShapes.first()}")
// 输出结果为 The first item in the list is: triangle
//sampleEnd
}
```
Note:
[.first()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/first.html) 和 [.last()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/last.html)
函数是 扩展 函数.
要对一个对象调用扩展函数, 请在对象之后加上点号 `.`, 然后把函数名写在后面.
关于扩展函数, 详细内容会在 [中级向导](kotlin-tour-intermediate-extension-functions.html#extension-functions) 中介绍.
目前, 你只需要知道如何调用它们就行了
要得到 List 中元素的数量, 请使用 [.count()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/count.html) 函数:
```KOTLIN
fun main() {
//sampleStart
val readOnlyShapes = listOf("triangle", "square", "circle")
println("This list has ${readOnlyShapes.count()} items")
// 输出结果为 This list has 3 items
//sampleEnd
}
```
要检查一个元素是否存在于 List 中, 请使用 [in 操作符](operator-overloading.html#in-operator):
```KOTLIN
fun main() {
//sampleStart
val readOnlyShapes = listOf("triangle", "square", "circle")
println("circle" in readOnlyShapes)
// 输出结果为 true
//sampleEnd
}
```
要对可变 List 添加或删除元素, 请分别使用 [.add()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-mutable-list/add.html)
和 [.remove()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/remove.html) 函数:
```KOTLIN
fun main() {
//sampleStart
val shapes: MutableList = mutableListOf("triangle", "square", "circle")
// 向 List 添加 "pentagon"
shapes.add("pentagon")
println(shapes)
// 输出结果为 [triangle, square, circle, pentagon]
// 从 List 中删除第一个 "pentagon"
shapes.remove("pentagon")
println(shapes)
// 输出结果为 [triangle, square, circle]
//sampleEnd
}
```
## Set
List 包含有顺序的元素, 并且允许元素重复, Set 则是 无顺序的, 并且只保存 唯一的 元素.
要创建一个只读的 Set ([Set](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-set/)),
请使用 [setOf()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/set-of.html) 函数.
要创建一个可变的 Set ([MutableSet](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-mutable-set/)),
请使用 [mutableSetOf()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/mutable-set-of.html) 函数.
创建 Set 时, Kotlin 可以推断它存储的元素类型.
如果要明确声明元素类型, 请在 Set 的声明之后的尖括号 `<>` 中添加类型:
```KOTLIN
fun main() {
//sampleStart
// 只读的 Set
val readOnlyFruit = setOf("apple", "banana", "cherry", "cherry")
// 可变的 Set, 带有明确的类型声明
val fruit: MutableSet = mutableSetOf("apple", "banana", "cherry", "cherry")
println(readOnlyFruit)
// 输出结果为 [apple, banana, cherry]
//sampleEnd
}
```
在上面的示例中你可以看到, 由于 Set 只包含唯一的元素, 重复的 `"cherry"` 元素被丢弃了.
Tip:
为了防止无意中修改 Set 的内容, 你可以将可变的 Set 赋值给一个 `Set`, 来创建它的一个只读的视图:
```KOTLIN
val fruit: MutableSet = mutableSetOf("apple", "banana", "cherry", "cherry")
val fruitLocked: Set = fruit
```
Note:
由于 Set 是 无顺序的, 你不能访问位于某个下标的元素.
要得到 Set 中元素的数量, 请使用 [.count()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/count.html) 函数:
```KOTLIN
fun main() {
//sampleStart
val readOnlyFruit = setOf("apple", "banana", "cherry", "cherry")
println("This set has ${readOnlyFruit.count()} items")
// 输出结果为 This set has 3 items
//sampleEnd
}
```
要检查一个元素是否存在于 Set 中, 请使用 [in 操作符](operator-overloading.html#in-operator):
```KOTLIN
fun main() {
//sampleStart
val readOnlyFruit = setOf("apple", "banana", "cherry", "cherry")
println("banana" in readOnlyFruit)
// 输出结果为 true
//sampleEnd
}
```
要对可变 Set 添加或删除元素, 请分别使用
[.add()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-mutable-set/add.html)
和 [.remove()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/remove.html) 函数:
```KOTLIN
fun main() {
//sampleStart
val fruit: MutableSet = mutableSetOf("apple", "banana", "cherry", "cherry")
fruit.add("dragonfruit") // 向 Set 添加 "dragonfruit"
println(fruit) // 输出结果为 [apple, banana, cherry, dragonfruit]
fruit.remove("dragonfruit") // 从 Set 中删除 "dragonfruit"
println(fruit) // 输出结果为 [apple, banana, cherry]
//sampleEnd
}
```
## Map
Map 将元素保存为键值对(key-value pair). 你通过引用键(Key)来访问值(Value).
你可以将 Map 想象为好像一个食品菜单.
你可以通过寻找你想要吃的食物(键)来找到价格(值).
如果你想要查找一个值, 但不像 List 那样使用数字下标, 那么 Map 是很有用的.
Note:
* Map 中的每个键必须是唯一的, 这样 Kotlin 才能懂得你想要得到哪个值.
* 在 Map 中你可以有重复的值.
要创建一个只读的 Map ([Map](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/)),
请使用 [mapOf()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/map-of.html) 函数.
要创建一个可变的 Map ([MutableMap](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-mutable-map/)),
请使用 [mutableMapOf()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/mutable-map-of.html) 函数.
创建 Map 时, Kotlin 可以推断它存储的元素类型.
如果要明确声明元素类型, 请在 Map 的声明之后的尖括号 `<>` 中添加键和值的类型.
例如: `MutableMap`.
键的类型为 `String`, 值的类型为 `Int`.
创建 Map 的最简单的办法是在每个键和它对应的值之间使用 [to](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/to.html) :
```KOTLIN
fun main() {
//sampleStart
// 只读 Map
val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
println(readOnlyJuiceMenu)
// 输出结果为 {apple=100, kiwi=190, orange=100}
// 可变的 Map, 带有明确的类型声明
val juiceMenu: MutableMap = mutableMapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
println(juiceMenu)
// 输出结果为 {apple=100, kiwi=190, orange=100}
//sampleEnd
}
```
Tip:
为了防止无意中修改 Map 的内容, 你可以将可变的 Map 赋值给一个 `Map`, 来创建它的一个只读的视图:
```KOTLIN
val juiceMenu: MutableMap = mutableMapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
val juiceMenuLocked: Map = juiceMenu
```
要访问 Map 中的值, 请使用 [下标操作符](operator-overloading.html#indexed-access-operator) `[]`, 以它的键为下标:
```KOTLIN
fun main() {
//sampleStart
// 只读 Map
val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
println("The value of apple juice is: ${readOnlyJuiceMenu["apple"]}")
// 输出结果为 The value of apple juice is: 100
//sampleEnd
}
```
Note:
如果你使用 Map 中不存在的 key 来访问键值对(key-value pair), 会得到 `null` 值:
```KOTLIN
fun main() {
//sampleStart
// 只读 Map
val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
println("The value of pineapple juice is: ${readOnlyJuiceMenu["pineapple"]}")
// 输出结果为 The value of pineapple juice is: null
//sampleEnd
}
```
本教程会在后面的 [Null 值安全性](kotlin-tour-null-safety.html) 章节解释 null 值.
你也可以使用 [下标操作符](operator-overloading.html#indexed-access-operator) `[]` 来向可变 Map 添加元素:
```KOTLIN
fun main() {
//sampleStart
val juiceMenu: MutableMap = mutableMapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
juiceMenu["coconut"] = 150 // 向 Map 添加键 "coconut" 和值 150
println(juiceMenu)
// 输出结果为 {apple=100, kiwi=190, orange=100, coconut=150}
//sampleEnd
}
```
要从可变 Map 删除元素, 请使用
[.remove()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/remove.html)
函数:
```KOTLIN
fun main() {
//sampleStart
val juiceMenu: MutableMap = mutableMapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
juiceMenu.remove("orange") // 从 Map 删除键 "orange"
println(juiceMenu)
// 输出结果为 {apple=100, kiwi=190}
//sampleEnd
}
```
要得到 Map 中元素的数量, 请使用 [.count()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/count.html) 函数:
```KOTLIN
fun main() {
//sampleStart
// 只读 Map
val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
println("This map has ${readOnlyJuiceMenu.count()} key-value pairs")
// 输出结果为 This map has 3 key-value pairs
//sampleEnd
}
```
要检查一个键是否存在于 Map 中, 请使用 [.containsKey()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/contains-key.html) 函数:
```KOTLIN
fun main() {
//sampleStart
val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
println(readOnlyJuiceMenu.containsKey("kiwi"))
// 输出结果为 true
//sampleEnd
}
```
要得到 Map 中所有键或所有值的集合, 请分别使用 [keys](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/keys.html)
和 [values](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/values.html) 属性:
```KOTLIN
fun main() {
//sampleStart
val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
println(readOnlyJuiceMenu.keys)
// 输出结果为 [apple, kiwi, orange]
println(readOnlyJuiceMenu.values)
// 输出结果为 [100, 190, 100]
//sampleEnd
}
```
Note:
[keys](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/keys.html) 和 [values](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/values.html)
是对象的 属性. 要访问一个对象的属性, 请在对象之后加上点号 `.`, 然后把属性名写在后面.
属性会在 [类](kotlin-tour-classes.html) 的章节中详细介绍.
目前你只需要知道如何访问它们就行了.
要检查一个键或值是否存在于 Map 中, 请使用 [in 操作符](operator-overloading.html#in-operator):
```KOTLIN
fun main() {
//sampleStart
val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
println("orange" in readOnlyJuiceMenu.keys)
// 输出结果为 true
// 或者, 也可以不使用 keys 属性
println("orange" in readOnlyJuiceMenu)
// 输出结果为 true
println(200 in readOnlyJuiceMenu.values)
// 输出结果为 false
//sampleEnd
}
```
关于集合的其它更多功能, 请参见 [集合](collections-overview.html).
现在你已经知道了基本类型, 以及如何管理集合, 下面我们来看看在你的程序中能够使用的 [控制流](kotlin-tour-control-flow.html).
## 实际练习
### 习题 1
你有一个 “绿色” 数字的 List, 和一个 “红色” 数字的 List.
完成下面的代码, 打印这两个 List 中总共有多少个数字.
```KOTLIN
fun main() {
val greenNumbers = listOf(1, 4, 23)
val redNumbers = listOf(17, 2)
// 在这里编写你的代码
}
```
```KOTLIN
fun main() {
val greenNumbers = listOf(1, 4, 23)
val redNumbers = listOf(17, 2)
val totalCount = greenNumbers.count() + redNumbers.count()
println(totalCount)
}
```
### 习题 2
你有一个 Set, 其中包含你的服务器支持的协议. 一个用户要求使用某个协议.
完成下面的程序, 检查用户要求使用的协议是否支持 (`isSupported` 必须是 Boolean 值).
```KOTLIN
fun main() {
val SUPPORTED = setOf("HTTP", "HTTPS", "FTP")
val requested = "smtp"
val isSupported = // 在这里编写你的代码
println("Support for $requested: $isSupported")
}
```
提示
: 请确保使用字符串的大写格式来检查请求的协议.
你可以使用
: [.uppercase()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/uppercase.html)
: 函数来帮助你实现这一点.
```KOTLIN
fun main() {
val SUPPORTED = setOf("HTTP", "HTTPS", "FTP")
val requested = "smtp"
val isSupported = requested.uppercase() in SUPPORTED
println("Support for $requested: $isSupported")
}
```
### 习题 3
定义一个 Map, 将 1 到 3 的数字对应到它们的拼写.
使用这个 Map 来拼写指定的数字.
```KOTLIN
fun main() {
val number2word = // 在这里编写你的代码
val n = 2
println("$n is spelled as '${< 在这里编写你的代码 >}'")
}
```
```KOTLIN
fun main() {
val number2word = mapOf(1 to "one", 2 to "two", 3 to "three")
val n = 2
println("$n is spelled as '${number2word[n]}'")
}
```
## 下一步
[控制流](kotlin-tour-control-flow.html)
# 控制流
 [Hello world](kotlin-tour-hello-world.html)
 [基本类型](kotlin-tour-basic-types.html)
 [集合](kotlin-tour-collections.html)
 控制流
 [函数](kotlin-tour-functions.html)
 [类](kotlin-tour-classes.html)
 [Null 值安全性](kotlin-tour-null-safety.html)
和其他的编程语言一样, Kotlin 能够根据一个代码片段的计算结果是否为 true 来做出决策.
这样的代码片段称为 条件表达式.
Kotlin 还能够创建循环, 并在循环上迭代.
## 条件表达式
Kotlin 提供了 `if` 和 `when` 来检测条件表达式.
Note:
如果你必须在 `if` 和 `when` 之间做选择, 我们推荐使用 `when`, 因为它能够:
* 让你的代码更加易于阅读.
* 易于添加新的分支.
* 让你的代码更少出现错误.
### If
要使用 `if`, 请将条件表达式放在小括号 `()` 之内, 当调节表达式的结果为 true 时要做的操作放在大括号 `{}` 之内:
```KOTLIN
fun main() {
//sampleStart
val d: Int
val check = true
if (check) {
d = 1
} else {
d = 2
}
println(d)
// 输出结果为 1
//sampleEnd
}
```
在 Kotlin 中没有三元操作符 `condition ? then : else`.
`if` 可以用作表达式, 替代三元操作符的功能.
如果每个分支中只有 1 行代码, 那么大括号 `{}` 可以省略:
```KOTLIN
fun main() {
//sampleStart
val a = 1
val b = 2
println(if (a > b) a else b) // 返回值: 2
//sampleEnd
}
```
### When
如果你的条件表达式存在多个分支, 请使用 `when`.
要使用 `when`, 你应该:
* 将你想要计算的值放在括号 `()` 之内.
* 将所有的分支放在大括号 `{}` 之内.
* 在每个分支中使用 `->` 来分隔, `->` 之前是分支的检查条件, 之后是检查成功时执行的操作.
`when` 可以用作语句, 也可以用作表达式.
语句 不会返回任何值, 只是执行一些动作.
下面是将 `when` 用作语句的例子:
```KOTLIN
fun main() {
//sampleStart
val obj = "Hello"
when (obj) {
// 检查 obj 是否等于 "1"
"1" -> println("One")
// 检查 obj 是否等于 "Hello"
"Hello" -> println("Greeting")
// 默认语句
else -> println("Unknown")
}
// 输出结果为 Greeting
//sampleEnd
}
```
Note:
注意, 会按顺序检查所有的分支条件, 直到遇到一个条件被满足.
因此只有第一个满足条件的分支会被执行.
表达式 返回一个值, 可以在之后的代码中使用.
下面是将 `when` 用作表达式的例子.
`when` 表达式的结果被立即赋值给一个变量, 之后的 `println()` 函数使用这个变量:
```KOTLIN
fun main() {
//sampleStart
val obj = "Hello"
val result = when (obj) {
// 如果 obj 等于 "1", 将 result 设置为 "one"
"1" -> "One"
// 如果 obj 等于 "Hello", 将 result 设置为 "Greeting"
"Hello" -> "Greeting"
// 如果前面的条件都不满足, 将 result 设置为 "Unknown"
else -> "Unknown"
}
println(result)
// 输出结果为 Greeting
//sampleEnd
}
```
到此为止的示例中, `when` 都存在一个判定对象: `obj`. 但 `when` 也可以不使用判定对象.
下面的例子使用 没有 判定对象的 `when` 表达式, 来判定一系列的 Boolean 表达式:
```KOTLIN
fun main() {
val trafficLightState = "Red" // 可以是 "Green", "Yellow", 或 "Red"
val trafficAction = when {
trafficLightState == "Green" -> "Go"
trafficLightState == "Yellow" -> "Slow down"
trafficLightState == "Red" -> "Stop"
else -> "Malfunction"
}
println(trafficAction)
// 输出结果为 Stop
}
```
但是, 你也可以使用 `trafficLightState` 作为判定对象, 来编写这段代码:
```KOTLIN
fun main() {
val trafficLightState = "Red" // 可以是 "Green", "Yellow", 或 "Red"
val trafficAction = when (trafficLightState) {
"Green" -> "Go"
"Yellow" -> "Slow down"
"Red" -> "Stop"
else -> "Malfunction"
}
println(trafficAction)
// 输出结果为 Stop
}
```
使用带有判定对象的 `when` 可以让你的代码更加易于阅读和维护.
当你对 `when` 表达式使用判定对象时, 也有助于 Kotlin 检查是否覆盖了所有的可能情况.
否则, 如果你对 `when` 表达式不使用判定对象, 你就需要添加一个 else 分支.
## 条件表达式的实际练习
### 习题 1
创建一个简单的游戏, 当你的 2 个骰子掷出相同的结果时, 可以获胜.
使用 `if` 来做判断, 如果骰子结果相同, 打印 `You win :)`, 否则打印 `You lose :(`.
Tip:
在这个习题中, 你要导入包, 以便使用 `Random.nextInt()` 函数, 得到一个随机的 `Int` 值.
关于导入包, 详情请参见 [包与导入](packages.html).
提示
: 使用
: [相等运算符](operator-overloading.html#equality-and-inequality-operators)
: (
: `==`
: ) 比较骰子的结果.
```KOTLIN
import kotlin.random.Random
fun main() {
val firstResult = Random.nextInt(6)
val secondResult = Random.nextInt(6)
// 在这里编写你的代码
}
```
```KOTLIN
import kotlin.random.Random
fun main() {
val firstResult = Random.nextInt(6)
val secondResult = Random.nextInt(6)
if (firstResult == secondResult)
println("You win :)")
else
println("You lose :(")
}
```
### 习题 2
使用 `when` 表达式, 更新下面的程序, 当你输入游戏控制台按钮的名称时, 打印对应的动作.
| 按钮 |动作 |
----------
| A |Yes |
| B |No |
| X |Menu |
| Y |Nothing |
| 其他 |There is no such button |
```KOTLIN
fun main() {
val button = "A"
println(
// 在这里编写你的代码
)
}
```
```KOTLIN
fun main() {
val button = "A"
println(
when (button) {
"A" -> "Yes"
"B" -> "No"
"X" -> "Menu"
"Y" -> "Nothing"
else -> "There is no such button"
}
)
}
```
## 值范围
在讨论循环之前, 有必要了解如何构造一个作为循环迭代对象的值范围.
在 Kotlin 中, 创建值范围最常见的办法是使用 `..` 操作符.
例如, `1..4` 相当于 `1, 2, 3, 4`.
要声明一个值范围, 不包含它的终端值, 请使用 `..<` 操作符.
例如, `1..<4` 相当于 `1, 2, 3`.
要声明一个相反顺序的值范围, 请使用 [downTo](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.ranges/down-to.html).
例如, `4 downTo 1` 相当于 `4, 3, 2, 1`.
要声明一个值范围, 递增步长不为 1, 请使用 [step](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.ranges/step.html) 指定你希望的递增步长值.
例如, `1..5 step 2` 相当于 `1, 3, 5`.
你也可以对 `Char` 的值范围进行相同的操作:
* `'a'..'d'` 相当于 `'a', 'b', 'c', 'd'`
* `'z' downTo 's' step 2` 相当于 `'z', 'x', 'v', 't'`
## 循环
在编程中两种最常见的循环结构是 `for` 和 `while`.
使用 `for` 可以对一个值范围进行遍历, 并执行某个操作.
使用 `while` 可以反复执行某个操作, 直到满足某个条件为止.
### for
使用关于值范围的新知识, 你可以创建一个 `for` 循环, 对数字 1 到 5 进行遍历, 并打印每个数字.
请将迭代器(iterator)和值范围放在小括号 `()` 之内, 并使用关键字 `in`.
将你想要执行的操作放在大括号 `{}` 之内:
```KOTLIN
fun main() {
//sampleStart
for (number in 1..5) {
// number 是迭代器(iterator), 1..5 是值范围
print(number)
}
// 输出结果为 12345
//sampleEnd
}
```
`for` 循环也可以对集合(Collection)进行遍历:
```KOTLIN
fun main() {
//sampleStart
val cakes = listOf("carrot", "cheese", "chocolate")
for (cake in cakes) {
println("Yummy, it's a $cake cake!")
}
// 输出结果为 Yummy, it's a carrot cake!
// 输出结果为 Yummy, it's a cheese cake!
// 输出结果为 Yummy, it's a chocolate cake!
//sampleEnd
}
```
### while
`while` 有两种使用方式:
* 当一个条件表达式为 true 时, 执行一个代码段. (`while`)
* 先执行一个代码段, 然后再检查条件表达式. (`do-while`)
在第一种使用场景 (`while`) 中:
* 在小括号 `()` 中声明条件表达式, 当满足这个条件表达式时, 循环会继续.
* 在大括号 `{}` 中, 添加你想要执行的操作.
Note:
下面的示例使用 [递增操作符](operator-overloading.html#increments-and-decrements) `++`
来增加 `cakesEaten` 变量的值.
```KOTLIN
fun main() {
//sampleStart
var cakesEaten = 0
while (cakesEaten < 3) {
println("Eat a cake")
cakesEaten++
}
// 输出结果为 Eat a cake
// 输出结果为 Eat a cake
// 输出结果为 Eat a cake
//sampleEnd
}
```
在第二种使用场景 (`do-while`) 中:
* 在小括号 `()` 中声明条件表达式, 当满足这个条件表达式时, 循环会继续.
* 在大括号 `{}` 中, 添加你想要执行的操作, 并添加关键字 `do`.
```KOTLIN
fun main() {
//sampleStart
var cakesEaten = 0
var cakesBaked = 0
while (cakesEaten < 3) {
println("Eat a cake")
cakesEaten++
}
do {
println("Bake a cake")
cakesBaked++
} while (cakesBaked < cakesEaten)
// 输出结果为 Eat a cake
// 输出结果为 Eat a cake
// 输出结果为 Eat a cake
// 输出结果为 Bake a cake
// 输出结果为 Bake a cake
// 输出结果为 Bake a cake
//sampleEnd
}
```
关于条件表达式与循环的更多示例, 请参见 [条件与循环](control-flow.html).
现在你已经直到了 Kotlin 控制流的基本知识, 下面我们来学习如何编写你自己的 [函数](kotlin-tour-functions.html).
## 循环的实际练习
### 习题 1
你有一个程序, 计算批萨的片数, 直到有了 8 片, 组成一整个批萨.
请用两种方式重构这个程序:
* 使用 `while` 循环.
* 使用 `do-while` 循环.
```KOTLIN
fun main() {
var pizzaSlices = 0
// 要重构的代码从这里开始
pizzaSlices++
println("There's only $pizzaSlices slice/s of pizza :(")
pizzaSlices++
println("There's only $pizzaSlices slice/s of pizza :(")
pizzaSlices++
println("There's only $pizzaSlices slice/s of pizza :(")
pizzaSlices++
println("There's only $pizzaSlices slice/s of pizza :(")
pizzaSlices++
println("There's only $pizzaSlices slice/s of pizza :(")
pizzaSlices++
println("There's only $pizzaSlices slice/s of pizza :(")
pizzaSlices++
println("There's only $pizzaSlices slice/s of pizza :(")
pizzaSlices++
// 要重构的代码到这里结束
println("There are $pizzaSlices slices of pizza. Hooray! We have a whole pizza! :D")
}
```
```KOTLIN
fun main() {
var pizzaSlices = 0
while ( pizzaSlices < 7 ) {
pizzaSlices++
println("There's only $pizzaSlices slice/s of pizza :(")
}
pizzaSlices++
println("There are $pizzaSlices slices of pizza. Hooray! We have a whole pizza! :D")
}
```
```KOTLIN
fun main() {
var pizzaSlices = 0
pizzaSlices++
do {
println("There's only $pizzaSlices slice/s of pizza :(")
pizzaSlices++
} while ( pizzaSlices < 8 )
println("There are $pizzaSlices slices of pizza. Hooray! We have a whole pizza! :D")
}
```
### 习题 2
编写一个程序, 模拟 [Fizz buzz](https://en.wikipedia.org/wiki/Fizz_buzz) 游戏.
你的任务是打印从 1 到 100 的数字, 如果数字能被 3 整除, 则将它替换为 "fizz", 能被 5 整除, 则将它替换为 "buzz".
同时能被 3 和 5 整除, 则将它替换为 "fizzbuzz".
提示 1
: 使用
: `for`
: 循环来计数, 使用
: `when`
: 表达式来决定每一步打印什么内容.
提示 2
: 使用取模运算符 (
: `%`
: ) 返回被除数的余数.
使用
: [相等运算符](operator-overloading.html#equality-and-inequality-operators)
: (
: `==`
: ) 检查余数是否为 0.
```KOTLIN
fun main() {
// 在这里编写你的代码
}
```
```KOTLIN
fun main() {
for (number in 1..100) {
println(
when {
number % 15 == 0 -> "fizzbuzz"
number % 3 == 0 -> "fizz"
number % 5 == 0 -> "buzz"
else -> "$number"
}
)
}
}
```
### 习题 3
你有一个单词列表. 使用 `for` 和 `if` 来打印以 `l` 字母开头的单词.
提示
: 使用
: `String`
: 类型的
: [.startsWith()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/starts-with.html)
: 函数.
```KOTLIN
fun main() {
val words = listOf("dinosaur", "limousine", "magazine", "language")
// 在这里编写你的代码
}
```
```KOTLIN
fun main() {
val words = listOf("dinosaur", "limousine", "magazine", "language")
for (w in words) {
if (w.startsWith("l"))
println(w)
}
}
```
## 下一步
[函数](kotlin-tour-functions.html)
# 函数
 [Hello world](kotlin-tour-hello-world.html)
 [基本类型](kotlin-tour-basic-types.html)
 [集合(Collection)](kotlin-tour-collections.html)
 [控制流](kotlin-tour-control-flow.html)
 函数
 [类](kotlin-tour-classes.html)
 [Null 值安全性](kotlin-tour-null-safety.html)
在 Kotlin 中, 你可以使用 `fun` 关键字声明你自己的函数.
```KOTLIN
fun hello() {
return println("Hello, world!")
}
fun main() {
hello()
// 输出结果为 Hello, world!
}
```
在 Kotlin 中:
* 函数参数写在小括号 `()` 之内.
* 每个参数必须指定类型, 多个参数必须用逗号 `,` 隔开.
* 返回值类型写在函数的小括号 `()` 之后, 用冒号 `:` 隔开.
* 函数的 body 部写在大括号 `{}` 之内.
* `return` 关键字用来退出函数, 或从函数返回某个值.
Note:
如果函数不返回任何有用的值, 那么可以省略返回值类型和 `return` 关键字.
关于这个问题, 详情请参见 [没有返回值的函数](#functions-without-return).
在下面的示例中:
* `x` 和 `y` 是函数参数.
* `x` 和 `y` 类型为 `Int`.
* 函数的返回值类型为 `Int`.
* 函数被调用时返回 `x` 和 `y` 的和.
```KOTLIN
fun sum(x: Int, y: Int): Int {
return x + y
}
fun main() {
println(sum(1, 2))
// 输出结果为 3
}
```
Note:
在我们的 [编码规约](coding-conventions.html#function-names) 中,
我们建议函数名称以小写字母开头, 并使用驼峰式大小写(Camel case), 不使用下划线.
## 命名参数
为了让代码更简洁, 调用函数时, 你不必指定参数名称.
但是, 指定参数名称可以让你的代码更易于阅读.
这种方式称为 命名参数(named argument).
如果你指定了参数名称, 那么可以用任意的顺序来写这些参数.
Tip:
在下面的示例中, 使用了 [字符串模板](strings.html#string-templates) (`$`) 来访问参数值,
并将它们转换为 `String` 类型, 然后拼接到一个字符串中, 用于打印输出.
```KOTLIN
fun printMessageWithPrefix(message: String, prefix: String) {
println("[$prefix] $message")
}
fun main() {
// 使用命名参数, 交换了参数的顺序
printMessageWithPrefix(prefix = "Log", message = "Hello")
// 输出结果为 [Log] Hello
}
```
## 默认的参数值
你可以为函数参数定义默认值. 调用你的函数时, 有默认值的参数可以省略.
要声明默认值, 请在参数类型之后使用赋值操作符 `=`:
```KOTLIN
fun printMessageWithPrefix(message: String, prefix: String = "Info") {
println("[$prefix] $message")
}
fun main() {
// 使用两个参数调用函数
printMessageWithPrefix("Hello", "Log")
// 输出结果为 [Log] Hello
// 只使用 message 参数调用函数
printMessageWithPrefix("Hello")
// 输出结果为 [Info] Hello
printMessageWithPrefix(prefix = "Log", message = "Hello")
// 输出结果为 [Log] Hello
}
```
Note:
你可以跳过某个有默认值的参数, 而不是省略所有参数.
但是, 在第一个跳过的参数之后, 你必须对后续的所有参数指定名称.
## 没有返回值的函数
如果你的函数不返回任何有用的值, 那么它的返回值类型为 `Unit`.
`Unit` 类型只有唯一的一个值 – `Unit`.
你不必在你的函数 body 部明确的声明返回值为 `Unit`.
因此你不必使用 `return` 关键字, 也不必声明返回值类型:
```KOTLIN
fun printMessage(message: String) {
println(message)
// `return Unit` 或 `return` 都是可选的
}
fun main() {
printMessage("Hello")
// 输出结果为 Hello
}
```
## 单一表达式函数
为了让代码更加简洁, 你可以使用单一表达式函数. 例如, `sum()` 函数可以写得更短一些:
```KOTLIN
fun sum(x: Int, y: Int): Int {
return x + y
}
fun main() {
println(sum(1, 2))
// 输出结果为 3
}
```
你可以删除大括号 `{}`, 使用赋值操作符 `=` 来声明函数的 body 部.
当你使用赋值操作符 `=` 时, Kotlin 会使用类型推断, 因此你也可以省略返回值类型.
这样, `sum()` 函数就变成只有 1 行:
```KOTLIN
fun sum(x: Int, y: Int) = x + y
fun main() {
println(sum(1, 2))
// 输出结果为 3
}
```
但是, 如果你想让你的代码能够被其他开发者快速理解, 那么即使使用赋值操作符 `=`, 也还是明确定义返回值类型更好一些.
Note:
如果你使用大括号 `{}` 来声明函数的 body 部, 那么必须声明返回类型, 否则返回值类型将是 `Unit`.
## 函数中的提前返回 (Early Return)
如果想要你的函数中的代码在某个点之后不再进行后续处理, 请使用 `return` 关键字.
这个示例使用 `if` 判断, 如果条件表达式为真, 就从一个函数中提前返回:
```KOTLIN
// 注册的用户名列表
val registeredUsernames = mutableListOf("john_doe", "jane_smith")
// 注册 EMail 列表
val registeredEmails = mutableListOf("john@example.com", "jane@example.com")
fun registerUser(username: String, email: String): String {
// 如果用户名已被使用, 则提前返回
if (username in registeredUsernames) {
return "Username already taken. Please choose a different username."
}
// 如果 EMail 已被注册, 则提前返回
if (email in registeredEmails) {
return "Email already registered. Please use a different email."
}
// 如果用户名和 EMail 都没有被使用, 则进行注册处理
registeredUsernames.add(username)
registeredEmails.add(email)
return "User registered successfully: $username"
}
fun main() {
println(registerUser("john_doe", "newjohn@example.com"))
// 输出结果为: Username already taken. Please choose a different username.
println(registerUser("new_user", "newuser@example.com"))
// 输出结果为: User registered successfully: new_user
}
```
## 函数的实际练习
### 习题 1
写一个名为 `circleArea` 的函数, 接受一个整数参数, 表示圆的半径, 输出圆的面积大小.
Tip:
在这个习题中, 你会导入一个包, 以便通过 `PI` 来访问 $π$ 值.
关于包的导入, 更多详情请参见 [包与导入](packages.html).
提示
: 圆面积的计算公式是
: $πr^2$
: , 其中
: $r$
: 是半径.
```KOTLIN
import kotlin.math.PI
// 在这里编写你的代码
fun main() {
println(circleArea(2))
}
```
```KOTLIN
import kotlin.math.PI
fun circleArea(radius: Int): Double {
return PI * radius * radius
}
fun main() {
println(circleArea(2)) // 输出结果为 12.566370614359172
}
```
### 习题 2
将前一个习题中的 `circleArea` 函数重写为单一表达式函数.
```KOTLIN
import kotlin.math.PI
// 在这里编写你的代码
fun main() {
println(circleArea(2))
}
```
```KOTLIN
import kotlin.math.PI
fun circleArea(radius: Int): Double = PI * radius * radius
fun main() {
println(circleArea(2)) // 输出结果为 12.566370614359172
}
```
### 习题 3
你有一个函数, 它接受一个时/分/秒单位给定的时间间隔, 然后翻译为秒单位.
大多数情况下, 你只需要传递 1 个或 2 个参数, 而其它参数为 0.
改进这个函数以及调用它的代码, 使用默认参数值和命名参数, 让代码更加易于阅读.
```KOTLIN
fun intervalInSeconds(hours: Int, minutes: Int, seconds: Int) =
((hours * 60) + minutes) * 60 + seconds
fun main() {
println(intervalInSeconds(1, 20, 15))
println(intervalInSeconds(0, 1, 25))
println(intervalInSeconds(2, 0, 0))
println(intervalInSeconds(0, 10, 0))
println(intervalInSeconds(1, 0, 1))
}
```
```KOTLIN
fun intervalInSeconds(hours: Int = 0, minutes: Int = 0, seconds: Int = 0) =
((hours * 60) + minutes) * 60 + seconds
fun main() {
println(intervalInSeconds(1, 20, 15))
println(intervalInSeconds(minutes = 1, seconds = 25))
println(intervalInSeconds(hours = 2))
println(intervalInSeconds(minutes = 10))
println(intervalInSeconds(hours = 1, seconds = 1))
}
```
## Lambda 表达式
Kotlin 允许你使用 Lambda 表达式, 为函数编写更加简洁的代码.
例如, 下面的 `uppercaseString()` 函数:
```KOTLIN
fun uppercaseString(text: String): String {
return text.uppercase()
}
fun main() {
println(uppercaseString("hello"))
// 输出结果为 HELLO
}
```
可以写成一个 Lambda 表达式:
```KOTLIN
fun main() {
val upperCaseString = { text: String -> text.uppercase() }
println(upperCaseString("hello"))
// 输出结果为 HELLO
}
```
Lambda 表达式初看起来可能难于理解, 所以我们将它分解成各个部分.
Lambda 表达式写在大括号 `{}` 之内.
在 Lambda 表达式之内, 你会写以下内容:
* 参数, 在 `->` 之前.
* 函数 body 部, 在 `->` 之后.
在上面的示例中:
* `text` 是函数参数.
* `text` 类型为 `String`.
* 函数返回对 `text` 调用 [.uppercase()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/uppercase.html) 函数的结果.
* 整个 Lambda 表达式通过赋值操作符 `=` 赋值给变量 `upperCaseString`.
* 像函数一样使用 `upperCaseString` 变量, 字符串 `"hello"` 作为参数, 就会调用 Lambda 表达式.
* `println()` 函数打印输出结果.
Note:
如果你声明没有参数的 Lambda 表达式, 那么不必使用 `->`. 例如:
```KOTLIN
{ println("Log message") }
```
可以用很多方式使用 Lambda 表达式. 你可以:
* [将 Lambda 表达式用作另一个函数的参数](#pass-to-another-function)
* [从一个函数返回 Lambda 表达式](#return-from-a-function)
* [单独调用一个 Lambda 表达式](#invoke-separately)
### 传递给另一个函数
将 Lambda 表达式传递给另一个函数, 这个功能是很有用的, 一个很好的例子是对集合(Collection)使用
[.filter()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/filter.html) 函数:
```KOTLIN
fun main() {
//sampleStart
val numbers = listOf(1, -2, 3, -4, 5, -6)
val positives = numbers.filter ({ x -> x > 0 })
val isNegative = { x: Int -> x < 0 }
val negatives = numbers.filter(isNegative)
println(positives)
// 输出结果为 [1, 3, 5]
println(negatives)
// 输出结果为 [-2, -4, -6]
//sampleEnd
}
```
`.filter()` 函数接受一个 Lambda 表达式作为判定条件, 并将它应用于列表的每个元素.
只有在判定条件返回 `true` 时, 元素才会保留:
* `{ x -> x > 0 }`, 如果元素为正数, 则返回 `true`.
* `{ x -> x < 0 }`, 如果元素为负数, 则返回 `true`.
这个示例演示了将 Lambda 表达式传递给函数的两种方式:
* 对于正数, 示例直接在 `.filter()` 函数中添加 Lambda 表达式.
* 对于负数, 示例将 Lambda 表达式赋值给 `isNegative` 变量. 然后将 `isNegative` 变量用作 `.filter()` 函数的参数. 这种情况下, 你必须在 Lambda 表达式中指定函数参数 (`x`) 的类型.
Note:
如果一个 Lambda 表达式是函数的唯一参数, 你可以去掉函数的小括号 `()`:
```KOTLIN
val positives = numbers.filter { x -> x > 0 }
```
这是 [尾缀 Lambda 表达式(Trailing Lambda)](#trailing-lambdas) 的一个例子, 我们会在本章末尾详细介绍.
另一个好的例子是, 使用 [.map()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/map.html)
函数, 对集合中的元素进行变换:
```KOTLIN
fun main() {
//sampleStart
val numbers = listOf(1, -2, 3, -4, 5, -6)
val doubled = numbers.map { x -> x * 2 }
val isTripled = { x: Int -> x * 3 }
val tripled = numbers.map(isTripled)
println(doubled)
// 输出结果为 [2, -4, 6, -8, 10, -12]
println(tripled)
// 输出结果为 [3, -6, 9, -12, 15, -18]
//sampleEnd
}
```
`.map()` 函数接受一个 Lambda 表达式, 作为变换函数:
* `{ x -> x * 2 }` 接受 List 中的每个元素, 返回这个元素乘以 2 的结果.
* `{ x -> x * 3 }` 接受 List 中的每个元素, 返回这个元素乘以 3 的结果.
### 函数类型
在从一个函数返回一个 Lambda 表达式之前, 你首先需要理解 函数类型.
你已经学习了基本类型, 但函数本身也有它的类型.
Kotlin 的类型推断功能能够通过参数类型推断一个函数的类型.
但有的时候你需要明确指定函数类型.
编译器需要函数类型, 然后才能知道对这个函数允许什么, 不允许什么.
函数类型的语法包括:
* 每个参数的类型, 写在小括号 `()` 之内, 以逗号 `,` 分隔.
* 返回值类型, 写在 `->` 之后.
例如: `(String) -> String`, 或 `(Int, Int) -> Int`.
如果为 `upperCaseString()` 定义一个函数类型, 那么 Lambda 表达式如下:
```KOTLIN
val upperCaseString: (String) -> String = { text -> text.uppercase() }
fun main() {
println(upperCaseString("hello"))
// 输出结果为 HELLO
}
```
如果你的 Lambda 表达式没有参数, 那么小括号 `()` 保留为空. 例如: `() -> Unit`
Note:
你必须声明参数类型和返回值类型, 要么写在 Lambda 表达式内, 要么声明为函数类型.
否则, 编译器无法知道你的 Lambda 表达式的类型.
例如, 下面的代码无法工作:
`val upperCaseString = { str -> str.uppercase() }`
### 从函数中返回
可以从函数中返回 Lambda 表达式.
为了让编译器知道返回的 Lambda 表达式 的类型, 你必须声明一个函数类型.
在下面的示例中, `toSeconds()` 函数返回的函数类型是 `(Int) -> Int`, 因为它总是返回一个 Lambda 表达式,
这个 Lambda 表达式接受一个 `Int` 类型的参数, 并返回一个 `Int` 值.
这个示例使用 `when` 表达式, 来确定在调用 `toSeconds()` 时返回哪个 Lambda 表达式:
```KOTLIN
fun toSeconds(time: String): (Int) -> Int = when (time) {
"hour" -> { value -> value * 60 * 60 }
"minute" -> { value -> value * 60 }
"second" -> { value -> value }
else -> { value -> value }
}
fun main() {
val timesInMinutes = listOf(2, 10, 15, 1)
val min2sec = toSeconds("minute")
val totalTimeInSeconds = timesInMinutes.map(min2sec).sum()
println("Total time is $totalTimeInSeconds secs")
// 输出结果为 Total time is 1680 secs
}
```
### 单独调用
Lambda 表达式可以单独调用, 方法是在大括号 `{}` 之后添加小括号 `()`, 并在小括号中加上参数:
```KOTLIN
fun main() {
//sampleStart
println({ text: String -> text.uppercase() }("hello"))
// 输出结果为 HELLO
//sampleEnd
}
```
### 尾缀 Lambda 表达式(Trailing Lambda)
你已经看到, 如果一个 Lambda 表达式是函数的唯一参数, 你可以去掉函数的小括号 `()`.
如果一个 Lambda 表达式是函数的最后一个参数, 那么 Lambda 表达式可以写在函数的小括号 `()` 之外.
对这两种情况, 这样的语法称为 尾缀 Lambda 表达式(Trailing Lambda).
例如, [.fold()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/fold.html)
函数接受一个初始值, 以及一个操作:
```KOTLIN
fun main() {
//sampleStart
// 初始值为 0.
// 操作是对初始值累加 List 中的每个元素.
println(listOf(1, 2, 3).fold(0, { x, item -> x + item })) // 输出结果为 6
// 或者, 也可以写成 尾缀 Lambda 表达式的形式
println(listOf(1, 2, 3).fold(0) { x, item -> x + item }) // 输出结果为 6
//sampleEnd
}
```
关于 Lambda 表达式, 更多详情请参见 [Lambda 表达式与匿名函数(Anonymous Function)](lambdas.html#lambda-expressions-and-anonymous-functions).
本教程的下一章是学习 Kotlin 中的 [类](kotlin-tour-classes.html).
## Lambda 表达式的实际练习
### 习题 1
你有一个 Web Service 支持的动作列表, 所有请求的一个共通前缀, 某个资源的一个 ID.
要对资源 ID 5 请求 `title` 动作, 你需要创建下面的 URL: `https://example.com/book-info/5/title`.
使用一个 Lambda 表达式, 从动作列表创建对应的 URL 列表.
```KOTLIN
fun main() {
val actions = listOf("title", "year", "author")
val prefix = "https://example.com/book-info"
val id = 5
val urls = // 在这里编写你的代码
println(urls)
}
```
```KOTLIN
fun main() {
val actions = listOf("title", "year", "author")
val prefix = "https://example.com/book-info"
val id = 5
val urls = actions.map { action -> "$prefix/$id/$action" }
println(urls)
}
```
### 习题 2
编写一个函数, 接受一个 `Int` 值和一个动作 (一个 `() -> Unit` 类型的函数), 然后重复执行这个动作指定的次数.
然后使用这个函数打印 “Hello” 5 次.
```KOTLIN
fun repeatN(n: Int, action: () -> Unit) {
// 在这里编写你的代码
}
fun main() {
// 在这里编写你的代码
}
```
```KOTLIN
fun repeatN(n: Int, action: () -> Unit) {
for (i in 1..n) {
action()
}
}
fun main() {
repeatN(5) {
println("Hello")
}
}
```
## 下一步
[类](kotlin-tour-classes.html)
# 类
 [Hello world](kotlin-tour-hello-world.html)
 [基本类型](kotlin-tour-basic-types.html)
 [集合(Collection)](kotlin-tour-collections.html)
 [控制流](kotlin-tour-control-flow.html)
 [函数](kotlin-tour-functions.html)
 类
 [Null 值安全性](kotlin-tour-null-safety.html)
Kotlin 通过类和对象支持面向对象的编程.
要在你的程序中存储数据, 对象是非常有用的. 类允许你为一个对象声明一组特性.
当你从一个类创建对象时, 你就可以节省时间和精力, 因为你不需要每次都声明这些特性.
要声明一个类, 请使用 `class` 关键字:
```KOTLIN
class Customer
```
## 属性
可以在属性中声明一个类的对象的特性. 你可以为一个类声明属性:
* 放在类的名称之后的小括号 `()` 之内.
```KOTLIN
class Contact(val id: Int, var email: String)
```
* 放在大括号 `{}` 定义的类的 body 部之内.
```KOTLIN
class Contact(val id: Int, var email: String) {
val category: String = ""
}
```
除非在类的实例创建之后需要修改属性的值, 否则我们推荐将属性声明为只读的 (`val`).
在小括号内声明属性时, 你可以不使用 `val` 或 `var`, 但在实例创建之后, 这样的属性将不可访问.
Note:
* 包含在小括号 `()` 之内的内容称为 类头部(Class Header).
* 声明类的属性时, 你可以使用 [尾随逗号(Trailing Comma)](coding-conventions.html#trailing-commas).
和函数参数一样, 类的属性可以有默认值:
```KOTLIN
class Contact(val id: Int, var email: String = "example@gmail.com") {
val category: String = "work"
}
```
## 创建实例
要从一个类创建一个对象, 你需要使用 构造器(Constructor), 声明一个类的 实例.
默认情况下, Kotlin 会使用类头部(Class Header)中声明的参数, 自动创建一个构造器.
例如:
```KOTLIN
class Contact(val id: Int, var email: String)
fun main() {
val contact = Contact(1, "mary@gmail.com")
}
```
在上面的示例中:
* `Contact` 是一个类.
* `contact` 是 `Contact` 类的一个实例.
* `id` 和 `email` 是属性.
* `id` 和 `email` 和默认构造器一起, 用来创建 `contact`.
Kotlin 类可以有多个构造器, 包括你自己定义的构造器.
关于如何声明多个构造器, 详情请参见 [构造器](classes.html#constructors-and-initializer-blocks).
## 访问属性
要访问一个实例的属性, 请在实例名称之后加上点号 `.`, 然后写上属性名称:
```KOTLIN
class Contact(val id: Int, var email: String)
fun main() {
val contact = Contact(1, "mary@gmail.com")
// 打印属性的值: email
println(contact.email)
// mary@gmail.com
// 更新属性的值: email
contact.email = "jane@gmail.com"
// 打印属性的新值: email
println(contact.email)
// 输出结果为 jane@gmail.com
}
```
Tip:
要把属性的值拼接为字符串的一部分, 你可以使用字符串模板 (`$`).
例如:
```KOTLIN
println("Their email address is: ${contact.email}")
```
## 成员函数
除了声明属性作为一个对象的特性之外, 你还可以通过成员函数来定义一个对象的行为.
在 Kotlin 中, 成员函数必须在类的 body 部之内声明.
要调用一个实例上的成员函数,
请在实例名称之后加上点号 `.`, 然后写上函数名称.
例如:
```KOTLIN
class Contact(val id: Int, var email: String) {
fun printId() {
println(id)
}
}
fun main() {
val contact = Contact(1, "mary@gmail.com")
// 调用成员函数 printId()
contact.printId()
// 输出结果为 1
}
```
## 数据类
Kotlin 有 数据类(Data Class), 非常适合于存储数据.
数据类有和普通类一样的功能, 但它们还自动带有一些额外的成员函数.
这些成员函数可以将实例打印为易于阅读的字符串输出, 比较类的实例, 复制实例, 等等等等.
由于这些函数是自动存在的, 因此你不必耗费时间为每个类编写相同的样板代码(Boilerplate Code).
要声明一个数据类, 请使用关键字 `data`:
```KOTLIN
data class User(val name: String, val id: Int)
```
数据类的预先定义的成员函数中, 最有用的是:
| 函数 |描述 |
----------
| `toString()` |将类实例和它的属性打印为一个易于阅读的字符串. |
| `equals()` 或 `==` |比较一个类的实例. |
| `copy()` |创建一个类的实例, 从另一个实例复制, 一部分属性可以不同. |
关于这些函数的使用示例, 请参见以下小节:
* [打印为字符串](#print-as-string)
* [比较实例](#compare-instances)
* [复制实例](#copy-instance)
### 打印为字符串
要将一个类的实例打印为易于阅读的字符串, 你可以明确调用 `toString()` 函数,
或使用打印函数(`println()` 和 `print()`), 这些函数会自动为你调用 `toString()`:
```KOTLIN
data class User(val name: String, val id: Int)
fun main() {
//sampleStart
val user = User("Alex", 1)
// 自动使用 toString() 函数, 让输出结果易于阅读
println(user)
// 输出结果为 User(name=Alex, id=1)
//sampleEnd
}
```
这个功能在调试程序或创建 log 时, 非常有用.
### 比较实例
要比较数据类的实例, 请使用相等比较操作符 `==`:
```KOTLIN
data class User(val name: String, val id: Int)
fun main() {
//sampleStart
val user = User("Alex", 1)
val secondUser = User("Alex", 1)
val thirdUser = User("Max", 2)
// 比较 user 和 second user
println("user == secondUser: ${user == secondUser}")
// 输出结果为 user == secondUser: true
// 比较 user 和 third user
println("user == thirdUser: ${user == thirdUser}")
// 输出结果为 user == thirdUser: false
//sampleEnd
}
```
### 复制实例
要对一个数据类的实例创建一个完全相同的复制, 请对这个实例调用 `copy()` 函数.
要对一个数据类的实例创建一个复制, 并且 改变一部分属性, 请对这个实例调用 `copy()` 函数,
并 加上要替换的属性值, 作为函数的参数.
例如:
```KOTLIN
data class User(val name: String, val id: Int)
fun main() {
//sampleStart
val user = User("Alex", 1)
// 创建 user 的完全相同的复制
println(user.copy())
// 输出结果为 User(name=Alex, id=1)
// 创建 user 的复制, 但使用另一个 name: "Max"
println(user.copy("Max"))
// 输出结果为 User(name=Max, id=1)
// 创建 user 的复制, 但使用另一个 id: 3
println(user.copy(id = 3))
// 输出结果为 User(name=Alex, id=3)
//sampleEnd
}
```
创建一个实例的复制, 要比修改原来的实例更加安全,
因为你对复制品所做的任何操作, 不会影响到依赖于原来那个实例的其他代码.
关于数据类, 更多详情请参见 [数据类](data-classes.html).
本教程的最后一章是介绍 Kotlin 的 [Null 值安全性](kotlin-tour-null-safety.html).
## 实际练习
### 习题 1
定义一个数据类 `Employee`, 带有两个属性: 一个是姓名, 一个是工资.
请确保工资的属性是可变的, 否则你在年底就不可能涨工资了!
主函数演示你如何使用这个数据类.
```KOTLIN
// 在这里编写你的代码
fun main() {
val emp = Employee("Mary", 20)
println(emp)
emp.salary += 10
println(emp)
}
```
```KOTLIN
data class Employee(val name: String, var salary: Int)
fun main() {
val emp = Employee("Mary", 20)
println(emp)
emp.salary += 10
println(emp)
}
```
### 习题 2
为了让下面的代码能够编译, 声明所需要的数据类.
```KOTLIN
data class Person(val name: Name, val address: Address, val ownsAPet: Boolean = true)
// 在这里编写你的代码
// data class Name(...)
fun main() {
val person = Person(
Name("John", "Smith"),
Address("123 Fake Street", City("Springfield", "US")),
ownsAPet = false
)
}
```
```KOTLIN
data class Person(val name: Name, val address: Address, val ownsAPet: Boolean = true)
data class Name(val first: String, val last: String)
data class Address(val street: String, val city: City)
data class City(val name: String, val countryCode: String)
fun main() {
val person = Person(
Name("John", "Smith"),
Address("123 Fake Street", City("Springfield", "US")),
ownsAPet = false
)
}
```
### 习题 3
为了测试你的代码, 你需要一个生成器, 它能够创建随机的员工数据.
定义一个 `RandomEmployeeGenerator` 类, 其中包括可用的姓名的固定列表 (包含在类的 body 部之内).
还可以指定工资的最小值和最大值 (包含在类头部之内) 来配置这个类.
在类的 body 部之内, 定义 `generateEmployee()` 函数.
这次也一样, 主函数演示你如何使用这个类.
Tip:
在这个习题中, 你会导入一个包, 这样就可以使用 [Random.nextInt()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.random/-random/next-int.html) 函数.
关于包的导入, 更多详情请参见 [包(Package)与导入(Import)](packages.html).
提示 1
: List 有一个名为
: [.random()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/random.html)
: 的扩展函数,
它返回 List 内的一个随机元素.
提示 2
: `Random.nextInt(from = ..., until = ...)`
: 返回给你一个随机的
: `Int`
: 值, 它在指定的上下限值之内.
```KOTLIN
import kotlin.random.Random
data class Employee(val name: String, var salary: Int)
// 在这里编写你的代码
fun main() {
val empGen = RandomEmployeeGenerator(10, 30)
println(empGen.generateEmployee())
println(empGen.generateEmployee())
println(empGen.generateEmployee())
empGen.minSalary = 50
empGen.maxSalary = 100
println(empGen.generateEmployee())
}
```
```KOTLIN
import kotlin.random.Random
data class Employee(val name: String, var salary: Int)
class RandomEmployeeGenerator(var minSalary: Int, var maxSalary: Int) {
val names = listOf("John", "Mary", "Ann", "Paul", "Jack", "Elizabeth")
fun generateEmployee() =
Employee(names.random(),
Random.nextInt(from = minSalary, until = maxSalary))
}
fun main() {
val empGen = RandomEmployeeGenerator(10, 30)
println(empGen.generateEmployee())
println(empGen.generateEmployee())
println(empGen.generateEmployee())
empGen.minSalary = 50
empGen.maxSalary = 100
println(empGen.generateEmployee())
}
```
## 下一步
[Null 值安全性](kotlin-tour-null-safety.html)
# Null 值安全性
 [Hello world](kotlin-tour-hello-world.html)
 [基本类型](kotlin-tour-basic-types.html)
 [集合(Collection)](kotlin-tour-collections.html)
 [控制流](kotlin-tour-control-flow.html)
 [函数](kotlin-tour-functions.html)
 [类](kotlin-tour-classes.html)
 Null 值安全性
在 Kotlin 中, 可以使用 `null` 值. Kotlin 使用 `null` 值表示某些值不存在, 或者还未确定的情况.
在 [集合](kotlin-tour-collections.html#kotlin-tour-map-no-key) 章节中,
你已经看到了 Kotlin 返回 `null` 值的例子, 那就是当你使用 Map 中不存在的 key 来访问一个键值对(key-value pair) 的情况.
尽管这样的方式使用 `null` 值是很有用的, 但如果你的代码没有准备好处理 `null` 值, 就可能会发生问题.
为了帮助在程序中防止 `null` 值相关的问题, Kotlin 提供了 null 值安全性功能.
null 值安全性功能会在编译期检测 `null` 值潜在的问题, 而不是在运行期.
Null 安全性是多种功能的组合, 使得你能够:
* 如果你的程序允许 `null` 值, 可以明确声明.
* 检查 `null` 值.
* 对可能包含 `null` 值的属性或函数, 使用安全调用.
* 如果检测到 `null` 值时, 声明如何处理.
## 可为 null 的类型
Kotlin 支持可为 null 的类型, 这样的类型允许存在 `null` 值.
默认情况下, 一个类型 不能 接受 `null` 值.
声明可为 null 的类型的方法是, 在类型声明之后明确添加 `?`.
例如:
```KOTLIN
fun main() {
// neverNull 的类型为: String
var neverNull: String = "This can't be null"
// 这里会出现编译器错误
neverNull = null
// nullable 的类型为: 可以为 null 的 String
var nullable: String? = "You can keep a null here"
// 这是可以的
nullable = null
// 默认情况下, 不能接受 null 值
var inferredNonNull = "The compiler assumes non-nullable"
// 这里会出现编译器错误
inferredNonNull = null
// notNull 不能接受 null 值
fun strLength(notNull: String): Int {
return notNull.length
}
println(strLength(neverNull)) // 输出结果为 18
println(strLength(nullable)) // 这里会出现编译器错误
}
```
Tip:
`length` 是 [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/) 类的属性,
它表示字符串中字符的数量.
## 检查 null 值
你可以在条件表达式中检查 `null` 值.
在下面的示例中, `describeString()` 函数包含一个 `if` 语句,
它检查 `maybeString` 是不是 非 `null` 值, 并且它的 `length` 是否大于 0:
```KOTLIN
fun describeString(maybeString: String?): String {
if (maybeString != null && maybeString.length > 0) {
return "String of length ${maybeString.length}"
} else {
return "Empty or null string"
}
}
fun main() {
val nullString: String? = null
println(describeString(nullString))
// 输出结果为 Empty or null string
}
```
## 使用安全调用
对于可能包含 `null` 值的对象, 要安全的访问它的属性, 请使用安全调用操作符 `?.`.
如果对象或对象的属性为 `null`, 安全调用操作符会返回 `null`.
如果你想要在你的代码中避免 `null` 值造成的错误, 这个功能会很有用.
在下面的示例中, `lengthString()` 函数使用安全调用, 返回字符串的长度, 或返回 `null` 值:
```KOTLIN
fun lengthString(maybeString: String?): Int? = maybeString?.length
fun main() {
val nullString: String? = null
println(lengthString(nullString))
// null
}
```
Tip:
可以对安全调用使用链式调用, 如果一个对象的任何属性包含 `null` 值, 则会返回 `null`, 而不会抛出错误.
例如:
```KOTLIN
person.company?.address?.country
```
安全调用操作符也可以用来对扩展函数或成员函数进行安全调用.
这种情况下, 会在调用函数之前进行 null 值检查.
如果检测到 `null` 值, 那么会跳过函数调用, 返回 `null`.
在下面的示例中, `nullString` 是 `null` 值, 因此对 [.uppercase()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/uppercase.html)
的调用会被跳过, 并返回 `null`:
```KOTLIN
fun main() {
val nullString: String? = null
println(nullString?.uppercase())
// 输出结果为 null
}
```
## 使用 Elvis 操作符
你可以使用 Elvis 操作符 `?:`, 指定检测到 `null` 值时的默认返回值.
Elvis 操作符的左侧, 是需要检测 `null` 值的表达式.
Elvis 操作符的右侧, 是检测到 `null` 值时应该返回的默认值.
在下面的示例中, `nullString` 是 `null` 值, 因此访问 `length` 属性的安全调用返回 `null` 值.
因此 Elvis 操作符的结果是, 返回 `0`:
```KOTLIN
fun main() {
val nullString: String? = null
println(nullString?.length ?: 0)
// 输出结果为 0
}
```
关于 Kotlin 中的 Null 值安全性, 更多详情请参见 [Null 值安全性](null-safety.html).
## 实际练习
### 习题
你有一个 `employeeById` 函数, 可以用来访问一个公司的员工数据库.
但是, 这个函数返回 `Employee?` 类型的值, 因此结果可能为 `null`.
你的目标是编写一个函数, 如果给定了员工 `id`, 则返回员工的工资, 如果在数据库中没有找到这个员工, 则返回 `0`.
```KOTLIN
data class Employee (val name: String, var salary: Int)
fun employeeById(id: Int) = when(id) {
1 -> Employee("Mary", 20)
2 -> null
3 -> Employee("John", 21)
4 -> Employee("Ann", 23)
else -> null
}
fun salaryById(id: Int) = // 在这里编写你的代码
fun main() {
println((1..5).sumOf { id -> salaryById(id) })
}
```
```KOTLIN
data class Employee (val name: String, var salary: Int)
fun employeeById(id: Int) = when(id) {
1 -> Employee("Mary", 20)
2 -> null
3 -> Employee("John", 21)
4 -> Employee("Ann", 23)
else -> null
}
fun salaryById(id: Int) = employeeById(id)?.salary ?: 0
fun main() {
println((1..5).sumOf { id -> salaryById(id) })
}
```
## 下一步做什么?
恭喜! 现在你已经完成了我们的 Kotlin 观光之旅的初级教程, 下面请阅读我们的中级教程, 更加深入的理解 Kotlin:
[中级教程: 扩展函数](kotlin-tour-intermediate-extension-functions.html)
# 中级教程: 扩展函数
 扩展函数
 [作用域函数](kotlin-tour-intermediate-scope-functions.html)
 [带接受者的 Lambda 表达式](kotlin-tour-intermediate-lambdas-receiver.html)
 [类与接口](kotlin-tour-intermediate-classes-interfaces.html)
 [对象](kotlin-tour-intermediate-objects.html)
 [开放类与特殊类](kotlin-tour-intermediate-open-special-classes.html)
 [属性](kotlin-tour-intermediate-properties.html)
 [Null 值安全性](kotlin-tour-intermediate-null-safety.html)
 [库与 API](kotlin-tour-intermediate-libraries-and-apis.html)
在这一章中, 你将探索一些特殊的 Kotlin 函数, 它们能够让你的代码更加简洁, 更加易读.
学习这些函数能够如何帮助你使用高效的设计模式, 将你的项目提升到更高水平.
## 扩展函数
在软件开发中, 你经常需要修改一个程序的行为, 但又不能修改原来的源代码.
例如, 你可能想要向一个来自第三方库的类添加额外的功能.
你可以通过添加 扩展函数 来扩展一个类.
调用扩展函数的方式与调用类的成员函数一样, 使用点号 `.`.
在介绍扩展函数的完整语法之前, 你需要理解什么是 接受者(Receiver).
接受者(Receiver)是指函数对哪个对象调用. 换句话说, 接受者就是共享信息的来源.

在这个示例中, `main()` 函数调用 [.first()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/first.html) 函数, 得到列表中的第 1 个元素.
`.first()` 函数 对 `readOnlyShapes` 变量调用, 因此 `readOnlyShapes` 变量就是接受者.
要创建扩展函数, 请写下你想要扩展的类名称, 之后是一个 `.` 号, 之后是你的函数名称.
后面是函数声明的其余部分, 包括它的参数和返回类型.
例如:
```KOTLIN
fun String.bold(): String = "$this"
fun main() {
// "hello" 是接受者
println("hello".bold())
// 输出结果为: hello
}
```
在这个示例中:
* `String` 是被扩展的类.
* `bold` 是扩展函数的名称.
* `.bold()` 扩展函数的返回类型是 `String`.
* `"hello"`, 一个 `String` 实例, 是接受者.
* 在函数的 body 部, 访问接受者时使用了 [关键字](keyword-reference.html): `this`.
* 使用了字符串模板 (`$`) 来访问 `this` 的值.
* `.bold()` 扩展函数接受一个字符串, 并将它包含在 `` HTML 元素内返回, 用于显示粗体文字.
## 面向扩展的设计
你可以在任何地方定义扩展函数, 因此可以创建面向扩展的设计.
这样的设计将核心功能与便利但并非必须的功能分离开, 让你的代码易于阅读和维护.
一个很好的例子是 Ktor 库的 [HttpClient](https://api.ktor.io/ktor-client-core/io.ktor.client/-http-client/index.html) 类, 它帮助你执行网络请求.
它的核心功能是单个函数 `request()`, 它的参数是一个 HTTP 请求需要的所有信息 :
```KOTLIN
class HttpClient {
fun request(method: String, url: String, headers: Map): HttpResponse {
// 网络代码
}
}
```
在实际运用中, 最常用的 HTTP 请求是 GET 或 POST 请求. 库为这些常见的使用场景提供更短的名称是很合理的.
但是, 不需要编写新的网络代码, 只需要特定的请求调用.
换句话说, 这些请求很适用定义为单独的 `.get()` 和 `.post()` 扩展函数:
```KOTLIN
fun HttpClient.get(url: String): HttpResponse = request("GET", url, emptyMap())
fun HttpClient.post(url: String): HttpResponse = request("POST", url, emptyMap())
```
这些 `.get()` 和 `.post()` 函数扩展了 `HttpClient` 类.
由于它们是在 `HttpClient` 类的实例上调用的, 也就是使用 `HttpClient` 类的实例作为接受者, 因此它们可以直接使用来自 `HttpClient` 类的 `request()` 函数.
你可以通过这些扩展函数, 使用适当的 HTTP 方法调用 `request()` 函数, 这样可以简化你的代码, 让代码更加易于理解:
```KOTLIN
class HttpClient {
fun request(method: String, url: String, headers: Map): HttpResponse {
println("Requesting $method to $url with headers: $headers")
return HttpResponse("Response from $url")
}
}
fun HttpClient.get(url: String): HttpResponse = request("GET", url, emptyMap())
fun main() {
val client = HttpClient()
// 直接使用 request(), 发起 GET 请求
val getResponseWithMember = client.request("GET", "https://example.com", emptyMap())
// 使用 get() 扩展函数, 发起 GET 请求
// client 实例是接受者
val getResponseWithExtension = client.get("https://example.com")
}
```
在 Kotlin 的 [标准库](https://kotlinlang.org/api/latest/jvm/stdlib/) 和其他库中, 大量使用了这种面向扩展的方案.
例如, `String` 类有很多 [扩展函数](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/#extension-functions) 帮助你处理字符串.
关于扩展函数, 详情请参见 [扩展](extensions.html).
## 实际练习
### 习题 1
编写一个扩展函数, 名为 `isPositive`, 接受一个整数参数, 判断它是不是正数.
```KOTLIN
fun Int.// 请在这里编写你的代码
fun main() {
println(1.isPositive())
// 输出结果为: true
}
```
```KOTLIN
fun Int.isPositive(): Boolean = this > 0
fun main() {
println(1.isPositive())
// 输出结果为: true
}
```
### 习题 2
编写一个扩展函数, 名为 `toLowercaseString`, 接受一个字符串参数, 返回它的小写形式.
提示
: 使用
: `String`
: 类型的
: [.lowercase()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/lowercase.html)
: 函数.
```KOTLIN
fun // 请在这里编写你的代码
fun main() {
println("Hello World!".toLowercaseString())
// 输出结果为: hello world!
}
```
```KOTLIN
fun String.toLowercaseString(): String = this.lowercase()
fun main() {
println("Hello World!".toLowercaseString())
// 输出结果为: hello world!
}
```
## 下一步
[中级教程: 作用域函数](kotlin-tour-intermediate-scope-functions.html)
# 中级教程: 作用域函数(Scope Function)
 [扩展函数](kotlin-tour-intermediate-extension-functions.html)
 作用域函数
 [带接受者的 Lambda 表达式](kotlin-tour-intermediate-lambdas-receiver.html)
 [类与接口](kotlin-tour-intermediate-classes-interfaces.html)
 [对象](kotlin-tour-intermediate-objects.html)
 [开放类与特殊类](kotlin-tour-intermediate-open-special-classes.html)
 [属性](kotlin-tour-intermediate-properties.html)
 [Null 值安全性](kotlin-tour-intermediate-null-safety.html)
 [库与 API](kotlin-tour-intermediate-libraries-and-apis.html)
在这一章中, 你在对扩展函数的理解的基础之上, 学习如何使用作用域函数来编写更加符合 Kotlin 惯用法的代码.
## 作用域函数(Scope Function)
在编程中, 作用域(scope) 是指一个能够识别变量或对象的区域.
最常见的作用域是全局作用域和局部作用域:
* 全局作用域(Global Scope) – 能够从程序任何位置访问的变量或对象.
* 局部作用域(Local Scope) – 只能在定义它的代码块或函数之内访问的变量或对象.
在 Kotlin 中, 还有作用域函数(Scope Function), 能够围绕一个对象创建临时作用域, 并执行一些代码.
作用域函数能够让你的代码更加简洁, 因为在临时作用域内, 你不必引用你的对象的名称.
根据作用域函数不同, 你可以通过关键字 `this` 来引用对象, 或者通过关键字 `it`, 将它作为一个参数来访问.
Kotlin 共有 5 个作用域函数: `let`, `apply`, `run`, `also`, 和 `with`.
每个作用域函数接受一个 Lambda 表达式参数, 并返回对象, 或返回 Lambda 表达式的结构.
在这篇教程, 我们会解释每个作用域函数, 以及如何使用.
Tip:
你也可以观看视频 [回到标准库: 充分利用 Kotlin 的标准库](https://youtu.be/DdvgvSHrN9g?feature=shared&t=1511),
由 Sebastian Aigner(Kotlin 开发者 Advocate)讲解作用域函数.
### let
当你想要在代码中执行 null 值检查, 然后对返回的对象执行进一步操作, 可以使用 `let` 作用域函数.
看看这个示例:
```KOTLIN
fun sendNotification(recipientAddress: String): String {
println("Yo $recipientAddress!")
return "Notification sent!"
}
fun getNextAddress(): String {
return "sebastian@jetbrains.com"
}
fun main() {
val address: String? = getNextAddress()
sendNotification(address)
}
```
这个示例有 2 个函数:
* `sendNotification()`, 有一个函数参数 `recipientAddress`, 返回一个字符串.
* `getNextAddress()`, 没有函数参数, 返回一个字符串.
这个示例创建一个变量 `address`, 类型是可为 null 的 `String`.
但当你调用 `sendNotification()` 函数时这会成为问题, 因为这个函数要求 `address` 不能是 `null` 值.
结果是编译器会报告错误:
```TEXT
Argument type mismatch: actual type is 'String?', but 'String' was expected.
```
在初学者教程中, 你已经知道了可以使用 if 条件, 或使用 [Elvis 操作符 ?:](kotlin-tour-null-safety.html#use-elvis-operator), 执行 null 值检查.
但如果你想要在之后的代码中使用返回的对象, 应该怎么办?
你可以使用 if 条件 和 一个 else 分支来实现:
```KOTLIN
fun sendNotification(recipientAddress: String): String {
println("Yo $recipientAddress!")
return "Notification sent!"
}
fun getNextAddress(): String {
return "sebastian@jetbrains.com"
}
fun main() {
//sampleStart
val address: String? = getNextAddress()
val confirm = if(address != null) {
sendNotification(address)
} else { null }
//sampleEnd
}
```
但是, 更加简洁的方法是使用 `let` 作用域函数:
```KOTLIN
fun sendNotification(recipientAddress: String): String {
println("Yo $recipientAddress!")
return "Notification sent!"
}
fun getNextAddress(): String {
return "sebastian@jetbrains.com"
}
fun main() {
//sampleStart
val address: String? = getNextAddress()
val confirm = address?.let {
sendNotification(it)
}
//sampleEnd
}
```
这个示例中:
* 创建名为 `address` 和 `confirm` 的变量.
* 在 `address` 变量上, 对 `let` 作用域函数使用一个安全调用.
* 在 `let` 作用域函数之内, 创建一个临时作用域.
* 将 `sendNotification()` 函数作为一个 Lambda 表达式, 传递给 `let` 作用域函数.
* 使用临时作用域, 通过 `it` 引用 `address` 变量.
* 将结果赋值给 `confirm` 变量.
通过这种方式, 你的代码能够处理 `address` 变量可能为 `null` 值的情况, 而且你能够在之后的代码中使用
`confirm` 变量.
### apply
使用 `apply` 作用域函数, 能够在创建时而不是在之后的代码中初始化对象, 例如一个类实例.
这种方法能够让你的代码更加易于阅读和管理.
看看这个示例:
```KOTLIN
class Client() {
var token: String? = null
fun connect() = println("connected!")
fun authenticate() = println("authenticated!")
fun getData() : String {
println("getting data!")
return "Mock data"
}
}
val client = Client()
fun main() {
client.token = "asdf"
client.connect()
// 输出结果为: connected!
client.authenticate()
// 输出结果为: authenticated!
client.getData()
// 输出结果为: getting data!
}
```
这个示例有一个 `Client` 类, 包含一个属性, 名为 `token`,
以及 3 个成员函数: `connect()`, `authenticate()`, 和 `getData()`.
这个示例创建 `Client` 类的实例 `client`,
之后在 `main()` 函数中初始化它的 `token` 属性, 并调用它的成员函数.
尽管这个示例很小, 但在实际应用中, 在你创建一个类实例之后, 可能要经过一段时间才能配置和使用它(以及它的成员函数).
但是, 如果你使用 `apply` 作用域函数, 你就可以在代码的同一处, 创建, 配置, 并对你的类实例使用成员函数:
```KOTLIN
class Client() {
var token: String? = null
fun connect() = println("connected!")
fun authenticate() = println("authenticated!")
fun getData() : String {
println("getting data!")
return "Mock data"
}
}
//sampleStart
val client = Client().apply {
token = "asdf"
connect()
// 输出结果为: connected!
authenticate()
// 输出结果为: authenticated!
}
fun main() {
client.getData()
// 输出结果为: getting data!
}
//sampleEnd
```
这个示例中:
* 创建 `Client` 类的实例 `client`.
* 对 `client` 实例使用 `apply` 作用域函数.
* 在 `apply` 作用域函数之内创建一个临时作用域, 因此在访问它的属性或函数时, 你不必明确的引用 `client` 实例.
* 向 `apply` 作用域函数传递一个 Lambda 表达式, 它更新 `token` 属性, 并调用 `connect()` 和 `authenticate()` 函数.
* 在 `main()` 函数中, 对 `client` 实例调用 `getData()` 成员函数.
你可以看到, 当你处理大段代码时, 这种方法会很方便.
### run
与 `apply` 类似, 你可以使用 `run` 作用域函数来初始化一个对象,
但 `run` 最好的使用场景是, 在代码的某个特定时刻初始化一个对象, 并且 立即计算一个结果.
我们继续前面的 `apply` 函数示例, 但这一次你想要将 `connect()` 和 `authenticate()` 函数组合在一起,
使它们对每一个请求都会被调用.
例如:
```KOTLIN
class Client() {
var token: String? = null
fun connect() = println("connected!")
fun authenticate() = println("authenticated!")
fun getData() : String {
println("getting data!")
return "Mock data"
}
}
//sampleStart
val client: Client = Client().apply {
token = "asdf"
}
fun main() {
val result: String = client.run {
connect()
// 输出结果为: connected!
authenticate()
// 输出结果为: authenticated!
getData()
// 输出结果为: getting data!
}
}
//sampleEnd
```
这个示例中:
* 创建 `Client` 类的实例 `client`.
* 对 `client` 实例使用 `apply` 作用域函数.
* 在 `apply` 作用域函数之内创建一个临时作用域, 因此在访问它的属性或函数时, 你不必明确的引用 `client` 实例.
* 向 `apply` 作用域函数传递一个 Lambda 表达式, 它更新 `token` 属性.
`main()` 函数中:
* 创建一个 `result` 变量, 类型为 `String`.
* 对 `client` 实例使用 `run` 作用域函数.
* 在 `run` 作用域函数之内创建一个临时作用域, 因此在访问它的属性或函数时, 你不必明确的引用 `client` 实例.
* 向 `run` 作用域函数传递一个 Lambda 表达式, 它调用 `connect()`, `authenticate()`, 和 `getData()` 函数.
* 将结果赋值给 `result` 变量.
现在你可以在后续代码中使用返回的结果了.
### also
使用 `also` 作用域函数, 对一个对象完成一个额外的动作, 然后返回对象在代码中继续使用, 例如输出一个 log.
看看这个示例:
```KOTLIN
fun main() {
val medals: List = listOf("Gold", "Silver", "Bronze")
val reversedLongUppercaseMedals: List =
medals
.map { it.uppercase() }
.filter { it.length > 4 }
.reversed()
println(reversedLongUppercaseMedals)
// 输出结果为: [BRONZE, SILVER]
}
```
这个示例中:
* 创建 `medals` 变量, 包含一个字符串 List.
* 创建 `reversedLongUpperCaseMedals` 变量, 类型为 `List`.
* 对 `medals` 变量使用 `.map()` 扩展函数.
* 向 `.map()` 函数传递一个 Lambda 表达式, 它通过 `it` 关键字引用 `medals`, 并对它调用 `.uppercase()` 扩展函数.
* 对 `medals` 变量使用 `.filter()` 扩展函数.
* 向 `.filter()` 函数传递一个 Lambda 表达式, 作为判定条件, 它通过 `it` 关键字引用 `medals`, 并检查列表中的元素是否超过 4 个字符.
* 对 `medals` 变量使用 `.reversed()` 扩展函数.
* 将结果赋值给 `reversedLongUpperCaseMedals` 变量.
* 打印输出 `reversedLongUpperCaseMedals` 变量中包含的列表.
如果能在函数调用之间添加一些 log 会非常有用, 这样就可以看到 `medals` 变量发生了什么变化.
`also` 函数能够帮助我们实现这一点:
```KOTLIN
fun main() {
val medals: List = listOf("Gold", "Silver", "Bronze")
val reversedLongUppercaseMedals: List =
medals
.map { it.uppercase() }
.also { println(it) }
// 输出结果为: [GOLD, SILVER, BRONZE]
.filter { it.length > 4 }
.also { println(it) }
// 输出结果为: [SILVER, BRONZE]
.reversed()
println(reversedLongUppercaseMedals)
// 输出结果为: [BRONZE, SILVER]
}
```
现在, 在这个示例中:
* 对 `medals` 变量使用 `also` 作用域函数.
* 在 `also` 作用域函数之内创建一个临时作用域, 因此在将它用作函数参数时, 你不必明确的引用 `medals` 变量.
* 向 `also` 作用域函数传递一个 Lambda 表达式, 它调用 `println()` 函数, 通过 `it` 关键字, 使用 `medals` 变量作为函数参数.
由于 `also` 函数返回对象, 它不仅能够用于 log 输出, 还适合于调试, 链接多个操作, 以及执行其它不影响代码主体流程的副作用操作.
### with
与其它作用域函数不同, `with` 不是扩展函数, 因此语法不同.
你需要向 `with` 传递接受者对象作为参数.
当你想要对一个对象调用多个函数时, 可以使用 `with` 作用域函数.
看看这个示例:
```KOTLIN
class Canvas {
fun rect(x: Int, y: Int, w: Int, h: Int): Unit = println("$x, $y, $w, $h")
fun circ(x: Int, y: Int, rad: Int): Unit = println("$x, $y, $rad")
fun text(x: Int, y: Int, str: String): Unit = println("$x, $y, $str")
}
fun main() {
val mainMonitorPrimaryBufferBackedCanvas = Canvas()
mainMonitorPrimaryBufferBackedCanvas.text(10, 10, "Foo")
mainMonitorPrimaryBufferBackedCanvas.rect(20, 30, 100, 50)
mainMonitorPrimaryBufferBackedCanvas.circ(40, 60, 25)
mainMonitorPrimaryBufferBackedCanvas.text(15, 45, "Hello")
mainMonitorPrimaryBufferBackedCanvas.rect(70, 80, 150, 100)
mainMonitorPrimaryBufferBackedCanvas.circ(90, 110, 40)
mainMonitorPrimaryBufferBackedCanvas.text(35, 55, "World")
mainMonitorPrimaryBufferBackedCanvas.rect(120, 140, 200, 75)
mainMonitorPrimaryBufferBackedCanvas.circ(160, 180, 55)
mainMonitorPrimaryBufferBackedCanvas.text(50, 70, "Kotlin")
}
```
这个示例创建一个 `Canvas` 类, 有 3 个成员函数: `rect()`, `circ()`, 和 `text()`.
每个成员函数打印输出由你提供的函数参数构建的一个句子.
这个示例创建 `Canvas` 类的实例 `mainMonitorPrimaryBufferBackedCanvas`,
然后对这个实例, 使用不同的函数参数调用一系列的成员函数.
你可以看到, 这段代码很难阅读. 如果你使用 `with` 函数, 代码会变得非常精简:
```KOTLIN
class Canvas {
fun rect(x: Int, y: Int, w: Int, h: Int): Unit = println("$x, $y, $w, $h")
fun circ(x: Int, y: Int, rad: Int): Unit = println("$x, $y, $rad")
fun text(x: Int, y: Int, str: String): Unit = println("$x, $y, $str")
}
fun main() {
//sampleStart
val mainMonitorSecondaryBufferBackedCanvas = Canvas()
with(mainMonitorSecondaryBufferBackedCanvas) {
text(10, 10, "Foo")
rect(20, 30, 100, 50)
circ(40, 60, 25)
text(15, 45, "Hello")
rect(70, 80, 150, 100)
circ(90, 110, 40)
text(35, 55, "World")
rect(120, 140, 200, 75)
circ(160, 180, 55)
text(50, 70, "Kotlin")
}
//sampleEnd
}
```
这个示例中:
* 使用 `with` 作用域函数, 将 `mainMonitorSecondaryBufferBackedCanvas` 实例作为接受者.
* 在 `with` 作用域函数之内创建一个临时作用域, 因此在调用它的成员函数, 你不必明确的引用 `mainMonitorSecondaryBufferBackedCanvas` 实例.
* 向 `with` 作用域函数传递一个 Lambda 表达式, 使用不同的函数参数调用一系列的成员函数.
现在这段代码变得更加容易阅读了, 犯错误的可能性也更低了.
## 使用场景概述
本节介绍 Kotlin 中的各种作用域函数, 以及它们的主要使用场景, 目的是让你的代码更加符合 Kotlin 惯用法.
你可以将这个表作为一个快速参考.
需要注意的是, 要在你的代码中使用这些函数, 你并不需要完全理解它们如何工作.
| 函数 |访问 `x` 的方式 |返回值 |使用场景 |
-----------------------------
| `let` |`it` |Lambda 表达式的结果 |在你的代码中执行 null 值检查, 然后对返回的对象执行后续操作. |
| `apply` |`this` |`x` |在创建时初始化对象. |
| `run` |`this` |Lambda 表达式的结果 |在创建时初始化对象, 并 计算一个结果. |
| `also` |`it` |`x` |在返回对象之前进行额外的操作 . |
| `with` |`this` |Lambda 表达式的结果 |在一个对象上调用多个函数 . |
关于作用域函数, 详情请参见 [作用域函数](scope-functions.html).
## 实际练习
### 习题 1
将 `.getPriceInEuros()` 函数重写为一个单一表达式函数, 它使用安全调用操作符 `?.` 和 `let` 作用域函数.
提示
: 使用安全调用操作符
: `?.`
: 以便安全的访问
: `getProductInfo()`
: 函数的
: `priceInDollars`
: 属性.
然后, 使用
: `let`
: 作用域函数, 将
: `priceInDollars`
: 的值转换为欧元.
```KOTLIN
data class ProductInfo(val priceInDollars: Double?)
class Product {
fun getProductInfo(): ProductInfo? {
return ProductInfo(100.0)
}
}
// 请重写这个函数
fun Product.getPriceInEuros(): Double? {
val info = getProductInfo()
if (info == null) return null
val price = info.priceInDollars
if (price == null) return null
return convertToEuros(price)
}
fun convertToEuros(dollars: Double): Double {
return dollars * 0.85
}
fun main() {
val product = Product()
val priceInEuros = product.getPriceInEuros()
if (priceInEuros != null) {
println("Price in Euros: €$priceInEuros")
// 输出结果为: Price in Euros: €85.0
} else {
println("Price information is not available.")
}
}
```
```KOTLIN
data class ProductInfo(val priceInDollars: Double?)
class Product {
fun getProductInfo(): ProductInfo? {
return ProductInfo(100.0)
}
}
fun Product.getPriceInEuros() = getProductInfo()?.priceInDollars?.let { convertToEuros(it) }
fun convertToEuros(dollars: Double): Double {
return dollars * 0.85
}
fun main() {
val product = Product()
val priceInEuros = product.getPriceInEuros()
if (priceInEuros != null) {
println("Price in Euros: €$priceInEuros")
// 输出结果为: Price in Euros: €85.0
} else {
println("Price information is not available.")
}
}
```
### 习题 2
你有一个 `updateEmail()` 函数, 它更新一个用户的 EMail 地址.
使用 `apply` 作用域函数来更新 EMail 地址, 然后使用 `also` 作用域函数打印输出一个 log 消息: `Updating email for user with ID: ${it.id}`.
```KOTLIN
data class User(val id: Int, var email: String)
fun updateEmail(user: User, newEmail: String): User = // 请在这里编写你的代码
fun main() {
val user = User(1, "old_email@example.com")
val updatedUser = updateEmail(user, "new_email@example.com")
// 输出结果为: Updating email for user with ID: 1
println("Updated User: $updatedUser")
// 输出结果为: Updated User: User(id=1, email=new_email@example.com)
}
```
```KOTLIN
data class User(val id: Int, var email: String)
fun updateEmail(user: User, newEmail: String): User = user.apply {
this.email = newEmail
}.also { println("Updating email for user with ID: ${it.id}") }
fun main() {
val user = User(1, "old_email@example.com")
val updatedUser = updateEmail(user, "new_email@example.com")
// 输出结果为: Updating email for user with ID: 1
println("Updated User: $updatedUser")
// 输出结果为: Updated User: User(id=1, email=new_email@example.com)
}
```
## 下一步
[中级教程: 带接受者的 Lambda 表达式](kotlin-tour-intermediate-lambdas-receiver.html)
# 中级教程: 带接受者的 Lambda 表达式
 [扩展函数](kotlin-tour-intermediate-extension-functions.html)
 [作用域函数](kotlin-tour-intermediate-scope-functions.html)
 带接受者的 Lambda 表达式
 [类与接口](kotlin-tour-intermediate-classes-interfaces.html)
 [对象](kotlin-tour-intermediate-objects.html)
 [开放类与特殊类](kotlin-tour-intermediate-open-special-classes.html)
 [属性](kotlin-tour-intermediate-properties.html)
 [Null 值安全性](kotlin-tour-intermediate-null-safety.html)
 [库与 API](kotlin-tour-intermediate-libraries-and-apis.html)
在这一章中, 你将学习在另一种函数类型 Lambda 表达式中如何使用接受者,
以及它们如何帮助你创建一个特定领域专用语言(Domain-Specific Language, DSL).
## 带接受者的 Lambda 表达式
在初学者教程中, 你已经学习了如何使用 [Lambda 表达式](kotlin-tour-functions.html#lambda-expressions). Lambda 表达式也可以带有接受者.
这种情况下, Lambda 表达式能够访问接受者的任何成员函数或属性, 而不必每次都明确的指明接受者.
没有了这些额外的引用, 你的代码会变得更加易于阅读和维护.
Tip:
带接受者的 Lambda 表达式也叫做带接受者的函数字面值.
带接受者的 Lambda 表达式的语法与定义函数类型时不同.
首先, 请写下你想要扩展的接受者. 之后, 是一个 `.` 号, 之后写下你的函数类型定义的其它部分.
例如:
```KOTLIN
MutableList.() -> Unit
```
这个函数类型:
* 接受者是 `MutableList`.
* 括号 `()` 之内没有函数参数.
* 没有返回值: `Unit`.
我们来看看下面的实例, 它在画布上绘制图形:
```KOTLIN
class Canvas {
fun drawCircle() = println("🟠 Drawing a circle")
fun drawSquare() = println("🟥 Drawing a square")
}
// 带接受者的 Lambda 表达式定义
fun render(block: Canvas.() -> Unit): Canvas {
val canvas = Canvas()
// 使用带接受者的 Lambda 表达式
canvas.block()
return canvas
}
fun main() {
render {
drawCircle()
// 输出结果为: 🟠 Drawing a circle
drawSquare()
// 输出结果为: 🟥 Drawing a square
}
}
```
在这个示例中:
* `Canvas` 类有 2 个函数, 模拟绘制圆形和正方形.
* `render()` 函数接受一个 `block` 参数, 返回一个 `Canvas` 类的实例.
* `block` 参数是一个带接受者的 Lambda 表达式, 其中 `Canvas` 类是接受者.
* `render()` 函数创建一个 `Canvas` 类的实例, 并使用它作为接受者, 在 `canvas` 实例上调用 `block()` Lambda 表达式.
* `main()` 函数调用 `render()` 函数, 使用 Lambda 表达式, 传递给 `block` 参数.
* 在传递给 `render()` 函数的 Lambda 表达式内, 程序会在 `Canvas` 类的实例上调用 `drawCircle()` 和 `drawSquare()` 函数. 由于 `drawCircle()` 和 `drawSquare()` 函数是在带接受者的 Lambda 表达式之内调用, 因此可以象在 `Canvas` 类之内一样调用它们.
如果你想要创建一个特定领域专用语言(Domain-Specific Language, DSL), 带接受者的 Lambda 表达式会非常有用.
因为你可以访问接受者的成员函数和属性, 而不必明确引用接受者, 你的代码会变得更加精简.
为了演示这一点, 我们来考虑一个配置菜单中项目的示例.
我们从一个 `MenuItem` 类和一个 `Menu` 类开始, `Menu` 类包含一个向菜单中添加项目的函数, 名为 `item()`,
以及一个包含所有项目的列表, 名为 `items`:
```KOTLIN
class MenuItem(val name: String)
class Menu(val name: String) {
val items = mutableListOf
```
## 工作原理
假设你需要用 Kotlin 来实现一个类型安全的构建器.
首先, 要对你想要构建的东西定义一组模型. 在这个示例中, 需要对 HTML 标签建模.
这个任务很简单, 只需要定义一组对象就可以了.
比如, `HTML` 是一个类, 负责描述 `` 标签, 它可以定义子标签, 比如 `` 和 ``.
(这个类的具体定义请参见[下文](#full-definition-of-the-com-example-html-package).)
现在, 回忆一下为什么你可以写这样的代码:
```KOTLIN
html {
// ...
}
```
`html` 实际上是一个函数调用, 它接受一个 [Lambda 表达式](lambdas.html) 作为参数.
这个函数的定义如下:
```KOTLIN
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
```
这个函数只接受唯一一个参数, 名为 `init`, 这个参数本身又是一个函数.
其类型是 `HTML.() -> Unit`, 它是一个 带接受者的函数类型.
也就是说, 你应该向这个函数传递一个 `HTML` 的实例(一个 接收者)作为参数,
而且在函数内, 你可以调用这个实例的成员.
接受者可以通过 `this` 关键字来访问:
```KOTLIN
html {
this.head { ... }
this.body { ... }
}
```
(`head` 和 `body` 是 `HTML` 类的成员函数.)
现在, `this` 关键字可以省略, 通常都是如此, 省略之后你的代码就已经非常接近一个构建器了:
```KOTLIN
html {
head { ... }
body { ... }
}
```
那么, 这个函数调用做了什么? 我们来看看上面定义的 `html` 函数体.
首先它创建了一个 `HTML` 类的新实例, 然后它调用通过参数得到的函数, 来初始化这个 `HTML` 实例
(在这个示例中, 这个初始化函数对 `HTML` 实例调用了 `head` 和 `body` 方法), 然后, 这个函数返回这个 `HTML` 实例.
这正是构建器应该做的.
`HTML` 类中 `head` 和 `body` 函数的定义与 `html` 函数类似.
唯一的区别是, 这些函数会将自己创建的对象实例添加到自己所属的 `HTML` 实例的 `children` 集合中:
```KOTLIN
fun head(init: Head.() -> Unit): Head {
val head = Head()
head.init()
children.add(head)
return head
}
fun body(init: Body.() -> Unit): Body {
val body = Body()
body.init()
children.add(body)
return body
}
```
实际上这两个函数做的事情完全相同, 因此你可以编写一个泛型化的函数, 名为 `initTag`:
```KOTLIN
protected fun initTag(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}
```
然后, 这你的函数就变得很简单了:
```KOTLIN
fun head(init: Head.() -> Unit) = initTag(Head(), init)
fun body(init: Body.() -> Unit) = initTag(Body(), init)
```
现在你可以使用这两个函数来构建 `` 和 `` 标签了.
还需要讨论的一个问题是, 你要如何在标签内部添加文本.
在上面的示例程序中, 你写了这样的代码:
```KOTLIN
html {
head {
title {+"XML encoding with Kotlin"}
}
// ...
}
```
你所作的, 仅仅只是将一个字符串放在一个标签之内, 但在字符串之前有一个小小的 `+`,
所以, 它是一个函数调用, 被调用的是前缀操作符函数 `unaryPlus()`.
这个操作符实际上是由扩展函数 `unaryPlus()` 定义的,
这个扩展函数是抽象类 `TagWithText` 的成员 (这个抽象类是 `Title` 类的祖先类):
```KOTLIN
operator fun String.unaryPlus() {
children.add(TextElement(this))
}
```
所以, 前缀操作符 `+` 所作的, 是将一个字符串封装到 `TextElement` 的一个实例中,
然后将这个实例添加到 `children` 集合中, 然后这个字符串就会成为标签树中一个适当的部分.
以上所有类和函数都定义在 `com.example.html` 包中, 上面的构建器示例程序的最上部引入了这个包.
在最后一节中, 你可以读到这个包的完整定义.
## 控制接受者的作用范围: @DslMarker
使用 DSL 时, 可能遇到的一个问题就是, 当前上下文中存在太多可供调用的函数.
在 Lambda 表达式内, 你可以调用所有 [隐含接受者](lambdas.html#function-literals-with-receiver) 的所有方法,
因此造成一种不正确的结果, 比如一个 `head` 之内可以嵌套另一个 `head` 标签:
```KOTLIN
html {
head {
head {} // 应该禁止这样的调用
}
// ...
}
```
在这个示例中, 应该只允许调用离当前代码最近的隐含接受者 `this@head` 的成员函数;
`head()` 是更外层接受者 `this@html` 的成员函数,因此调用它应该是不允许的.
为了解决这个问题, 有一种特殊机制来控制接受者的作用范围.
要让编译器控制接受者的作用范围, 你只需要用一个相同的注解, 对 DSL 中用到的所有接受者的类型进行标注.
比如, 对 HTML 构建器你可以定义一个注解 `@HtmlTagMarker`:
```KOTLIN
@DslMarker
@Target(AnnotationTarget.CLASS)
annotation class HtmlTagMarker
```
如果对一个注解类标注了 `@DslMarker` 注解, 我们将它称作一个 DSL 标记.
`@Target` 注解限制了 `@HtmlTagMarker` 能够使用的范围.
DSL 标记只有在应用于以下目标时, 才会影响作用范围控制:
* 类型声明 (`CLASS`): 用作 DSL 接受者的类或接口.
* 类型使用 (`TYPE`): 在函数类型签名中的接受者类型.
* 类型别名 (`TYPEALIAS`): 对 DSL 接受者类型进行扩展的类型别名.
将 DSL 标记应用到其他目标 (例如函数或属性), 不会对作用范围控制造成影响.
Note:
关于 DSL 标记的工作方式, 详情请参见相应的 [KEEP 文档](https://github.com/Kotlin/KEEP/blob/main/notes/0005-dsl-marker.md).
在我们的 DSL 中, 所有的标签类都继承自相同的超类 `Tag`.
只需要对超类标注 `@HtmlTagMarker` 注解就够了,
然后 Kotlin 编译器会将所有的派生类都看作已被标注了同样的注解:
```KOTLIN
@HtmlTagMarker
abstract class Tag(val name: String) { ... }
```
你不必对 `HTML` 或 `Head` 类再标注 `@HtmlTagMarker` 注解, 因为它们的超类已经标注过了这个注解:
```KOTLIN
class HTML() : Tag("html") { ... }
class Head() : Tag("head") { ... }
```
标注这个注解之后, Kotlin 编译器就可以知道哪些隐含的接受者属于相同的 DSL, 因此编译器只允许代码调用离当前位置最近的接受者的成员函数:
```KOTLIN
html {
head {
head { } // 编译错误: 这是外层接受者的成员函数, 因此不允许在这里调用
}
// ...
}
```
注意, 如果确实需要调用外层接受者的成员函数, 仍然是可以实现的, 但这时你必须明确指定具体的接受者:
```KOTLIN
html {
head {
this@html.head { } // 仍然可以调用外层接受者的成员函数
}
// ...
}
```
你也可以直接对 [函数类型](lambdas.html#function-types) 使用 `@DslMarker` 注解.
这需要在注解的使用目标(Target)中包含 `AnnotationTarget.TYPE`:
```KOTLIN
@DslMarker
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
annotation class HtmlTagMarker
```
这样做之后, `@DslMarker` 注解就可以被用于函数类型了, 最常见的情况是用于带接受者的 Lambda 表达式.
例如:
```KOTLIN
fun html(init: @HtmlTagMarker HTML.() -> Unit): HTML { ... }
fun HTML.head(init: @HtmlTagMarker Head.() -> Unit): Head { ... }
fun Head.title(init: @HtmlTagMarker Title.() -> Unit): Title { ... }
```
当你调用这些函数时, 在标注了 `@DslMarker` 注解的 Lambda 表达式的 body 部中, 这个注解会限制对外层接受者的访问,
除非你明确的指明接受者:
```KOTLIN
html {
head {
title {
// 在这里, 会禁止访问外层接受者的 title, head 或其它函数.
}
}
}
```
在 Lambda 表达式内, 只有最内层的接受者的成员和扩展可以访问, 防止在嵌套的作用域之间发生意外的交互.
如果一个作用域中存在相同名称的隐含接受者成员和来自 [上下文参数](context-parameters.html) 的声明,
编译器会报告警告信息, 因为隐含接受者会被上下文参数遮盖.
要解决这个问题, 请使用 `this` 限定符, 明确的调用接受者, 或者使用 `contextOf()` 调用上下文声明:
```KOTLIN
interface HtmlTag {
fun setAttribute(name: String, value: String)
}
// 声明相同名称的顶层函数,
// 这个函数可以通过上下文参数访问
context(tag: HtmlTag)
fun setAttribute(name: String, value: String) { tag.setAttribute(name, value) }
fun test(head: HtmlTag, extraInfo: HtmlTag) {
with(head) {
// 在内层作用域中引入一个相同类型的上下文值
context(extraInfo) {
// 这里会出现警告:
// Uses an implicit receiver shadowed by a context parameter
setAttribute("user", "1234")
// 明确的调用接受者的成员
this.setAttribute("user", "1234")
// 明确的调用上下文声明
contextOf().setAttribute("user", "1234")
}
}
}
```
### com.example.html 包的完整定义
下面是 `com.example.html` 包的完整定义(但只包含上文示例程序使用到的元素).
它可以构建一个 HTML 树.
这段代码大量使用了 [扩展函数](extensions.html) 和 [带接受者的 Lambda 表达式](lambdas.html#function-literals-with-receiver).
```KOTLIN
package com.example.html
interface Element {
fun render(builder: StringBuilder, indent: String)
}
class TextElement(val text: String) : Element {
override fun render(builder: StringBuilder, indent: String) {
builder.append("$indent$text\n")
}
}
@DslMarker
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
annotation class HtmlTagMarker
@HtmlTagMarker
abstract class Tag(val name: String) : Element {
val children = arrayListOf()
val attributes = hashMapOf()
protected fun initTag(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}
override fun render(builder: StringBuilder, indent: String) {
builder.append("$indent<$name${renderAttributes()}>\n")
for (c in children) {
c.render(builder, indent + " ")
}
builder.append("$indent$name>\n")
}
private fun renderAttributes(): String {
val builder = StringBuilder()
for ((attr, value) in attributes) {
builder.append(" $attr=\"$value\"")
}
return builder.toString()
}
override fun toString(): String {
val builder = StringBuilder()
render(builder, "")
return builder.toString()
}
}
abstract class TagWithText(name: String) : Tag(name) {
operator fun String.unaryPlus() {
children.add(TextElement(this))
}
}
class HTML : TagWithText("html") {
fun head(init: Head.() -> Unit) = initTag(Head(), init)
fun body(init: Body.() -> Unit) = initTag(Body(), init)
}
class Head : TagWithText("head") {
fun title(init: Title.() -> Unit) = initTag(Title(), init)
}
class Title : TagWithText("title")
abstract class BodyTag(name: String) : TagWithText(name) {
fun b(init: B.() -> Unit) = initTag(B(), init)
fun p(init: P.() -> Unit) = initTag(P(), init)
fun h1(init: H1.() -> Unit) = initTag(H1(), init)
fun a(href: String, init: A.() -> Unit) {
val a = initTag(A(), init)
a.href = href
}
}
class Body : BodyTag("body")
class B : BodyTag("b")
class P : BodyTag("p")
class H1 : BodyTag("h1")
class A : BodyTag("a") {
var href: String
get() = attributes["href"]!!
set(value) {
attributes["href"] = value
}
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
```
# 通过构建器类型推断(Builder Type Inference)使用构建器
Kotlin 支持 构建器类型推断(Builder Type Inference) (或者叫构建器推断),
当你使用泛型构建器时, 这个功能可以很有用.
它能够帮助编译器, 通过构建器的 Lambda 表达式参数内的其它调用的类型信息, 推断出构建器调用的类型参数.
请参考下面的示例程序中对
[buildMap()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/build-map.html)
的使用:
```KOTLIN
fun addEntryToMap(baseMap: Map, additionalEntry: Pair?) {
val myMap = buildMap {
putAll(baseMap)
if (additionalEntry != null) {
put(additionalEntry.first, additionalEntry.second)
}
}
}
```
这里没有足够的类型信息来通过通常的方式推断类型参数, 但构建器推断能够分析 Lambda 表达式参数内的函数调用.
根据 `putAll()` 和 `put()` 调用的类型信息, 编译器可以自动将 `buildMap()` 调用的类型参数推断为 `String` 和 `Number`.
使用泛型构建器时, 构建器推断功能允许我们省略类型参数.
## 编写你自己的构建器
### 启用构建器推断的要求条件
Note:
在 Kotlin 1.7.0 以前, 对一个构建器函数启用构建器推断, 需要添加编译器选项 `-Xenable-builder-inference`.
在 1.7.0 中, 这个选项会默认启用.
要对你自己的构建器使用构建器推断, 请确认它的声明有一个构建器 Lambda 表达式参数, 类型为带接受者的函数类型.
对接受者类型还有 2 个要求:
1. 它应该使用构建器推断需要推断的那个类型参数. 比如:
```KOTLIN
fun buildList(builder: MutableList.() -> Unit) { ... }
```
Note:
注意, 直接传递类型参数的类型, 比如 `fun myBuilder(builder: T.() -> Unit)`, 目前还不支持.
2. 它应该提供 public 成员函数, 或扩展函数, 签名中包含对应的类型参数. 比如:
```KOTLIN
class ItemHolder {
private val items = mutableListOf()
fun addItem(x: T) {
items.add(x)
}
fun getLastItem(): T? = items.lastOrNull()
}
fun ItemHolder.addAllItems(xs: List) {
xs.forEach { addItem(it) }
}
fun itemHolderBuilder(builder: ItemHolder.() -> Unit): ItemHolder =
ItemHolder().apply(builder)
fun test(s: String) {
val itemHolder1 = itemHolderBuilder { // itemHolder1 的类型是 ItemHolder
addItem(s)
}
val itemHolder2 = itemHolderBuilder { // itemHolder2 的类型是 ItemHolder
addAllItems(listOf(s))
}
val itemHolder3 = itemHolderBuilder { // itemHolder3 的类型是 ItemHolder
val lastItem: String? = getLastItem()
// ...
}
}
```
### 支持的功能
构建器推断支持以下功能:
* 推断多个类型参数 ```KOTLIN fun myBuilder(builder: MutableMap.() -> Unit): Map { ... } ```
* 推断一个调用之内, 相互依赖的多个构建器 Lambda 表达式的类型参数 ```KOTLIN fun myBuilder( listBuilder: MutableList.() -> Unit, mapBuilder: MutableMap.() -> Unit ): Pair, Map> = mutableListOf().apply(listBuilder) to mutableMapOf().apply(mapBuilder) fun main() { val result = myBuilder( { add(1) }, { put("key", 2) } ) // result 的类型是 Pair, Map> } ```
* 推断 Lambda 表达式的参数或返回类型中出现的类型参数 ```KOTLIN fun myBuilder1( mapBuilder: MutableMap.() -> K ): Map = mutableMapOf().apply { mapBuilder() } fun myBuilder2( mapBuilder: MutableMap.(K) -> Unit ): Map = mutableMapOf().apply { mapBuilder(2 as K) } fun main() { // result1 推断得到的类型是 Map val result1 = myBuilder1 { put(1L, "value") 2 } val result2 = myBuilder2 { put(1, "value 1") // 你可以将 `it` 用作 "推迟类型变量" 类型 // 详情请参见以下章节 put(it, "value 2") } } ```
## 构建器推断的工作原理
### 推迟类型变量(Postponed Type Variable)
构建器推断使用 推迟类型变量(Postponed Type Variable), 在构建器推断分析时, 它出现在构建器的 Lambda 表达式之内.
一个推迟类型变量的类型是类型参数中的一个, 具体类型还在推断过程中.
编译器使用它来收集类型参数的类型信息.
我们来看看下面示例中的 [buildList()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/build-list.html):
```KOTLIN
val result = buildList {
val x = get(0)
}
```
这里 `x` 的类型是推迟类型变量: `get()` 调用返回一个类型 `E` 的值, 但 `E` 自身还未确定.
在这个时刻, 还不知道 `E` 的确定类型.
当一个推迟类型变量的值关联到一个确定的类型, 构建器推断会收集这个信息, 在构建器推断分析结束后, 推断对应的类型参数的结果类型.
比如:
```KOTLIN
val result = buildList {
val x = get(0)
val y: String = x
} // result 的类型推断为 List
```
在推迟类型变量赋值给一个 `String` 类型变量之后, 构建器推断得到信息, `x` 是 `String` 的子类型.
这个赋值是构建器 Lambda 表达式内的最后一条语句, 因此构建器推断分析结束, 结果是将类型参数 `E` 推断为 `String`.
注意, 你总是可以将推迟类型变量作为接受者, 调用 `equals()`, `hashCode()`, 和 `toString()` 函数.
### 向构建器推断结果贡献信息
构建器推断可以收集不同种类的类型信息, 这些信息都会贡献到分析结果.
它会考虑以下信息:
* 对 Lambda 表达式的接受者, 使用类型参数的类型调用方法 ```KOTLIN val result = buildList { // 根据传递的 "value" 参数, 类型参数被推断为 String add("value") } // result 的类型被推断为 List ```
* 对返回类型参数类型的调用, 指定期望的类型 ```KOTLIN val result = buildList { // 根据期待的类型, 类型参数被推断为 Float val x: Float = get(0) } // result 的类型被推断为 List ``` ```KOTLIN class Foo { val items = mutableListOf() } fun myBuilder(builder: Foo.() -> Unit): Foo = Foo().apply(builder) fun main() { val result = myBuilder { val x: List = items // ... } // result 的类型被推断为 Foo } ```
* 向期待确定类型的方法传递推迟类型变量的类型 ```KOTLIN fun takeMyLong(x: Long) { ... } fun String.isMoreThat3() = length > 3 fun takeListOfStrings(x: List) { ... } fun main() { val result1 = buildList { val x = get(0) takeMyLong(x) } // result1 的类型为 List val result2 = buildList { val x = get(0) val isLong = x.isMoreThat3() // ... } // result2 的类型为 List val result3 = buildList { takeListOfStrings(this) } // result3 的类型为 List } ```
* 取得一个指向 Lambda 表达式接受者的成员的可调用的引用 ```KOTLIN fun main() { val result = buildList { val x: KFunction1 = ::get } // result 的类型为 List } ``` ```KOTLIN fun takeFunction(x: KFunction1) { ... } fun main() { val result = buildList { takeFunction(::get) } // result 的类型为 List } ```
在分析结束后, 构建器推断考虑收集的所有类型信息, 尝试合并这些信息得到结果类型. 请看下面的示例.
```KOTLIN
val result = buildList { // 开始推断推迟类型变量 E
// 认为 E 是 Number 或 Number 的一个子类型
val n: Number? = getOrNull(0)
// 认为 E 是 Int 或 Int 的一个超类型
add(1)
// E 被推断为 Int
} // result 的类型为 List
```
结果类型是与分析过程中收集到的类型信息对应的最具体的类型.
如果给定的类型信息是发生矛盾, 无法合并, 编译器会报告错误.
注意, 只有在通常的类型推断无法推断类型参数时, Kotlin 编译器才会使用构建器推断.
也就是说, 你可以在构建器 Lambda 表达式之外贡献类型信息, 那么就不需要构建器推断分析了. 请看下面的示例:
```KOTLIN
fun someMap() = mutableMapOf()
fun MutableMap.f(x: MutableMap) { ... }
fun main() {
val x: Map = buildMap {
put("", "")
f(someMap()) // 类型不匹配 (要求 String 类型, 但实际是 CharSequence 类型)
}
}
```
这里会出现类型不匹配, 因为在构建器 Lambda 表达式之外指定了期待的 Map 类型.
编译器会使用固定的接受者类型 `Map` 来分析 Lambda 表达式内的所有的语句.
# 上下文参数(Context Parameter)
Tip:
上下文参数替代了旧的实验性功能 [上下文接受者(Context Receiver)](whatsnew1620.html#prototype-of-context-receivers-for-kotlin-jvm).
你可以在 [上下文参数的设计文档](https://github.com/Kotlin/KEEP/blob/master/proposals/context-parameters.md#summary-of-changes-from-the-previous-proposal) 中找到它们的主要差别.
要从上下文接受者迁移到上下文参数, 你可以使用 IntelliJ IDEA 中的辅助支持,
详情请参见相关的 [blog](https://blog.jetbrains.com/kotlin/2025/04/update-on-context-parameters/).
上下文参数(Context Parameter) 允许函数和属性声明在周围上下文(Surrounding Context)中隐含可用的依赖项.
使用上下文参数, 在一组函数调用中, 你就不需要手动的反复传递那些共用而且极少变更的值, 例如服务或依赖项.
要对属性和函数声明上下文参数, 请使用 `context` 关键字, 之后是参数列表, 每个参数声明为 `name: Type`.
下面是一个示例, 依赖于 `UserService` 接口:
```KOTLIN
// UserService 定义上下文中需要的依赖项
interface UserService {
fun log(message: String)
fun findUserById(id: Int): String
}
// 声明一个带有上下文参数的函数
context(users: UserService)
fun outputMessage(message: String) {
// 使用上下文中的 log
users.log("Log: $message")
}
// 声明一个带有上下文参数的属性
context(users: UserService)
val firstUser: String
// 使用上下文中的 findUserById
get() = users.findUserById(1)
fun main() {
val users = object : UserService {
override fun log(message: String) {
println(message)
}
override fun findUserById(id: Int): String {
return "User $id"
}
}
context(users) {
outputMessage("Looking up the first user")
println(firstUser)
// 输出结果为: User 1
}
}
```
可以使用 `_` 作为上下文参数的名称.
这种情况下, 参数值可以用来解析, 但在代码段内不能通过名称访问:
```KOTLIN
// 使用 "_" 作为上下文参数名称
context(_: UserService)
fun logWelcome() {
// 解析结果仍然能够从 UserService 找到适当的 log 函数
outputMessage("Welcome!")
}
```
## 上下文参数的解析
Kotlin 通过在当前的范围(Scope) 中搜索匹配的上下文值, 在调用端解析上下文参数.
Kotlin 会根据它们的类型进行匹配.
如果在同一个范围层级存在多个兼容的值, 编译器会报告歧义:
```KOTLIN
// UserService 定义上下文中需要的依赖项
interface UserService {
fun log(message: String)
}
// 声明一个带有上下文参数的函数
context(users: UserService)
fun outputMessage(message: String) {
users.log("Log: $message")
}
fun main() {
// 实现 UserService
val serviceA = object : UserService {
override fun log(message: String) = println("A: $message")
}
// 实现 UserService
val serviceB = object : UserService {
override fun log(message: String) = println("B: $message")
}
// 在调用端, serviceA 和 serviceB 都匹配期望的 UserService 类型
context(serviceA, serviceB) {
// 这会导致歧义错误
outputMessage("This will not compile")
}
}
```
### 明确的传递上下文参数
当多个函数重载的区别仅仅只是上下文参数不同, 如果存在多个匹配的上下文值, 那么函数调用就会发生歧义.
为了解决这种歧义, 请在调用处明确指定上下文参数:
```KOTLIN
class EmailSender
class SmsSender
context(emailSender: EmailSender)
fun sendNotification() {
println("Sent email notification")
}
context(smsSender: SmsSender)
fun sendNotification() {
println("Sent SMS notification")
}
context(defaultEmailSender: EmailSender, defaultSmsSender: SmsSender)
fun notifyUser() {
// 选择使用 EmailSender 上下文参数的重载函数
sendNotification(emailSender = defaultEmailSender)
// 选择使用 SmsSender 上下文参数的重载函数
sendNotification(smsSender = defaultSmsSender)
}
```
你也可以使用明确的上下文参数, 在某些函数调用中减少嵌套:
* 对单个调用, 使用明确的上下文参数, 可以让调用更加易读.
* 如果多个调用使用相同的上下文参数, 请使用 `context()` 函数.
这个功能是 [实验性功能](components-stability.html#stability-levels-explained).
要表示使用者同意(Opt-in), 请向你的构建脚本文件添加以下编译器选项:
Gradle:
```KOTLIN
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xexplicit-context-arguments")
}
}
```
Maven:
```XML
org.jetbrains.kotlin
kotlin-maven-plugin
-Xexplicit-context-arguments
```
## 限制
上下文参数还在不断改进中, 目前的一些限制包括:
* 构造器不能声明上下文参数.
* 上下文参数的属性不能拥有后端域变量(Backing Field), 也不能拥有初始化器.
* 带上下文参数的属性 不能使用委托.
尽管存在这些限制, 上下文参数通过简化依赖项注入, 改进 DSL 设计, 以及范围操作, 简化了依赖的管理.
# 内联函数(Inline Function)
使用 [高阶函数](lambdas.html) 在运行时会带来一些不利: 每个函数都是一个对象, 而且它还要捕获一个闭包,
闭包是指一个环境范围, 在这个范围内, 函数体内部可以访问外层变量.
内存占用(函数对象和类都会占用内存) 以及虚方法调用都会带来运行时的消耗.
但在很多情况下, 通过将 Lambda 表达式内联在使用处, 可以消除这些运行时消耗.
下文中的函数就是很好的例子. `lock()` 函数可以很容易地内联在调用处.
看看下面的例子:
```KOTLIN
lock(l) { foo() }
```
编译器可以直接产生下面的代码, 而不必为参数创建函数对象, 然后再调用这个参数指向的函数:
```KOTLIN
l.lock()
try {
foo()
} finally {
l.unlock()
}
```
为了让编译器做到这点, 需要对 `lock()` 函数标记 `inline` 修饰符:
```KOTLIN
inline fun lock(lock: Lock, body: () -> T): T { ... }
```
`inline` 修饰符既会影响到函数本身, 也影响到传递给它的 Lambda 表达式: 这两者都会被内联到调用处.
函数内联也许会导致编译产生的代码尺寸变大.
但只要你合理的使用(不要内联太大的函数), 就可以换来性能的提高, 尤其是在循环内发生的 "megamorphic" 函数调用.
(译注: 关于 megamorphic 请参见 [Inline caching](https://en.wikipedia.org/wiki/Inline_caching#Megamorphic_inline_caching))
## noinline
如果一个内联函数的参数中有多个 Lambda 表达式, 而你只希望内联其中的一部分,
可以对函数的一部分参数添加 `noinline` 修饰符:
```KOTLIN
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { ... }
```
可内联的 Lambda 表达式只能在内联函数内部调用, 或者再作为可内联的参数传递给其他函数,
而 `noinline` 的 Lambda 表达式可以按照你喜欢的方式任意使用:
可以保存在域内, 也可以当作参数传递, 等等.
Note:
如果一个内联函数不存在可以内联的函数类型参数, 而且没有 [实体化的类型参数](#reified-type-parameters),
编译器将会产生一个警告, 因为将这样的函数内联不太可能带来任何益处.
(如果你确信需要内联, 可以使用 `@Suppress("NOTHING_TO_INLINE")` 注解关闭这个警告)
## 非局部(non-local)的跳转表达式
### return
在 Kotlin 中, 使用无限定符的通常的 `return` 语句, 只能用来退出一个有名称的函数, 或匿名函数.
要退出一个 Lambda 表达式, 可以使用一个 [标签](returns.html#return-to-labels).
在 Lambda 表达式内禁止使用无标签的 `return`, 因为 Lambda 表达式不允许强制包含它的函数 `return`:
```KOTLIN
fun ordinaryFunction(block: () -> Unit) {
println("hi!")
}
//sampleStart
fun foo() {
ordinaryFunction {
return // 错误: 这里不允许让 `foo` 函数返回
}
}
//sampleEnd
fun main() {
foo()
}
```
但是, 如果 Lambda 表达式被传递去的函数是内联函数, 那么 `return` 语句也可以内联, 因此 `return` 是允许的:
```KOTLIN
inline fun inlined(block: () -> Unit) {
println("hi!")
}
//sampleStart
fun foo() {
inlined {
return // OK: 这里的 Lambda 表达式是内联的
}
}
//sampleEnd
fun main() {
foo()
}
```
这样的 `return` 语句(位于 Lambda 表达式内部, 但是退出包含 Lambda 表达式的函数) 称为 非局部(non-local) 返回.
这样的结构经常出现在循环中, 而循环也常常就是包含内联函数的地方:
```KOTLIN
fun hasZeros(ints: List): Boolean {
ints.forEach {
if (it == 0) return true // 从 hasZeros 函数返回
}
return false
}
```
注意, 有些内联函数可能并不在自己的函数体内直接调用传递给它的 Lambda 表达式参数, 而是通过另一个执行环境来调用,
比如通过一个局部对象, 或者一个嵌套函数. 这种情况下, 在 Lambda 表达式内, 非局部的控制流同样是禁止的.
为了标识内联函数的 Lambda 表达式参数不能使用非局部(non-local)返回,
需要对 Lambda 表达式参数添加 `crossinline` 修饰符:
```KOTLIN
inline fun f(crossinline body: () -> Unit) {
val f = object: Runnable {
override fun run() = body()
}
// ...
}
```
### break 和 continue
与非局部的(non-local) `return` 类似, 对于包含循环的内联函数, 在作为参数传递给内联函数的 Lambda 表达式中,
你也可以使用 `break` 和 `continue` [跳转表达式](returns.html):
```KOTLIN
fun processList(elements: List): Boolean {
for (element in elements) {
val variable = element.nullableMethod() ?: run {
log.warning("Element is null or invalid, continuing...")
continue
}
if (variable == 0) return true
}
return false
}
```
## 实体化的类型参数(Reified type parameter)
有些时候你需要访问作为参数传递来的类型:
```KOTLIN
fun TreeNode.findParentOfType(clazz: Class): T? {
var p = parent
while (p != null && !clazz.isInstance(p)) {
p = p.parent
}
@Suppress("UNCHECKED_CAST")
return p as T?
}
```
这里, 你向上遍历一颗树, 然后使用反射来检查节点是不是某个特定的类型.
这些都没问题, 但这个函数的调用代码不太漂亮:
```KOTLIN
treeNode.findParentOfType(MyTreeNode::class.java)
```
更好的解决方案是简单地将一个类型传递给这个函数, 可以像这样调用它:
```KOTLIN
treeNode.findParentOfType()
```
为了达到这个目的, 内联函数支持 实体化的类型参数(reified type parameter), 使用这个功能你可以将代码写成:
```KOTLIN
inline fun TreeNode.findParentOfType(): T? {
var p = parent
while (p != null && p !is T) {
p = p.parent
}
return p as T?
}
```
上面的代码给类型参数添加了 `reified` 修饰符, 使得它可以在函数内部访问, 就好像它是一个普通的类一样.
由于函数是内联的, 因此不必使用反射, 而且通常的操作符都可以使用, 比如 `!is` 和 `as`.
此外, 你可以通过上面提到那种方式来调用这个函数: `myTree.findParentOfType()`.
虽然很多情况下并不需要, 但你仍然可以对一个实体化的类型参数使用反射:
```KOTLIN
inline fun membersOf() = T::class.members
fun main(s: Array) {
println(membersOf().joinToString("\n"))
}
```
通常的函数(没有使用 inline 标记的) 不能够使用实体化的类型参数.
一个没有运行时表现的类型(比如, 一个没有实体化的类型参数, 或者一个虚拟类型, 比如 `Nothing`) 不可以用作实体化的类型参数.
## 内联属性(Inline property)
对于不存在 [后端域变量(Backing Field)](properties.html#backing-fields) 的属性, 可以对它的取值和设值方法使用 `inline` 修饰符.
你可以标识单个的属性取值/设值方法:
```KOTLIN
val foo: Foo
inline get() = Foo()
var bar: Bar
get() = ...
inline set(v) { ... }
```
也可以标注整个属性, 等于将它的取值和设值方法都标注为 `inline`:
```KOTLIN
inline var bar: Bar
get() = ...
set(v) { ... }
```
属性取值/设值方法被标注为 `inline` 后, 会被内联到调用处, 就像通常的内联函数一样.
## 对 Public API 内联函数的限制
当一个内联函数是 `public` 或 `protected` 的, 但不属于 `private` 或 `internal` 类型的一部分,
这个函数将被认为是一个 [模块(module)](visibility-modifiers.html#modules) 的 Public API.
它可以在其它模块中调用, 并且被内联到调用处.
假如内联函数的定义模块发生了变化, 而调用它的模块没有重新编译, 这时就可能会造成二进制代码不兼容的风险.
为了解决由模块中的 非-public API 变更带来的不兼容性,
Public API 内联函数的函数体部分, 不允许使用 非-Public-API, 也就是, 定义为 `private` 和 `internal` 的部分.
定义为 `internal` 的元素也可以使用 `@PublishedApi` 注解, 这就允许它被 Public API 内联函数使用.
当 `internal` 内联函数标注为 `@PublishedApi` 时, 也会像 Public API 内联函数一样检查它的函数体.
# 操作符重载
Kotlin 允许你对数据类型的一组预定义的操作符提供自定义的实现函数.
这些操作符有预定义的表达符号(比如 `+` 或 `*`), 以及预定义的优先顺序.
要实现这些操作符, 需要对相应的数据类型实现一个特定名称的 [成员函数](functions.html#member-functions) 或 [扩展函数](extensions.html),
这里的数据类型, 对于二元操作符, 是指左侧操作数的类型, 对于一元操作符, 是指唯一一个操作数的类型.
要重载操作符, 要对相应的函数使用 `operator` 修饰符.
```KOTLIN
interface IndexedContainer {
operator fun get(index: Int)
}
```
如果在后代类中 [重载](inheritance.html#overriding-methods) 操作符, 可以省略 `operator`:
```KOTLIN
class OrdersList: IndexedContainer {
override fun get(index: Int) { /*...*/ }
}
```
## 一元操作符
### 一元前缀操作符
| 表达式 |翻译为 |
------------
| `+a` |`a.unaryPlus()` |
| `-a` |`a.unaryMinus()` |
| `!a` |`a.not()` |
上表告诉我们说, 当编译器处理一元操作符时, 比如表达式 `+a`, 它将执行以下步骤:
* 确定 `a` 的类型, 假设为 `T`.
* 查找带有 `operator` 修饰符, 无参数的 `unaryPlus()` 函数, 而且函数的接受者类型为 `T`, 也就是说, `T` 类型的成员函数或扩展函数.
* 如果这个函数不存在, 或者找到多个, 则认为是编译错误.
* 如果这个函数存在, 并且返回值类型为 `R`, 则表达式 `+a` 的类型为 `R`.
Note:
这些操作符, 以其其它所有操作符, 都对 [基本类型](types-overview.html) 进行了优化,
因此不会发生函数调用, 并由此产生性能损耗.
举例来说, 我们可以这样来重载负号操作符:
```KOTLIN
data class Point(val x: Int, val y: Int)
operator fun Point.unaryMinus() = Point(-x, -y)
val point = Point(10, 20)
fun main() {
println(-point) // 打印结果为 "Point(x=-10, y=-20)"
}
```
### 递增与递减操作符
| 表达式 |翻译为 |
------------
| `a++` |`a.inc()` (参见下文) |
| `a--` |`a.dec()` (参见下文) |
`inc()` 和 `dec()` 函数必须返回一个值, 这个返回值将会赋值给使用 `++` 或 `--` 操作符的对象变量.
这两个函数不应该改变调用 `inc` 或 `dec` 函数的对象的内容.
对于 后缀 形式操作符, 比如 `a++`, 编译器解析时将执行以下步骤:
* 确定 `a` 的类型, 假设为 `T`.
* 查找带有 `operator` 修饰符, 无参数的 `inc()` 函数, 而且函数的接受者类型为 `T`.
* 检查函数的返回值类型是不是`T` 的子类型.
计算这个表达式所造成的影响是:
* 将 `a` 的初始值保存到临时变量 `a0` 中.
* 将 `a0.inc()` 的结果赋值给 `a`.
* 返回 `a0`, 作为表达式的计算结果值.
对于 `a--`, 计算步骤完全类似.
对于 前缀 形式的操作符 `++a` 和 `--a`, 解析过程是一样的, 计算表达式所造成的影响是:
* 将 `a.inc()` 的结果赋值给 `a`.
* 返回 `a` 的新值, 作为表达式的计算结果值.
## 二元操作符
### 算数操作符
| 表达式 |翻译为 |
------------
| `a + b` |`a.plus(b)` |
| `a - b` |`a.minus(b)` |
| `a * b` |`a.times(b)` |
| `a / b` |`a.div(b)` |
| `a % b` |`a.rem(b)` |
| `a..b` |`a.rangeTo(b)` |
| `a.. b` |`a.compareTo(b) > 0` |
| `a < b` |`a.compareTo(b) < 0` |
| `a >= b` |`a.compareTo(b) >= 0` |
| `a <= b` |`a.compareTo(b) <= 0` |
所有的比较操作符都被翻译为对 `compareTo` 函数的调用, 这个函数的返回值必须是 `Int` 类型.
### 属性委托操作符
关于 `provideDelegate`, `getValue` 和 `setValue` 操作符函数, 请参见 [委托属性](delegated-properties.html).
## 对命名函数的中缀式调用
使用 [中缀式函数调用](functions.html#infix-notation), 你可以模拟自定义的中缀操作符.
# 未使用的返回值检查器
Note:
这个功能计划在未来的 Kotlin 版本中进入稳定版, 并继续改进.
欢迎在我们的问题追踪系统 [YouTrack](https://youtrack.jetbrains.com/issue/KT-12719) 中提供反馈意见.
更多信息, 请参见相关的 [KEEP 提案](https://github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0412-unused-return-value-checker.md).
未使用的返回值检查器可以检测 被忽略的结果.
这些值是从表达式中返回的, 其返回类型不是 `Unit`, `Nothing`, 或 `Nothing?`, 并且没有被:
* 存储到变量或属性中.
* 被返回或抛出.
* 作为参数传递给另一个函数.
* 在调用或安全调用中用作接收者.
* 在 `if`, `when`, 或 `while` 等条件中检查.
* 用作 Lambda 表达式的最后一条语句.
对于 `++` 和 `--` 等递增操作, 以及右侧退出当前函数的布尔快捷方式(例如 `condition || return`),
检查器不会报告被忽略的结果.
你可以使用未使用的返回值检查器来捕获 bug, 即函数调用产生了有意义的结果, 但结果被悄悄的丢弃.
这有助于防止预想之外的行为, 使此类问题更容易追踪.
以下是一个示例, 其中创建了一个字符串但从未使用, 因此检查器将其报告为被忽略的结果:
```KOTLIN
fun formatGreeting(name: String): String {
if (name.isBlank()) return "Hello, anonymous user!"
if (!name.contains(' ')) {
// 检查器报告一个警告, 说明这个结果被忽略了:
// "Unused return value of 'plus'."
"Hello, " + name.replaceFirstChar(Char::titlecase) + "!"
}
val (first, last) = name.split(' ')
return "Hello, $first! Or should I call you Dr. $last?"
}
```
## 配置未使用的返回值检查器
你可以使用 `-Xreturn-value-checker` 编译器选项, 控制编译器如何报告被忽略的结果.
它有以下几种模式:
* `disable`: 禁用未使用的返回值检查器(这是默认值).
* `check`: 启用检查器, 并对来自 [已标记函数](#mark-functions-to-check-ignored-results) 的被忽略结果报告警告.
* `full`: 启用检查器, 将项目中的所有函数视为 [已标记](#mark-functions-to-check-ignored-results), 并对被忽略结果报告警告.
Note:
所有已标记函数会被相应的传播, 如果一个项目将你的代码作为依赖项, 并且启用了检查器, 则会报告被忽略的结果.
要在项目中使用未使用的返回值检查器, 请将编译器选项添加到构建配置文件中:
Gradle:
```KOTLIN
// build.gradle(.kts)
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xreturn-value-checker=check")
}
}
```
Maven:
```XML
org.jetbrains.kotlin
..
-Xreturn-value-checker=check
```
## 标记函数, 检查被忽略的结果
将 [-Xreturn-value-checker 编译器选项](#configure-the-unused-return-value-checker) 设置为 `check` 时,
检查器只对已标记的表达式报告被忽略的结果, 例如 Kotlin 标准库中的大多数函数.
要标记你自己的代码,
请使用 [@MustUseReturnValues](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-must-use-return-value/) 注解.
你可以将它应用于文件, 类, 或函数, 具体取决于你希望检查器覆盖的范围.
例如, 你可以标记整个文件:
```KOTLIN
// 标记这个文件中的所有函数和类, 使检查器报告未使用的返回值
@file:MustUseReturnValues
package my.project
fun someFunction(): String
```
或者标记特定的类:
```KOTLIN
// 标记这个类中的所有函数, 使检查器报告未使用的返回值
@MustUseReturnValues
class Greeter {
fun greet(name: String): String = "Hello, $name"
}
fun someFunction(): Int = ...
```
Note:
你可以将 `-Xreturn-value-checker` 编译器选项设置为 `full`, 将检查器应用于整个项目.
使用这个选项时, 不必用 `@MustUseReturnValues` 来注解你的代码.
## 抑制对被忽略结果的报告
你可以使用 [@IgnorableReturnValue](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-ignorable-return-value/)
注解, 抑制对特定函数的报告.
对那些忽略结果是常见情况和预期情况的函数, 请添加注解, 例如 `MutableList.add`:
```KOTLIN
@IgnorableReturnValue
fun MutableList.addAndIgnoreResult(element: T): Boolean {
return add(element)
}
```
你可以在不注解函数本身的情况下抑制警告.
方法是, 使用下划线语法 (`_`) 将结果赋值给一个特殊的无名变量:
```KOTLIN
// 不允许忽略的函数
fun computeValue(): Int = 42
fun main() {
// 报告警告: 结果被忽略
computeValue()
// 使用特殊的未使用变量, 仅对这个调用抑制警告
val _ = computeValue()
}
```
### 函数覆盖中的被忽略结果
当你覆盖一个函数时, 覆盖函数会继承基类声明上注解所定义的报告规则.
这个规则同样适用于基类声明是 Kotlin 标准库或其他库依赖项的一部分的情况,
因此检查器会对像 `Any.hashCode()` 这样的函数的覆盖函数, 报告被忽略结果.
此外, 你不能用另一个 [要求使用其返回值](#mark-functions-to-check-ignored-results) 的函数
来覆盖标记了 `@IgnorableReturnValue` 的函数.
但是, 如果结果可以安全的忽略, 你可以在标注了 `@MustUseReturnValues` 的类或接口中,
用 `@IgnorableReturnValue` 标记覆盖函数:
```KOTLIN
@MustUseReturnValues
interface Greeter {
fun greet(name: String): String
}
object SilentGreeter : Greeter {
@IgnorableReturnValue
override fun greet(name: String): String = ""
}
fun check(g: Greeter) {
// 报告警告: 未使用的返回值
g.greet("John")
// 没有警告
SilentGreeter.greet("John")
}
```
## 检查高阶函数中的未使用结果
一些高阶函数, 例如 `let` 作用域函数, 会返回 Lambda 表达式的结果.
要检查高阶函数的未使用的 Lambda 表达式结果, 请将 [实验性的](components-stability.html#stability-levels-explained)
`returnsResultOf()` 契约添加到函数的契约中.
Warning:
Kotlin 契约是实验性功能. 要选择使用者同意(Opt-in), 请在声明带有契约的函数时,
添加 `@OptIn(ExperimentalContracts::class)` 注解.
以下是一个示例:
```KOTLIN
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
inline fun T.customLet(block: (T) -> R): R {
contract {
returnsResultOf(block)
}
return block(this)
}
```
然后, 你可以使用带有这个契约的函数, 例如 `.customLet()`, 来检查 Lambda 结果是否被使用:
```KOTLIN
fun handleNullablePackageName(packageName: String?, builder: StringBuilder) {
// 检查器不报告警告, 因为 append() 的返回值可以被忽略
packageName?.customLet { builder.append(it) }
// 检查器报告警告, 因为返回的字符串未被使用
packageName?.customLet { "kotlin.$it" }
}
```
Warning:
`returnsResultOf()` 契约需要单独的编译器选项才能选择启用.
请注意, 使用它会产生预发布的(Pre-Release)二进制文件, 2.4.0 版本之前的 Kotlin 编译器无法读取这些文件.
要对你的项目标注使用者同意(Opt-in), 请将以下编译器选项添加到构建文件中:
Gradle:
```KOTLIN
// build.gradle(.kts)
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xallow-returns-result-of")
}
}
```
Maven:
```XML
org.jetbrains.kotlin
kotlin-maven-plugin
-Xallow-returns-result-of
```
## 与 Java 注解的互操作性
一些 Java 库使用不同注解实现类似的机制.
未使用的返回值检查器将以下注解视为等同于使用 `@MustUseReturnValues`:
* [com.google.errorprone.annotations.CheckReturnValue](https://errorprone.info/api/latest/com/google/errorprone/annotations/CheckReturnValue.html)
* [edu.umd.cs.findbugs.annotations.CheckReturnValue](https://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/CheckReturnValue.html)
* [org.jetbrains.annotations.CheckReturnValue](https://javadoc.io/doc/org.jetbrains/annotations/latest/org/jetbrains/annotations/CheckReturnValue.html)
* [org.springframework.lang.CheckReturnValue](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/lang/CheckReturnValue.html)
* [org.jooq.CheckReturnValue](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/CheckReturnValue.html)
它还将
[com.google.errorprone.annotations.CanIgnoreReturnValue](https://errorprone.info/api/latest/com/google/errorprone/annotations/CanIgnoreReturnValue.html)
视为等同于使用 `@IgnorableReturnValue`.
# 类
Tip:
在创建类之前, 如果目的是存储数据, 请考虑使用 [数据类](data-classes.html).
或者, 也可以考虑使用 [扩展](extensions.html) 来扩展已有的类, 而不是从头创建一个新类.
与其他面向对象的语言一样, Kotlin 使用 类(class) 来封装数据(属性)和行为(函数), 实现可重用的结构化代码.
类是对象的蓝图或模板, 可以通过 [构造器](#constructors-and-initializer-blocks) 来创建对象.
当你 [创建类的实例](#creating-instances) 时, 就是在根据这个蓝图构建一个具体的对象.
Kotlin 提供了简洁的语法来声明类. 要声明类, 请使用 `class` 关键字, 后面加上类名:
```KOTLIN
class Person { /*...*/ }
```
类的声明由以下几部分构成:
* 类头部(Class Header), 包括但不限于: * `class` 关键字 * 类名 * 类型参数(如果有的话) * [主构造器](#primary-constructor) (可选)
* 类体(Class Body) (可选), 由大括号 `{}` 括起, 包含 类的成员(Class Member), 例如: * [次级构造器(Secondary Constructor)](#secondary-constructors) * [初始化代码块](#initializer-blocks) * [函数](functions.html) * [属性](properties.html) * [嵌套类和内部类](nested-classes.html) * [对象声明](object-declarations.html)
类头部和 Body 部都可以省略到最简单的形式.
如果类没有 Body 部, 可以省略大括号 `{}`:
```KOTLIN
// 类有主构造器, 但没有 Body 部
class Person(val name: String, var age: Int)
```
下面示例声明了一个类, 带有类头部和 Body 部, 然后从它 [创建了一个实例](#creating-instances):
```KOTLIN
// Person 类, 有主构造器, 用于初始化 name 属性
class Person(val name: String) {
// Body 部, 包含 age 属性
var age: Int = 0
}
fun main() {
// 调用构造器, 创建 Person 类的实例
val person = Person("Alice")
// 访问实例的属性
println(person.name)
// 输出结果为: Alice
println(person.age)
// 输出结果为: 0
}
```
## 创建实例
当你使用类作为蓝图, 构建一个在程序中使用的实际对象时, 就创建了一个实例.
要创建类的实例, 请使用类名后加上括号 `()`, 类似于调用一个 [函数](functions.html):
```KOTLIN
// 创建 Person 类的实例
val anonymousUser = Person()
```
在 Kotlin 中, 可以通过以下方式创建实例:
* 不带参数 (`Person()`): 如果类中声明了默认值, 则使用默认值创建实例.
* 带参数 (`Person(value)`): 传入特定的值来创建实例.
你可以将创建的实例赋值给可变(`var`)或只读(`val`)的 [变量](basic-syntax.html#variables):
```KOTLIN
// 使用默认值创建实例, 并赋值给可变变量
var anonymousUser = Person()
// 传入特定的值创建实例, 并赋值给只读变量
val namedUser = Person("Joe")
```
可以在任何需要的地方创建实例: 在 [main() 函数](basic-syntax.html#program-entry-point) 内, 在其他函数内, 或在另一个类内.
此外, 也可以在另一个函数内创建实例, 然后从 `main()` 调用这个函数.
以下代码声明了一个 `Person` 类, 它有一个属性来存储姓名.
还演示了如何使用默认构造器的值和特定的值来创建实例:
```KOTLIN
// 类头部有一个主构造器, 用默认值初始化 name
class Person(val name: String = "Sebastian")
fun main() {
// 使用构造器的默认值创建实例
val anonymousUser = Person()
// 传入特定的值创建实例
val namedUser = Person("Joe")
// 访问两个实例的 name 属性
println(anonymousUser.name)
// 输出结果为: Sebastian
println(namedUser.name)
// 输出结果为: Joe
}
```
Note:
在 Kotlin 中, 与其他面向对象的编程语言不同, 创建类的实例时不需要 `new` 关键字.
关于如何创建嵌套类, 内部类, 以及匿名内部类的实例, 请参见 [嵌套类](nested-classes.html) 章节.
## 构造器和初始化代码块
当你创建类的实例时, 就会调用其中一个构造器.
Kotlin 中的类可以有一个 [主构造器(Primary Constructor)](#primary-constructor), 和一个或多个 [次级构造器(Secondary Constructor)](#secondary-constructors).
主构造器是初始化类的主要方式, 在类头部中声明.
次级构造器提供额外的初始化逻辑, 在 Body 部中声明.
主构造器和次级构造器都是可选的, 但类至少要有一个构造器.
### 主构造器
主构造器在 [创建实例](#creating-instances) 时设置实例的初始状态.
要声明主构造器, 请放在类头部的类名之后:
```KOTLIN
class Person constructor(name: String) { /*...*/ }
```
如果主构造器没有任何 [注解](annotations.html) 或 [可见度修饰符](visibility-modifiers.html#constructors),
可以省略 `constructor` 关键字:
```KOTLIN
class Person(name: String) { /*...*/ }
```
主构造器可以将参数声明为属性.
在参数名之前使用 `val` 关键字声明只读属性, 使用 `var` 关键字声明可变属性:
```KOTLIN
class Person(val name: String, var age: Int) { /*...*/ }
```
这些构造器参数属性作为实例的一部分存储, 可以从类的外部访问.
也可以声明不是属性的主构造器参数.
这些参数之前没有 `val` 或 `var`, 因此不存储在实例中, 只在 Body 部中可以访问:
```KOTLIN
// 主构造器参数, 同时也是属性
class PersonWithProperty(val name: String) {
fun greet() {
println("Hello, $name")
}
}
// 主构造器参数, 只是参数(不作为属性存储)
class PersonWithAssignment(name: String) {
// 必须赋值给属性, 才能在之后使用
val displayName: String = name
fun greet() {
println("Hello, $displayName")
}
}
```
在主构造器中声明的属性, 可以被类的 [成员函数](functions.html) 访问:
```KOTLIN
// 在主构造器中声明属性的类
class Person(val name: String, var age: Int) {
// 成员函数, 访问类属性
fun introduce(): String {
return "Hi, I'm $name and I'm $age years old."
}
}
```
也可以在主构造器中为属性指定默认值:
```KOTLIN
class Person(val name: String = "John", var age: Int = 30) { /*...*/ }
```
如果在 [创建实例](#creating-instances) 时没有传入值, 属性将使用默认值:
```KOTLIN
// 类的主构造器包含 name 和 age 的默认值
class Person(val name: String = "John", var age: Int = 30)
fun main() {
// 使用默认值创建实例
val person = Person()
println("Name: ${person.name}, Age: ${person.age}")
// 输出结果为: Name: John, Age: 30
}
```
在 Body 部中, 可以直接使用主构造器参数, 初始化额外的类属性:
```KOTLIN
// 类的主构造器包含 name 和 age 的默认值
class Person(
val name: String = "John",
var age: Int = 30
) {
// 使用主构造器参数, 初始化 description 属性
val description: String = "Name: $name, Age: $age"
}
fun main() {
// 创建 Person 类的实例
val person = Person()
// 访问 description 属性
println(person.description)
// 输出结果为: Name: John, Age: 30
}
```
与函数一样, 可以在构造器声明中使用 [尾随逗号](coding-conventions.html#trailing-commas):
```KOTLIN
class Person(
val name: String,
val lastName: String,
var age: Int,
) { /*...*/ }
```
### 初始化代码块
主构造器初始化类并设置属性.
大多数情况下, 使用简单的代码就能处理.
如果需要在 [创建实例](#creating-instances) 时执行更加复杂的操作,
请将这些逻辑放在 Body 部内的 初始化代码块(Initializer Block) 中.
这些代码块在主构造器执行时运行.
使用 `init` 关键字后面加上大括号 `{}`, 来声明初始化代码块.
在大括号内编写你希望在初始化时运行的代码:
```KOTLIN
// 类的主构造器初始化 name 和 age
class Person(val name: String, var age: Int) {
init {
// 初始化代码块在创建实例时运行
println("Person created: $name, age $age.")
}
}
fun main() {
// 创建 Person 类的实例
Person("John", 30)
// 输出结果为: Person created: John, age 30.
}
```
你可以根据需要添加任意数量的初始化代码块(`init {}`).
它们按照在 Body 部中出现的顺序运行, 与属性初始化代码一起执行:
```KOTLIN
//sampleStart
// 类的主构造器初始化 name 和 age
class Person(val name: String, var age: Int) {
// 第一个初始化代码块
init {
// 创建实例时首先运行
println("Person created: $name, age $age.")
}
// 第二个初始化代码块
init {
// 在第一个初始化代码块之后运行
if (age < 18) {
println("$name is a minor.")
} else {
println("$name is an adult.")
}
}
}
fun main() {
// 创建 Person 类的实例
Person("John", 30)
// 输出结果为:
// Person created: John, age 30.
// John is an adult.
}
//sampleEnd
```
可以在初始化代码块中使用主构造器参数.
例如, 上面的代码中, 第一个和第二个初始化代码块都使用了主构造器的 `name` 和 `age` 参数.
`init` 代码块的一个常见的使用场景是数据校验.
例如, 通过调用 [require 函数](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/require.html):
```KOTLIN
class Person(val age: Int) {
init {
require(age > 0) { "age must be positive" }
}
}
```
### 次级构造器(secondary constructor)
在 Kotlin 中, 类除了主构造器之外, 还可以拥有额外的次级构造器.
在你需要多种方式初始化类时, 或在 [与 Java 互操作](java-to-kotlin-interop.html) 时, 次级构造器会很有用.
要声明次级构造器, 请在 Body 部内使用 `constructor` 关键字, 在括号 `()` 内添加构造器参数.
在大括号 `{}` 内添加构造器逻辑:
```KOTLIN
// 类头部有一个主构造器, 初始化 name 和 age
class Person(val name: String, var age: Int) {
// 次级构造器, 接收 String 类型的 age, 并将其转换为 Int 类型
constructor(name: String, age: String) : this(name, age.toIntOrNull() ?: 0) {
println("$name created with converted age: ${this.age}")
}
}
fun main() {
// 使用次级构造器, 传入 String 类型的 age
Person("Bob", "8")
// 输出结果为: Bob created with converted age: 8
}
```
Tip:
表达式 `age.toIntOrNull() ?: 0` 使用了 Elvis 操作符.
详情请参见 [null 值安全性](null-safety.html#elvis-operator).
在上面的代码中, 次级构造器通过 `this` 关键字委托给主构造器,
传入参数是 `name`, 和 `age` 转换为整数的值.
在 Kotlin 中, 次级构造器必须委托给主构造器.
这种委托确保了主构造器所有的初始化逻辑在次级构造器逻辑运行之前执行.
构造器委托可以是:
* 直接(Direct) 委托, 次级构造器直接调用主构造器.
* 间接(Indirect) 委托, 一个次级构造器调用另一个次级构造器, 后者再委托给主构造器.
下面的示例演示直接委托和间接委托的工作方式:
```KOTLIN
// 类头部有一个主构造器, 初始化 name 和 age
class Person(
val name: String,
var age: Int
) {
// 直接委托给主构造器的次级构造器
constructor(name: String) : this(name, 0) {
println("Person created with default age: $age and name: $name.")
}
// 使用间接委托的次级构造器:
// this("Bob") -> constructor(name: String) -> 主构造器
constructor() : this("Bob") {
println("New person created with default age: $age and name: $name.")
}
}
fun main() {
// 根据直接委托创建实例
Person("Alice")
// 输出结果为: Person created with default age: 0 and name: Alice.
// 根据间接委托创建实例
Person()
// 输出结果为:
// Person created with default age: 0 and name: Bob.
// New person created with default age: 0 and name: Bob.
}
```
在含有初始化代码块(`init {}`)的类中, 这些代码块中的代码成为主构造器的一部分.
由于次级构造器首先委托给主构造器, 所有初始化代码块和属性初始化代码都会在次级构造器体之前运行.
即使类没有主构造器, 委托仍然会隐含地发生:
```KOTLIN
// 类头部没有主构造器
class Person {
// 创建实例时, 初始化代码块运行
init {
// 在次级构造器之前运行
println("1. First initializer block runs")
}
// 次级构造器, 接收整数参数
constructor(i: Int) {
// 在初始化代码块之后运行
println("2. Person $i is created")
}
}
fun main() {
// 创建 Person 类的实例
Person(1)
// 输出结果为:
// 1. First initializer block runs
// 2. Person 1 created
}
```
### 没有构造器的类
没有声明任何构造器(主构造器或次级构造器)的类, 有一个隐含的无参数主构造器:
```KOTLIN
// 类没有明确的构造器
class Person {
// 没有声明主构造器, 或次级构造器
}
fun main() {
// 使用隐含的主构造器创建 Person 类的实例
val person = Person()
}
```
这个隐含的主构造器的可见度是 public, 因此它可以从任何地方访问.
如果不希望你的类拥有 public 的构造器, 请声明一个空的主构造器, 使用默认值之外的可见度:
```KOTLIN
class Person private constructor() { /*...*/ }
```
Note:
在 JVM 中, 如果主构造器的所有参数都有默认值, 编译器会隐含地提供一个无参数构造器, 使用这些默认值.
这使得 Kotlin 更容易与各种库配合使用, 例如 [Jackson](https://github.com/FasterXML/jackson),
或 [Spring Data JPA](https://spring.io/projects/spring-data-jpa) 等等,
这些库通过无参数构造器来创建类的实例.
在下面的示例中, Kotlin 隐含地提供了一个无参数构造器 `Person()`, 使用默认值 `""`:
```KOTLIN
class Person(val personName: String = "")
```
## 继承
Kotlin 中的类继承, 可以从已有的类(称为基类)创建新的类(称为派生类), 继承基类的属性和函数, 同时添加或修改行为.
关于继承的层级结构, 以及如何使用 `open` 关键字, 详情请参见 [继承](inheritance.html) 章节.
## 抽象类
在 Kotlin 中, 抽象类是不能直接实例化的类. 它们被设计为由其他类继承, 由继承类来定义实际的行为.
这种行为称为 实现(implementation).
抽象类可以声明抽象的属性和函数, 它们必须由子类实现.
抽象类也可以有构造器.
这些构造器初始化类属性, 并强制子类提供必要的参数.
使用 `abstract` 关键字声明抽象类:
```KOTLIN
abstract class Person(val name: String, val age: Int)
```
抽象类可以同时拥有抽象的成员和非抽象的成员(属性和函数).
要将成员声明为抽象的, 必须明确使用 `abstract` 关键字.
不需要对抽象类或函数标注 `open` 关键字, 因为它们默认就是可继承的.
关于 `open` 关键字, 详情请参见 [继承](inheritance.html#open-keyword).
抽象成员在抽象类中没有实现.
你需要在子类或继承类中, 通过 `override` 函数或属性来定义实现:
```KOTLIN
// 抽象类, 主构造器声明 name 和 age
abstract class Person(
val name: String,
val age: Int
) {
// 抽象成员
// 不提供实现, 必须由子类实现
abstract fun introduce()
// 非抽象成员(有实现)
fun greet() {
println("Hello, my name is $name.")
}
}
// 子类, 为抽象成员提供实现
class Student(
name: String,
age: Int,
val school: String
) : Person(name, age) {
override fun introduce() {
println("I am $name, $age years old, and I study at $school.")
}
}
fun main() {
// 创建 Student 类的实例
val student = Student("Alice", 20, "Engineering University")
// 调用非抽象成员
student.greet()
// 输出结果为: Hello, my name is Alice.
// 调用被覆盖的抽象成员
student.introduce()
// 输出结果为: I am Alice, 20 years old, and I study at Engineering University.
}
```
## 同伴对象(Companion Object)
在 Kotlin 中, 每个类都可以有一个 [同伴对象](object-declarations.html#companion-objects).
同伴对象是一种对象声明, 可以使用类名访问其成员, 而不需要创建类的实例.
假如你需要编写一个函数, 可以在不创建类实例的情况下调用它, 但它在逻辑上仍与类紧密关联(例如工厂函数).
这种情况下, 你可以在类内的同伴 [对象声明](object-declarations.html) 中声明它:
```KOTLIN
// 类, 主构造器声明 name 属性
class Person(
val name: String
) {
// Body 部包含同伴对象
companion object {
fun createAnonymous() = Person("Anonymous")
}
}
fun main() {
// 在不创建类实例的情况下调用函数
val anonymous = Person.createAnonymous()
println(anonymous.name)
// 输出结果为: Anonymous
}
```
如果你在类中声明了同伴对象, 那么只需要使用类名作为限定符就可以访问同伴对象的成员.
详情请参见 [同伴对象](object-declarations.html#companion-objects).
# 数据类(Data Class)
Kotlin 中数据类(Data Class)的主要用来保存数据.
对每个数据类, 编译器会自动生成一些额外的成员函数, 可以用来将对象输出为可读的格式, 比较对象实例, 复制对象实例, 等等.
数据类通过 `data`关键字标记:
```KOTLIN
data class User(val name: String, val age: Int)
```
编译器会根据主构造器中声明的全部属性, 自动推断产生以下成员函数:
* `equals()`/`hashCode()` 函数对.
* `toString()` 函数, 输出格式为 `"User(name=John, age=42)"`.
* [componentN() 函数群](destructuring-declarations.html), 这些函数与类的属性对应, 函数名中的数字 1 到 N, 与属性的声明顺序一致.
* [copy() 函数](#copying).
为了保证自动生成的代码的行为一致, 并且有意义, 数据类必须满足以下所有要求:
* 主构造器必须有一个以上参数.
* 主构造器的所有参数必须标记为 `val` 或 `var`.
* 数据类不能是抽象类, open 类, 封闭(sealed)类, 或内部(inner)类.
此外, 考虑到成员函数继承的问题, 成员函数的生成遵循以下规则:
* 对于 `equals()`, `hashCode()` 或 `toString()` 函数, 如果在数据类的定义体中存在明确的实现, 或在超类中存在 `final` 的实现, 那么这些成员函数不会自动生成, 而会使用已存在的实现.
* 如果超类存在 `open` 的 `componentN()` 函数, 并且返回一个兼容的数据类型, 那么子类中对应的函数会自动生成, 并覆盖超类中的函数. 如果超类中的函数签名不一致, 或者是 `final` 的, 导致子类无法覆盖, 则会报告编译错误.
* 不允许对 `componentN()` 和 `copy()` 函数提供明确的实现(译注, 这些函数必须由编译器自动生成).
数据类可以继承其他类 (示例请参见 [封闭类(Sealed class)](sealed-classes.html)).
Note:
在 JVM 平台, 如果自动生成的类需要拥有一个无参数的构造器, 那么需要为属性指定默认值
(参见 [构造器](classes.html#constructors-and-initializer-blocks)):
```KOTLIN
data class User(val name: String = "", val age: Int = 0)
```
## 在类主体部声明的属性
编译器对自动生成的函数, 只使用主构造器中定义的属性.
如果想要在自动生成的函数实现中排除某个属性, 你可以将它声明在类的主体部:
```KOTLIN
data class Person(val name: String) {
var age: Int = 0
}
```
在下面的示例中, 在 `toString()`, `equals()`, `hashCode()`, 和 `copy()` 函数的实现中, 默认只使用了 `name` 属性,
而且只存在 1 个组件函数 `component1()`.
`age` 属性定义在类的 body 部, 因此被排除了.
所以, 两个 `Person` 对象拥有相同的 `name`, 不同的 `age` 值, 它们会被认为值相等.
因为 `equals()` 只计算主构造器中的属性:
```KOTLIN
data class Person(val name: String) {
var age: Int = 0
}
fun main() {
//sampleStart
val person1 = Person("John")
val person2 = Person("John")
person1.age = 10
person2.age = 20
println("person1 == person2: ${person1 == person2}")
// 输出结果为 person1 == person2: true
println("person1 with age ${person1.age}: ${person1}")
// 输出结果为 person1 with age 10: Person(name=John)
println("person2 with age ${person2.age}: ${person2}")
// 输出结果为 person2 with age 20: Person(name=John)
//sampleEnd
}
```
## 对象复制
使用 `copy()` 函数来复制对象, 可以修改 一部分 属性值, 但保持其他属性不变.
对于前面示例中的 `User` 类, 函数的实现将会是下面这样:
```KOTLIN
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)
```
因此你可以编写下面这样的代码:
```KOTLIN
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
```
`copy()` 函数会创建实例的 浅 拷贝. 也就是说, 它不会递归的复制对象组件的内容.
因此, 会继续使用对其它对象的相同的引用.
例如, 如果一个属性保存了一个可变的 List, 通过 "原来的" 值进行的变更, 也会反映到拷贝中,
反过来, 通过拷贝进行的变更, 也会反映到原来的值中:
```KOTLIN
data class Employee(val name: String, val roles: MutableList)
fun main() {
val original = Employee("Jamie", mutableListOf("developer"))
val duplicate = original.copy()
duplicate.roles.add("team lead")
println(original)
// 输出结果为: Employee(name=Jamie, roles=[developer, team lead])
println(duplicate)
// 输出结果为: Employee(name=Jamie, roles=[developer, team lead])
}
```
你可以看到, 修改 `duplicate.roles` 属性时, 也会修改 `original.roles` 属性, 因为这 2 个属性使用相同的 List 引用.
## 数据类中成员数据的解构
编译器会为数据类生成 组件函数(Component function), 有了这些组件函数,
就可以在 [解构声明(destructuring declaration)](destructuring-declarations.html) 中使用数据类:
```KOTLIN
val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age")
// 输出结果为 Jane, 35 years of age
```
## 标准库中的数据类
Kotlin 的标准库提供了 `Pair` 和 `Triple` 类可供使用.
但大多数情况下, 使用有具体名称的数据类是一种更好的设计方式,
因为, 数据类可以为属性指定有含义的名称, 因此可以让代码更加易读.
# 扩展
Kotlin 扩展(extension) 能够向一个类或接口扩展新的功能, 而不必使用继承, 或 装饰器(Decorator) 之类设计模式.
在处理无法直接修改的第三方库时, 扩展非常有用.
扩展创建后, 可以像调用原来的类或接口的成员一样调用它.
最常见的扩展形式是 [扩展函数(extension function)](#extension-functions) 和 [扩展属性(extension property)](#extension-properties).
重要的是, 扩展并不会修改它所扩展的类或接口.
在定义扩展时, 并不会添加新的成员. 只是能够使用相同的语法, 调用的新函数, 访问新的属性.
## 接收者(Receiver)
扩展总是在一个接收者上调用. 接收者必须具有与被扩展的类或接口相同的类型.
要使用扩展, 请在接收者前加上前缀, 然后是 `.` 和函数或属性名.
例如, 标准库中的 [.appendLine()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.text/append-line.html)
扩展函数扩展了 `StringBuilder` 类.
因此这里的接收者是一个 `StringBuilder` 实例, 接收者类型 是 `StringBuilder`:
```KOTLIN
fun main() {
//sampleStart
// builder 是 StringBuilder 的一个实例
val builder = StringBuilder()
// 在 builder 上调用 .appendLine() 扩展函数
.appendLine("Hello")
.appendLine()
.appendLine("World")
println(builder.toString())
// 输出结果为:
// Hello
//
// World
}
//sampleEnd
```
## 扩展函数
在创建自己的扩展函数之前, 请先查看 Kotlin [标准库](https://kotlinlang.org/api/core/kotlin-stdlib/)中是否已经有了你需要的功能.
标准库提供了许多有用的扩展函数, 用于:
* 操作集合: [.map()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/map.html), [.filter()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/filter.html), [.reduce()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/reduce.html), [.fold()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/fold.html), [.groupBy()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/group-by.html).
* 转换为字符串: [.joinToString()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/join-to-string.html).
* 处理 null 值: [.filterNotNull()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/filter-not-null.html).
要创建自己的扩展函数, 请在函数名称之前加上接收者类型和 `.`.
在以下示例中, `.truncate()` 函数扩展了 `String` 类, 因此接收者类型是 `String`:
```KOTLIN
fun String.truncate(maxLength: Int): String {
return if (this.length <= maxLength) this else take(maxLength - 3) + "..."
}
fun main() {
val shortUsername = "KotlinFan42"
val longUsername = "JetBrainsLoverForever"
println("Short username: ${shortUsername.truncate(15)}")
// 输出结果为: KotlinFan42
println("Long username: ${longUsername.truncate(15)}")
// 输出结果为: JetBrainsLov...
}
```
`.truncate()` 函数根据 `maxLength` 参数截断被调用的字符串, 并添加省略号 `...`.
如果字符串比 `maxLength` 短, 函数返回原始字符串.
在这个示例中, `.displayInfo()` 函数扩展了 `User` 接口:
```KOTLIN
interface User {
val name: String
val email: String
}
fun User.displayInfo(): String = "User(name=$name, email=$email)"
// 继承并实现 User 接口的属性
class RegularUser(override val name: String, override val email: String) : User
fun main() {
val user = RegularUser("Alice", "alice@example.com")
println(user.displayInfo())
// 输出结果为: User(name=Alice, email=alice@example.com)
}
```
`.displayInfo()` 函数返回一个字符串, 其中包含 `RegularUser` 实例的 `name` 和 `email`.
在你需要一次性向实现接口的所有类型添加功能时, 在接口上定义这样的扩展会非常有用.
在这个示例中, `.mostVoted()` 函数扩展了 `Map` 类:
```KOTLIN
fun Map.mostVoted(): String? {
return maxByOrNull { (key, value) -> value }?.key
}
fun main() {
val poll = mapOf(
"Cats" to 37,
"Dogs" to 58,
"Birds" to 22
)
println("Top choice: ${poll.mostVoted()}")
// 输出结果为: Top choice: Dogs
}
```
`.mostVoted()` 函数遍历它被调用的 map 的键值对,
并使用
[maxByOrNull()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/max-by-or-null.html)
函数, 返回包含最大值的键值对的键.
如果 map 为空, `maxByOrNull()` 函数返回 `null`.
`mostVoted()` 函数使用安全调用 `?.`, 只在 `maxByOrNull()` 函数返回非 null 值时才访问 `key` 属性.
### 泛型扩展函数
要创建泛型扩展函数, 请在函数名称之前声明泛型类型参数, 使其在接收者类型表达式中可以使用.
在这个示例中, `.endpoints()` 函数扩展了 `List`, 其中 `T` 可以是任意类型:
```KOTLIN
fun List.endpoints(): Pair {
return first() to last()
}
fun main() {
val cities = listOf("Paris", "London", "Berlin", "Prague")
val temperatures = listOf(21.0, 19.5, 22.3)
val cityEndpoints = cities.endpoints()
val tempEndpoints = temperatures.endpoints()
println("First and last cities: $cityEndpoints")
// 输出结果为: (Paris, Prague)
println("First and last temperatures: $tempEndpoints")
// 输出结果为: (21.0, 22.3)
}
```
`.endpoints()` 函数返回一个 pair, 其中包含它被调用的列表的第一个和最后一个元素.
在函数体内, 它调用 `first()` 和 `last()` 函数, 并使用 `to` 中缀函数, 将返回值合并为一个 `Pair`.
关于泛型, 详情请参见 [泛型函数](generics.html).
### 可为 null 的接收者(Nullable Receiver)
你可以定义接收者类型可为 null 的扩展函数, 这样即使变量的值为 null 也可以调用它.
当接收者为 `null` 时, `this` 也是 `null`. 请确保在函数内正确处理可空性.
例如, 在函数体内使用 `this == null` 检查, [安全调用 ?.](null-safety.html#safe-call-operator),
或 [Elvis 操作符 ?:](null-safety.html#elvis-operator).
在这个示例中, 你可以调用 `.toString()` 函数, 无需检查 `null`, 因为检查已经在扩展函数内部完成了:
```KOTLIN
fun main() {
//sampleStart
// 对可为 null 的 Any 的扩展函数
fun Any?.toString(): String {
if (this == null) return "null"
// null 检查之后, `this` 被智能类型转换为不可为 null 的 Any
// 所以这里调用的是通常的 toString() 函数
return toString()
}
val number: Int? = 42
val nothing: Any? = null
println(number.toString())
// 输出结果为: 42
println(nothing.toString())
// 输出结果为: null
//sampleEnd
}
```
### 调用扩展函数还是成员函数?
由于扩展函数和成员函数的调用方式相同, 编译器如何知道该调用哪一个?
扩展函数是 静态 派发的, 也就是说, 编译器会在编译期间根据接收者类型决定调用哪个函数. 例如:
```KOTLIN
fun main() {
//sampleStart
open class Shape
class Rectangle: Shape()
fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"
fun printClassName(shape: Shape) {
println(shape.getName())
}
printClassName(Rectangle())
// 输出结果为: Shape
//sampleEnd
}
```
在这个示例中, 编译器调用 `Shape.getName()` 扩展函数, 因为参数 `shape` 声明为 `Shape` 类型.
由于扩展函数是静态解析的, 编译器根据声明类型而非实际实例来选择函数.
所以即使示例传入了一个 `Rectangle` 实例, `.getName()` 函数也会解析为 `Shape.getName()`,
因为变量声明为 `Shape` 类型.
如果一个类有成员函数, 同时又有相同接收者类型, 相同名称而且参数兼容的扩展函数, 成员函数优先. 例如:
```KOTLIN
fun main() {
//sampleStart
class Example {
fun printFunctionType() { println("Member function") }
}
fun Example.printFunctionType() { println("Extension function") }
Example().printFunctionType()
// 输出结果为: Member function
//sampleEnd
}
```
但是, 扩展函数可以重载(overload)名称相同但签名 不同 的成员函数:
```KOTLIN
fun main() {
//sampleStart
class Example {
fun printFunctionType() { println("Member function") }
}
// 名称相同, 但签名不同
fun Example.printFunctionType(index: Int) { println("Extension function #$index") }
Example().printFunctionType(1)
// 输出结果为: Extension function #1
//sampleEnd
}
```
在这个示例中, 由于向 `.printFunctionType()` 函数传入了一个 `Int`, 编译器选择了匹配该签名的扩展函数.
编译器忽略了不接受参数的成员函数.
### 匿名扩展函数
你可以定义扩展函数, 但不指定函数名称.
如果你想避免污染全局命名空间, 或需要将某些扩展行为作为参数传递时, 这很有用.
例如, 假设你想扩展一个数据类, 添加一次性的函数来计算运费, 而不给它命名:
```KOTLIN
fun main() {
//sampleStart
data class Order(val weight: Double)
val calculateShipping = fun Order.(rate: Double): Double = this.weight * rate
val order = Order(2.5)
val cost = order.calculateShipping(3.0)
println("Shipping cost: $cost")
// 输出结果为: Shipping cost: 7.5
}
```
要将扩展行为作为参数传递, 请使用带有类型注解的 [Lambda 表达式](lambdas.html#lambda-expression-syntax).
例如, 假设你想检查一个数字是否在某个范围内, 而不定义一个命名函数:
```KOTLIN
fun main() {
val isInRange: Int.(min: Int, max: Int) -> Boolean = { min, max -> this in min..max }
println(5.isInRange(1, 10))
// 输出结果为: true
println(20.isInRange(1, 10))
// 输出结果为: false
}
```
在这个示例中, `isInRange` 变量持有一个函数, 类型为 `Int.(min: Int, max: Int) -> Boolean`.
这个类型是对 `Int` 类的扩展函数, 接受 `min` 和 `max` 参数并返回 `Boolean`.
Lambda 表达式的函数体 `{ min, max -> this in min..max }` 检查调用该函数的 `Int` 值是否在 `min` 和 `max` 参数之间的范围内.
如果检查成功, Lambda 表达式 返回 `true`.
更多信息, 请参见 [Lambda 表达式和匿名函数](lambdas.html).
## 扩展属性
Kotlin 支持扩展属性, 对于执行数据转换, 或创建 UI 显示辅助功能非常有用, 同时不会使你操作的类变得混乱.
要创建扩展属性, 请写下你想扩展的类名, 后跟 `.` 和你的属性名称.
例如, 假设你有一个表示用户名字和姓氏的数据类, 你想创建一个属性, 在访问时返回邮件风格的用户名.
代码可能如下所示:
```KOTLIN
data class User(val firstName: String, val lastName: String)
// 扩展属性, 获取用于邮件地址的用户名
val User.emailUsername: String
get() = "${firstName.lowercase()}.${lastName.lowercase()}"
fun main() {
val user = User("Mickey", "Mouse")
// 调用扩展属性
println("Generated email username: ${user.emailUsername}")
// 输出结果为: Generated email username: mickey.mouse
}
```
由于扩展实际上不会向类添加新成员, 扩展属性没有有效的方式拥有 [后端域变量(backing field)](properties.html#backing-fields).
这就是为什么扩展属性不允许使用初始化器.
你只能通过明确提供 getter 和 setter 来定义它们的行为. 例如:
```KOTLIN
data class House(val streetName: String)
// 无法编译, 因为没有 getter 和 setter
// var House.number = 1
// Error: Initializers are not allowed for extension properties
// 成功编译
val houseNumbers = mutableMapOf()
var House.number: Int
get() = houseNumbers[this] ?: 1
set(value) {
println("Setting house number for ${this.streetName} to $value")
houseNumbers[this] = value
}
fun main() {
val house = House("Maple Street")
// 显示默认值
println("Default number: ${house.number} ${house.streetName}")
// 输出结果为: Default number: 1 Maple Street
house.number = 99
// 输出结果为: Setting house number for Maple Street to 99
// 显示更新后的号码
println("Updated number: ${house.number} ${house.streetName}")
// 输出结果为: Updated number: 99 Maple Street
}
```
在这个示例中, getter 使用 [Elvis 操作符](null-safety.html#elvis-operator), 返回 `houseNumbers` map 中存在的门牌号码,
如果不存在则返回 `1`.
关于如何编写 getter 和 setter, 请参见 [自定义 getter 和 setter](properties.html#custom-getters-and-setters).
## 对同伴对象(Companion Object)的扩展
如果一个类定义了 [同伴对象](object-declarations.html#companion-objects), 你也可以为同伴对象定义扩展函数和扩展属性.
与同伴对象的普通成员一样, 你可以只使用类名作为限定符来调用它们.
编译器默认将同伴对象命名为 `Companion`:
```KOTLIN
class Logger {
companion object { }
}
fun Logger.Companion.logStartupMessage() {
println("Application started.")
}
fun main() {
Logger.logStartupMessage()
// 输出结果为: Application started.
}
```
## 将扩展定义为成员
你可以在一个类的内部为另一个类声明扩展. 这样的扩展有多个 隐含接受者(implicit receiver).
隐含接受者是指, 可以不使用 [this](this-expressions.html#qualified-this) 限定符就能访问其成员的对象:
* 扩展声明所在的类, 是 派发接受者(dispatch receiver).
* 扩展函数的接收者类型, 是 扩展接受者(extension receiver).
考虑这个示例, 其中 `Connection` 类有一个为 `Host` 类定义的扩展函数, 名为 `printConnectionString()`:
```KOTLIN
class Host(val hostname: String) {
fun printHostname() { print(hostname) }
}
class Connection(val host: Host, val port: Int) {
fun printPort() { print(port) }
// Host 是扩展接受者
fun Host.printConnectionString() {
// 调用 Host.printHostname()
printHostname()
print(":")
// 调用 Connection.printPort()
// Connection 是派发接受者
printPort()
}
fun connect() {
/*...*/
// 调用扩展函数
host.printConnectionString()
}
}
fun main() {
Connection(Host("kotl.in"), 443).connect()
// 输出结果为: kotl.in:443
// 触发错误, 因为扩展函数在 Connection 之外不可用
// Host("kotl.in").printConnectionString()
// Unresolved reference 'printConnectionString'.
}
```
这个示例在 `Connection` 类内部声明了 `printConnectionString()` 函数, 因此 `Connection` 类是派发接受者.
扩展函数的接收者类型是 `Host` 类, 因此 `Host` 类是扩展接受者.
如果派发接受者和扩展接受者存在相同名称的成员, 扩展接受者的成员优先.
要明确的访问派发接受者, 请使用 [带限定符的 this 语法](this-expressions.html#qualified-this):
```KOTLIN
class Connection {
fun Host.getConnectionString() {
// 调用 Host.toString()
toString()
// 调用 Connection.toString()
this@Connection.toString()
}
}
```
### 覆盖成员扩展
你可以将成员扩展声明为 `open`, 并在子类中覆盖它, 这在你想为每个子类自定义扩展行为时非常有用.
编译器对每种接收者类型的处理方式不同:
| 接收者类型 |解析时机 |派发类型 |
---------------------
| 派发接受者 |运行时期 |虚拟派发 |
| 扩展接受者 |编译时期 |静态派发 |
考虑这个示例, 其中 `User` 类是 `open` 的, `Admin` 类继承自它.
`NotificationSender` 类为 `User` 和 `Admin` 类定义了 `sendNotification()` 扩展函数,
而 `SpecialNotificationSender` 类覆盖了这些函数:
```KOTLIN
open class User
class Admin : User()
open class NotificationSender {
open fun User.sendNotification() {
println("Sending user notification from normal sender")
}
open fun Admin.sendNotification() {
println("Sending admin notification from normal sender")
}
fun notify(user: User) {
user.sendNotification()
}
}
class SpecialNotificationSender : NotificationSender() {
override fun User.sendNotification() {
println("Sending user notification from special sender")
}
override fun Admin.sendNotification() {
println("Sending admin notification from special sender")
}
}
fun main() {
// 派发接受者是 NotificationSender
// 扩展接受者是 User
// 解析为 NotificationSender 中的 User.sendNotification()
NotificationSender().notify(User())
// 输出结果为: Sending user notification from normal sender
// 派发接受者是 SpecialNotificationSender
// 扩展接受者是 User
// 解析为 SpecialNotificationSender 中的 User.sendNotification()
SpecialNotificationSender().notify(User())
// 输出结果为: Sending user notification from special sender
// 派发接受者是 SpecialNotificationSender
// 扩展接受者是 User, 不是 Admin
// notify() 函数将 user 声明为 User 类型
// 静态解析为 SpecialNotificationSender 中的 User.sendNotification()
SpecialNotificationSender().notify(Admin())
// 输出结果为: Sending user notification from special sender
}
```
派发接受者使用虚拟派发, 在运行时期解析, 这使得 `main()` 函数中的行为更容易理解.
可能令你惊讶的是, 当你对一个 `Admin` 实例调用 `notify()` 函数时,
编译器根据声明类型 `user: User` 选择扩展, 因为它静态的解析扩展接受者.
## 扩展与可见度修饰符
扩展使用的 [可见度修饰符](visibility-modifiers.html), 与在同一范围内声明的通常函数相同,
包括作为其他类成员声明的扩展.
例如, 在文件顶级声明的扩展可以访问同一文件中其他 `private` 顶级声明:
```KOTLIN
// 文件: StringUtils.kt
private fun removeWhitespace(input: String): String {
return input.replace("\\s".toRegex(), "")
}
fun String.cleaned(): String {
return removeWhitespace(this)
}
fun main() {
val rawEmail = " user @example. com "
val cleaned = rawEmail.cleaned()
println("Raw: '$rawEmail'")
// 输出结果为: Raw: ' user @example. com '
println("Cleaned: '$cleaned'")
// 输出结果为: Cleaned: 'user@example.com'
println("Looks like an email: ${cleaned.contains("@") && cleaned.contains(".")}")
// 输出结果为: Looks like an email: true
}
```
如果扩展声明在其接收者类型之外, 它无法访问接收者的 `private` 或 `protected` 成员:
```KOTLIN
class User(private val password: String) {
fun isLoggedIn(): Boolean = true
fun passwordLength(): Int = password.length
}
// 在类外部声明的扩展
fun User.isSecure(): Boolean {
// 无法访问 password, 因为它是 private 的:
// return password.length >= 8
// 应改为依赖 public 成员:
return passwordLength() >= 8 && isLoggedIn()
}
fun main() {
val user = User("supersecret")
println("Is user secure: ${user.isSecure()}")
// 输出结果为: Is user secure: true
}
```
如果扩展被标记为 `internal`, 它只在其 [模块](visibility-modifiers.html#modules) 内可访问:
```KOTLIN
// Networking 模块
// JsonParser.kt
internal fun String.parseJson(): Map {
return mapOf("fakeKey" to "fakeValue")
}
```
## 扩展的范围
大多数情况下, 你可以直接位于包之下的顶级位置定义扩展:
```KOTLIN
package org.example.declarations
fun List.getLongestString() { /*...*/}
```
要在这个包之外使用扩展, 需要在调用处导入这个扩展:
```KOTLIN
package org.example.usage
import org.example.declarations.getLongestString
fun main() {
val list = listOf("red", "green", "blue")
list.getLongestString()
}
```
详情请参见 [导入](packages.html#imports).
# 接口(Interface)
Kotlin 中的接口可以包含抽象方法的声明, 也可以包含方法的实现.
接口与抽象类的区别在于, 接口不能存储状态数据.
接口可以有属性, 但这些属性必须是抽象的, 或者必须提供访问器的自定义实现.
接口使用 `interface` 关键字来定义:
```KOTLIN
interface MyInterface {
fun bar()
fun foo() {
// 方法体是可选的
}
}
```
## 实现接口
类或者对象可以实现一个或多个接口
```KOTLIN
class Child : MyInterface {
override fun bar() {
// 方法体
}
}
```
## 接口中的属性
你可以在接口中定义属性. 接口中声明的属性要么是抽象的, 要么提供访问器的自定义实现.
接口中声明的属性不能拥有后端域变量(backing field),
因此, 在接口中定义的属性访问器也不能访问属性的后端域变量:
```KOTLIN
interface MyInterface {
val prop: Int // 抽象属性
val propertyWithImplementation: String
get() = "foo"
fun foo() {
print(prop)
}
}
class Child : MyInterface {
override val prop: Int = 29
}
```
## 接口的继承
接口也可以继承其他接口, 因此它可以对父接口中的成员提供实现, 同时又声明新的函数和属性.
很自然的, 类在实现这样的接口时, 只需要实现缺少的函数和属性:
```KOTLIN
interface Named {
val name: String
}
interface Person : Named {
val firstName: String
val lastName: String
override val name: String get() = "$firstName $lastName"
}
data class Employee(
// 不需要实现 'name' 属性
override val firstName: String,
override val lastName: String,
val position: Position
) : Person
```
## 解决覆盖冲突(overriding conflict)
如果你为一个类指定了多个超类, 可能会导致对同一个方法继承得到了多个实现:
```KOTLIN
interface A {
fun foo() { print("A") }
fun bar()
}
interface B {
fun foo() { print("B") }
fun bar() { print("bar") }
}
class C : A {
override fun bar() { print("bar") }
}
class D : A, B {
override fun foo() {
super.foo()
super.foo()
}
override fun bar() {
super.bar()
}
}
```
接口 A 和 B 都定义了函数 foo() 和 bar(). 它们也都实现了 foo(),
但只有 B 实现了 bar()
(在 A 中 bar() 没有标记为 abstract, 因为在接口中, 如果没有定义函数体, 则函数默认为 abstract).
现在, 如果你从 A 派生一个实体类 C, 那么必须覆盖函数 bar(), 并提供一个实现.
然而, 如果你从 A 和 B 派生出 D, 对于从多个接口中继承得到的所有方法我们都需要实现,
并且指明 D 具体应该如何实现各个方法.
对于只继承得到了单个实现的方法(如上例中的 bar() 方法),
以及继承得到了多个实现的方法(如上例中的 foo() 方法), 都存在这个限制.
## 为接口函数生成 JVM 默认方法
在 JVM 上, 接口中声明的函数会被编译为默认方法.
你可以将 `-jvm-default` 编译器选项设置为下面的值, 来控制这个行为:
* `enable` (默认值): 在接口中生成默认实现, 并在子类和 `DefaultImpls` 类中包含桥接函数(Bridge Function). 请使用这个模式来维持与旧 Kotlin 版本的二进制兼容性.
* `no-compatibility`: 只在接口中生成默认实现. 这个模式会略过兼容性桥接函数和 `DefaultImpls` 类, 因此只适用于新的 Kotlin 代码.
* `disable`: 略过默认方法, 只生成兼容性桥接函数和 `DefaultImpls` 类.
要配置 `-jvm-default` 编译器选项, 请在你的 Gradle Kotlin DSL 中设置 `jvmDefault` 属性:
```KOTLIN
kotlin {
compilerOptions {
jvmDefault = JvmDefaultMode.NO_COMPATIBILITY
}
}
```
# 委托
[委托模式](https://en.wikipedia.org/wiki/Delegation_pattern) 已被实践证明为类继承模式之外的另一种很好的替代方案,
Kotlin 直接支持委托模式, 因此你不必再为了实现委托模式而手动编写那些无聊的样板代码(Boilerplate Code)了.
比如, `Derived` 类可以实现 `Base` 接口, 将接口所有的 public 成员委托给一个指定的对象:
```KOTLIN
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
class Derived(b: Base) : Base by b
fun main() {
val base = BaseImpl(10)
Derived(base).print()
}
```
`Derived` 类声明的基类列表中的 `by` 子句表示, `b` 将被保存在 `Derived` 的对象实例内部,
而且编译器将会生成继承自 `Base` 接口的所有方法, 并将调用转发给 `b`.
## 覆盖由委托实现的接口成员
函数和属性的 [覆盖](inheritance.html#overriding-methods) 会如你预期的那样工作:
编译器将会使用你的 `override` 实现, 而不会使用委托对象中的实现.
如果你想要在 `Derived` 中添加一段函数覆盖 `override fun printMessage() { print("abc") }`,
那么上面程序中调用 `printMessage` 时的打印结果将是 abc, 而不是 10:
```KOTLIN
interface Base {
fun printMessage()
fun printMessageLine()
}
class BaseImpl(val x: Int) : Base {
override fun printMessage() { print(x) }
override fun printMessageLine() { println(x) }
}
class Derived(b: Base) : Base by b {
override fun printMessage() { print("abc") }
}
fun main() {
val base = BaseImpl(10)
Derived(base).printMessage()
Derived(base).printMessageLine()
}
```
注意, 使用上述方式覆盖的接口成员, 在委托对象的成员函数内无法调用.
委托对象的成员函数内, 只能访问它自己的接口方法实现:
```KOTLIN
interface Base {
val message: String
fun print()
}
class BaseImpl(x: Int) : Base {
override val message = "BaseImpl: x = $x"
override fun print() { println(message) }
}
class Derived(b: Base) : Base by b {
// 在 b 的 `print` 方法实现中无法访问这个属性
override val message = "Message of Derived"
}
fun main() {
val b = BaseImpl(10)
val derived = Derived(b)
derived.print()
println(derived.message)
}
```
更多信息请参见 [委托属性](delegated-properties.html).
# 继承
Tip:
在创建类的继承层级结构之前, 请考虑使用 [抽象类](classes.html#abstract-classes) 或 [接口](interfaces.html).
默认情况下, 你可以从抽象类和接口继承. 它们的目的就是为了让其它类能够继承并实现它们的成员.
Kotlin 中所有的类都有一个共同的超类 `Any`, 如果类声明时没有指定超类, 则默认为 `Any`:
```KOTLIN
class Example // 隐含地继承自 Any
```
`Any` 拥有三个函数: `equals()`, `hashCode()` 和 `toString()`. 因此, Kotlin 的所有类都拥有这些函数.
默认情况下, Kotlin 的类是 final 的 - 不能再被继承.
如果要允许一个类被继承, 需要使用 `open` 关键字标记这个类:
```KOTLIN
open class Base // 这个类现在是 open 的, 可以被继承
```
[详情请参见 open 关键字](#open-keyword).
要明确声明类的超类, 要在类的头部添加一个冒号, 冒号之后指定超类:
```KOTLIN
open class Base(p: Int)
class Derived(p: Int) : Base(p)
```
如果子类有主构造器, 那么可以(而且必须)在主构造器中使用主构造器的参数来初始化基类.
如果子类没有主构造器, 那么所有的次级构造器都必须使用 `super` 关键字来初始化基类,
或者委托到另一个构造器, 由被委托的构造器来初始化基类.
注意, 这种情况下, 不同的次级构造器可以调用基类中不同的构造器:
```KOTLIN
class MyView : View {
constructor(ctx: Context) : super(ctx)
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
```
##
`open` 关键字
在 Kotlin 中, `open` 关键字表示一个类或一个成员 (函数或属性) 在子类中能够被覆盖.
默认情况下, Kotlin 类及其成员都是 final 的,
也就是说, 除非你明确的将它们标记为 `open`, 否则类不能被继承, 成员不能被覆盖:
```KOTLIN
// 带有 open 关键字的基类, 允许继承
open class Person(
val name: String,
) {
// open 的函数, 在子类中能够被覆盖
open fun introduce() {
println("Hello, my name is $name.")
}
}
// 子类继承自 Person, 并覆盖 introduce() 函数
class Student(
name: String,
val school: String,
) : Person(name) {
override fun introduce() {
println("Hi, I'm $name, and I study at $school.")
}
}
```
如果你覆盖基类的一个成员, 那么覆盖后的成员默认也是 open 的.
如果你想要修改这个行为, 禁止你的类的子类覆盖你的实现, 你可以将覆盖后的成员明确的标记为 `final`:
```KOTLIN
// 带有 open 关键字的基类, 允许继承
open class Person(
val name: String,
) {
// open 的函数, 在子类中能够被覆盖
open fun introduce() {
println("Hello, my name is $name.")
}
}
// 子类继承自 Person, 并覆盖 introduce() 函数
class Student(
name: String,
val school: String,
) : Person(name) {
// final 关键字禁止子类中进一步覆盖
final override fun introduce() {
println("Hi, I'm $name, and I study at $school.")
}
}
```
## 方法的覆盖
Kotlin 要求使用明确的修饰符来标识允许被子类覆盖的成员, 也要求使用明确的修饰符来标识对超类成员的覆盖:
```KOTLIN
open class Shape {
open fun draw() { /*...*/ }
fun fill() { /*...*/ }
}
class Circle() : Shape() {
override fun draw() { /*...*/ }
}
```
对于 `Circle.draw()` 必须添加 `override` 修饰符. 如果遗漏了这个修饰符, 编译器将会报告错误.
如果一个函数没有标注 `open` 修饰符, 比如上例中的 `Shape.fill()`, 那么不允许在子类中声明一个同名同参的方法,
无论是否添加 `override` 修饰符, 都不可以.
在一个 final 类(也就是, 没有添加 `open` 修饰符的类)的成员上添加 `open` 修饰符, 不会发生任何效果.
当一个子类成员标记了 `override` 修饰符来覆盖父类成员时, 覆盖后的子类成员本身也将是 open 的,
因此子类成员可以被自己的子类再次覆盖.
如果你希望禁止这种再次覆盖, 可以使用 `final` 关键字:
```KOTLIN
open class Rectangle() : Shape() {
final override fun draw() { /*...*/ }
}
```
## 属性的覆盖
属性的覆盖方式与方法覆盖相同; 超类中声明的属性在后代类中再次声明时, 必须使用 `override` 关键字来标记,
而且覆盖后的属性数据类型必须与超类中的属性数据类型兼容.
超类中声明的属性, 在后代类中可以使用带初始化器的属性来覆盖, 也可以使用带 `get` 方法的属性来覆盖:
```KOTLIN
open class Shape {
open val vertexCount: Int = 0
}
class Rectangle : Shape() {
override val vertexCount = 4
}
```
你也可以使用一个 `var` 属性覆盖一个 `val` 属性, 但不可以反过来使用一个 `val` 属性覆盖一个 `var` 属性.
允许这种覆盖的原因是, `val` 属性本质上只是定义了一个 `get` 方法,
使用 `var` 属性来覆盖它, 只是向后代类中添加了一个 `set` 方法.
注意, 你可以在主构造器的属性声明中使用 `override` 关键字:
```KOTLIN
interface Shape {
val vertexCount: Int
}
class Rectangle(override val vertexCount: Int = 4) : Shape // 长方形总是拥有 4 个顶点
class Polygon : Shape {
override var vertexCount: Int = 0 // 多边形的顶点数目不定, 可以变更为任何数字
}
```
## 子类的初始化顺序
子类新实例构造的过程中, 首先完成的第一步是要初始化基类 (顺序上仅次于计算传递给基类构造器的参数值),
因此要在子类的初始化逻辑之前执行.
```KOTLIN
//sampleStart
open class Base(val name: String) {
init { println("Initializing a base class") }
open val size: Int =
name.length.also { println("Initializing size in the base class: $it") }
}
class Derived(
name: String,
val lastName: String,
) : Base(name.replaceFirstChar { it.uppercase() }.also { println("Argument for the base class: $it") }) {
init { println("Initializing a derived class") }
override val size: Int =
(super.size + lastName.length).also { println("Initializing size in the derived class: $it") }
}
//sampleEnd
fun main() {
println("Constructing the derived class(\"hello\", \"world\")")
Derived("hello", "world")
}
```
也就是说, 基类构造器执行时, 在子类中定义或覆盖的属性还没有被初始化.
如果在基类初始化逻辑中使用到这些属性
(无论是直接使用, 还是通过另一个被覆盖的 `open` 成员间接使用),
可能会导致不正确的行为, 甚至导致运行时错误.
因此, 设计基类时, 在构造器, 属性初始化器, 以及 `init` 代码段中, 你应该避免使用 `open` 成员.
## 调用超类中的实现
后代类中的代码, 可以使用 `super` 关键字来调用超类中的函数和属性访问器的实现:
```KOTLIN
open class Rectangle {
open fun draw() { println("Drawing a rectangle") }
val borderColor: String get() = "black"
}
class FilledRectangle : Rectangle() {
override fun draw() {
super.draw()
println("Filling the rectangle")
}
val fillColor: String get() = super.borderColor
}
```
在内部类(inner class)的代码中, 可以使用 `super` 关键字加上外部类名称限定符: `super@Outer`
来访问外部类(outer class)的超类:
```KOTLIN
open class Rectangle {
open fun draw() { println("Drawing a rectangle") }
val borderColor: String get() = "black"
}
//sampleStart
class FilledRectangle: Rectangle() {
override fun draw() {
val filler = Filler()
filler.drawAndFill()
}
inner class Filler {
fun fill() { println("Filling") }
fun drawAndFill() {
super@FilledRectangle.draw() // 调用 Rectangle 的 draw() 函数实现
fill()
println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") // 使用 Rectangle 的 borderColor 属性的 get() 函数
}
}
}
//sampleEnd
fun main() {
val fr = FilledRectangle()
fr.draw()
}
```
## 覆盖的规则
在 Kotlin 中, 类继承中的方法实现问题, 遵守以下规则: 如果一个类从它的直接超类中继承了同一个成员的多个实现,
那么这个子类必须覆盖这个成员, 并提供一个自己的实现(可以使用继承得到的多个实现中的某一个).
为了表示使用的方法是从哪个超类继承得到的, 可以使用 `super` 关键字, 将超类名称放在尖括号类, 比如, `super`:
```KOTLIN
open class Rectangle {
open fun draw() { /* ... */ }
}
interface Polygon {
fun draw() { /* ... */ } // 接口的成员默认是 'open' 的
}
class Square() : Rectangle(), Polygon {
// 编译器要求 draw() 方法必须覆盖:
override fun draw() {
super.draw() // 调用 Rectangle.draw()
super.draw() // 调用 Polygon.draw()
}
}
```
同时继承 `Rectangle` 和 `Polygon` 是合法的,
但他们都实现了函数 `draw()` 的继承就发生了问题, 因此你需要在 `Square` 类中覆盖函数 `draw()`,
并提供单独的实现, 这样才能消除歧义.
# 对象声明与对象表达式
在 Kotlin 中, 你可以使用对象, 只需一步就能定义一个类并创建它的一个实例.
当你需要重用一个单例(singleton instance), 或者一个一次性的对象时, 这个功能会非常有用.
为了处理这样的场景, Kotlin 提供了两种方案:
对象声明 用于创建单例, 以及 对象表达式 用于创建匿名的, 一次性对象.
Tip:
单例可以确保一个类只有一个实例, 并为这个实例提供一个全局的访问点.
对象声明和对象表达式 最适合于下面的场景:
* 对共用的资源使用单例: 你需要确保一个类在整个应用程序中只存在一个实例. 例如, 管理一个数据库连接池.
* 创建工厂方法: 你需要一种便利的方法来有效率的创建实例. 你可以使用 [同伴对象](#companion-objects) 来定义类级的函数和属性, 绑定到一个类, 简化类实例的创建和管理.
* 临时修改既有的类的行为: 你想要一个既有的类的行为, 但不创建新的子类. 例如, 对一个对象添加临时的功能, 执行特定的操作.
* 需要类型安全的设计: 你需要使用对象表达式作为接口或 [抽象类](classes.html#abstract-classes) 的一次性的实现. 例如, 对于按钮的点击事件处理程序之类的场景, 这个功能会非常有用.
## 对象声明(Object declaration)
在 Kotlin 中, 你可以使用对象声明创建对象的单个实例, 对象声明由 `object` 关键字加上对象名称构成.
只需一步就能定义一个类并创建它的一个实例, 非常便于实现单例:
```KOTLIN
//sampleStart
// 声明一个单例对象, 用来管理 DataProvider
object DataProviderManager {
private val providers = mutableListOf()
// 注册一个新的 DataProvider
fun registerDataProvider(provider: DataProvider) {
providers.add(provider)
}
// 获取所有注册的 DataProvider
val allDataProviders: Collection
get() = providers
}
//sampleEnd
// 示例 DataProvider 的接口
interface DataProvider {
fun provideData(): String
}
// 示例 DataProvider 的实现
class ExampleDataProvider : DataProvider {
override fun provideData(): String {
return "Example data"
}
}
fun main() {
// 创建 ExampleDataProvider 的一个实例
val exampleProvider = ExampleDataProvider()
// 要引用 `object`, 直接使用它的名称
DataProviderManager.registerDataProvider(exampleProvider)
// 获取所有注册的 DataProvider, 并打印输出
println(DataProviderManager.allDataProviders.map { it.provideData() })
// 输出结果为: [Example data]
}
```
Tip:
对象声明中的初始化处理是线程安全的(thread-safe), 而且会在对象初次访问时完成初始化处理.
要引用这个 `object`, 请直接使用它的名称:
```KOTLIN
DataProviderManager.registerDataProvider(exampleProvider)
```
对象声明也可以指定基类,
与 [匿名对象从既有的类继承, 或实现接口](#inherit-anonymous-objects-from-supertypes) 的方式类似:
```KOTLIN
object DefaultListener : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { ... }
override fun mouseEntered(e: MouseEvent) { ... }
}
```
与变量声明一样, 对象声明不是表达式, 因此不能用在赋值语句的右侧:
```KOTLIN
// 语法错误: 对象表达式不能指定名称.
val myObject = object MySingleton {
val name = "Singleton"
}
```
对象声明不可以是局部的, 也就是说, 不可以直接嵌套在函数之内.
但是, 可以嵌套在另一个对象声明之内, 或者嵌套在另一个非内部类(non-inner class)之内.
### 数据对象
如果在 Kotlin 中打印一个普通的对象声明, 它的字符串表达包含对象的名称和 hash 值:
```KOTLIN
object MyObject
fun main() {
println(MyObject)
// 输出结果为: MyObject@hashcode
}
```
但是, 如果使用 `data` 修饰符标记对象表达式,
你可以让编译器在调用 `toString()` 时返回对象真正的名称, 与 [数据类](data-classes.html) 的工作方式一样:
```KOTLIN
data object MyDataObject {
val number: Int = 3
}
fun main() {
println(MyDataObject)
// 输出结果为: MyDataObject
}
```
此外, 编译器还会为你的 `data object` 生成一些函数:
* `toString()` 返回数据对象的名称
* `equals()`/`hashCode()` 可以用于相等检查, 以及基于 hash 值的集合 Note: 你不能为 `data object` 的 `equals` 或 `hashCode` 函数提供自定义实现.
`data object` 的 `equals()` 函数会保证你的 `data object` 的所有对象都被看作相等.
大多数情况下, 你的 `data object` 在运行期只会存在单个实例, 因为 `data object` 声明的就是一个单例(singleton).
但是, 在某些特殊情况下, 也可以在运行期生成相同类型的其他对象 (例如, 通过 `java.lang.reflect` 使用平台的反射功能, 或通过底层使用了这个 API 的 JVM 序列化库),
这个功能可以确保这些对象被当作相等.
Warning:
请确保只对 `data objects` 进行结构化的相等比较 (使用 `==` 操作符), 而不要进行引用相等比较 (使用 `===` 操作符).
如果数据对象在运行期有一个以上的实例存在, 这样可以帮助你避免错误.
```KOTLIN
import java.lang.reflect.Constructor
data object MySingleton
fun main() {
val evilTwin = createInstanceViaReflection()
println(MySingleton)
// 输出结果为: MySingleton
println(evilTwin)
// 输出结果为: MySingleton
// 即使一个库强行创建了 MySingleton 的第二个实例,
// 它的 equals() 函数也会返回 true:
println(MySingleton == evilTwin)
// 输出结果为: true
// 不要使用 === 比较数据对象
println(MySingleton === evilTwin)
// 输出结果为: false
}
fun createInstanceViaReflection(): MySingleton {
// Kotlin 的反射功能不允许创建数据对象的实例.
// 这段代码 "强行" 创建新的 MySingleton 实例 (使用 Java 平台的反射功能)
// 在你的代码中一定不要这样做!
return (MySingleton.javaClass.declaredConstructors[0].apply { isAccessible = true } as Constructor).newInstance()
}
```
编译器生成的 `hashCode()` 函数的行为与 `equals()` 函数保持一致, 因此一个 `data object` 的所有运行期实例都拥有相同的 hash 值.
#### 数据对象与数据类的区别
尽管 `data object` 和 `data class` 声明经常一起使用, 而且很相似, 但对于 `data object` 有一些函数没有生成:
* 没有 `copy()` 函数. 因为 `data object` 声明通常用作单例, 因此不会生成 `copy()` 函数. 单例限制一个类只有单个实例, 如果允许创建实例的拷贝, 就破坏了只存在单个实例的原则.
* 没有 `componentN()` 函数. 与 `data class` 不同, `data object` 没有任何数据属性. 对这种没有数据属性的对象进行解构是没有意义的, 因此不会生成 `componentN()` 函数.
#### 在封闭层级结构(Sealed Hierarchy)中使用数据对象
数据对象声明非常适合在封闭层级结构(Sealed Hierarchy) 中使用, 例如 [封闭类或封闭接口](sealed-classes.html).
这样的方式允许你声明数据类和数据对象, 并保持对称性.
在这个示例中, 将 `EndOfFile` 声明为 `data object`, 而不是普通的 `object`,
代表它自动拥有 `toString()` 函数, 不需要手动的覆盖这个函数:
```KOTLIN
sealed interface ReadResult
data class Number(val number: Int) : ReadResult
data class Text(val text: String) : ReadResult
data object EndOfFile : ReadResult
fun main() {
println(Number(7))
// 输出结果为: Number(number=7)
println(EndOfFile)
// 输出结果为: EndOfFile
}
```
### 同伴对象(Companion Object)
同伴对象(Companion Object) 可以用来定义类级的函数和属性.
因此可以很容易的创建工厂方法, 声明常数, 访问共用的工具函数.
一个类内部的对象声明, 可以使用 `companion` 关键字标记为同伴对象:
```KOTLIN
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}
```
访问 `companion object` 的成员时, 可以直接使用类名称作为限定符:
```KOTLIN
class User(val name: String) {
// 定义一个同伴对象, 作为创建 User 实例的工厂
companion object Factory {
fun create(name: String): User = User(name)
}
}
fun main(){
// 使用类名称作为限定符, 调用同伴对象的工厂方法.
// 创建一个新的 User 实例
val userInstance = User.create("John Doe")
println(userInstance.name)
// 输出结果为: John Doe
}
```
`companion object` 的名称可以省略, 如果省略, 会使用默认名称 `Companion`:
```KOTLIN
class User(val name: String) {
// 定义一个同伴对象, 不指定名称
companion object { }
}
// 访问同伴对象
val companionUser = User.Companion
```
类的成员可以访问对应的 `companion object` 的 `private` 成员:
```KOTLIN
class User(val name: String) {
companion object {
private val defaultGreeting = "Hello"
}
fun sayHi() {
println(defaultGreeting)
}
}
User("Nick").sayHi()
// 输出结果为: Hello
```
直接使用一个类的名称时, 表示对这个类的同伴对象的引用, 无论同伴对象有没有名称:
```KOTLIN
//sampleStart
class User1 {
// 定义一个同伴对象, 有名称
companion object Named {
fun show(): String = "User1's Named Companion Object"
}
}
// 使用类名称引用 User1 的同伴对象
val reference1 = User1
class User2 {
// 定义一个同伴对象, 没有名称
companion object {
fun show(): String = "User2's Companion Object"
}
}
// 使用类名称引用 User2 的同伴对象
val reference2 = User2
//sampleEnd
fun main() {
// 对 User1 的同伴对象调用 show() 函数
println(reference1.show())
// 输出结果为: User1's Named Companion Object
// 对 User2 的同伴对象调用 show() 函数
println(reference2.show())
// 输出结果为: User2's Companion Object
}
```
尽管 Kotlin 中的同伴对象的成员看起来很像其他语言中的类的静态成员(static member),
但它们实际上是同伴对象的实例成员, 也就是说它们属于对象自身,
因此同伴对象可以实现接口:
```KOTLIN
interface Factory {
fun create(name: String): T
}
class User(val name: String) {
// 定义一个同伴对象, 实现 Factory 接口
companion object : Factory {
override fun create(name: String): User = User(name)
}
}
fun main() {
// 将同伴对象作为 Factory 使用
val userFactory: Factory = User
val newUser = userFactory.create("Example User")
println(newUser.name)
// 输出结果为: Example User
}
```
但是, 在 JVM 上, 如果使用 `@JvmStatic` 注解, 你可以让同伴对象的成员被编译为真正的静态方法(static method)和静态域(static field).
详情请参见 [与 Java 的互操作性](java-to-kotlin-interop.html#static-fields).
## 对象表达式(Object expression)
对象表达式(object expression) 会声明一个类, 并为这个类创建一个实例, 但类和实例都没有名称.
这些类适合一次性使用. 这种类可以从头开始创建, 也可以从既有的类继承, 或者实现接口.
这些类的实例称为 匿名对象, 因为它们通过表达式来定义, 而不是通过名称.
### 从头创建匿名对象
对象表达式以 `object` 关键字起始.
如果对象不继承任何类也不实现任何接口, 你可以直接在 `object` 关键字之后的大括号内定义对象的成员:
```KOTLIN
fun main() {
//sampleStart
val helloWorld = object {
val hello = "Hello"
val world = "World"
// 对象表达式继承 Any 类型, 已经有了 toString() 函数,
// 因此必须覆盖这个函数
override fun toString() = "$hello $world"
}
print(helloWorld)
// 输出结果为: Hello World
//sampleEnd
}
```
### 从基类继承匿名对象
要创建一个继承自某个类(或多个类)的匿名对象, 需要在 `object` 关键字和冒号 `:` 之后指定基类.
然后实现或覆盖基类的成员, 就和你在 [继承](inheritance.html) 这个基类时一样:
```KOTLIN
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { /*...*/ }
override fun mouseEntered(e: MouseEvent) { /*...*/ }
})
```
如果某个基类有构造器, 那么必须向构造器传递适当的参数.
要指定多个基类, 可以用逗号分隔, 放在冒号之后:
```KOTLIN
//sampleStart
// 创建一个 open 类 BankAccount, 包含 balance 属性
open class BankAccount(initialBalance: Int) {
open val balance: Int = initialBalance
}
// 定义一个接口 Transaction, 包含 execute() 函数
interface Transaction {
fun execute()
}
// 这个函数对一个 BankAccount 执行特殊交易
fun specialTransaction(account: BankAccount) {
// 创建一个匿名对象, 继承 BankAccount 类, 并实现 Transaction 接口
// 指定的 account 的 balance 被传递给 BankAccount 超类的构造器
val temporaryAccount = object : BankAccount(account.balance), Transaction {
override val balance = account.balance + 500 // 临时的奖金
// 实现 Transaction 接口的 execute() 函数
override fun execute() {
println("Executing special transaction. New balance is $balance.")
}
}
// 执行交易
temporaryAccount.execute()
}
//sampleEnd
fun main() {
// 创建一个 BankAccount, 初始的 balance 值为 1000
val myAccount = BankAccount(1000)
// 对创建的 account 执行特殊交易
specialTransaction(myAccount)
// 输出结果为: Executing special transaction. New balance is 1500.
}
```
### 将匿名对象用作返回类型或值类型
当你从一个局部的, 或 [private](visibility-modifiers.html#packages) 的函数或属性,
返回一个匿名对象, 那么通过这个函数或属性可以访问匿名对象的所有成员:
```KOTLIN
//sampleStart
class UserPreferences {
private fun getPreferences() = object {
val theme: String = "Dark"
val fontSize: Int = 14
}
fun printPreferences() {
val preferences = getPreferences()
println("Theme: ${preferences.theme}, Font Size: ${preferences.fontSize}")
}
}
//sampleEnd
fun main() {
val userPreferences = UserPreferences()
userPreferences.printPreferences()
// 输出结果为: Theme: Dark, Font Size: 14
}
```
因此你可以返回一个包含特定属性的匿名对象, 提供一种简单的方式来封装数据或行为, 而不必创建一个单独的类.
如果返回匿名对象的函数或属性的可见度为 `public`, `protected`, 或 `internal`, 那么它的真实类型为:
* 如果匿名对象没有声明基类型, 则类型为 `Any`.
* 如果匿名对象声明了唯一一个基类型, 则类型为这个基类型.
* 如果匿名对象声明了多个基类型, 则需要为这个函数或属性明确声明类型.
在这些情况中, 通过这个函数或属性的返回值, 对于匿名对象新添加的成员, 不可访问.
对于匿名对象覆盖的成员, 如果定义在这个函数或属性的真实类型中, 则可以访问. 例如:
```KOTLIN
//sampleStart
interface Notification {
// 在 Notification 接口中声明 notifyUser()
fun notifyUser()
}
interface DetailedNotification
class NotificationManager {
// 返回类型为 Any. 不能访问 message 属性.
// 当返回类型为 Any 时, 只能访问 Any 类的成员.
fun getNotification() = object {
val message: String = "General notification"
}
// 返回类型为 Notification, 因为匿名对象只实现一个接口
// 可以访问 notifyUser() 函数, 因为它是 Notification 接口的一部分
// 不能访问 message 属性, 因为它没有在 Notification 接口中声明
fun getEmailNotification() = object : Notification {
override fun notifyUser() {
println("Sending email notification")
}
val message: String = "You've got mail!"
}
// 返回类型为 DetailedNotification. 不能访问 notifyUser() 函数和 message 属性
// 只能访问 DetailedNotification 接口中声明的成员
fun getDetailedNotification(): DetailedNotification = object : Notification, DetailedNotification {
override fun notifyUser() {
println("Sending detailed notification")
}
val message: String = "Detailed message content"
}
}
//sampleEnd
fun main() {
// 这里不会产生输出
val notificationManager = NotificationManager()
// 这里不能访问 message 属性, 因为返回类型为 Any
// 这里不会产生输出
val notification = notificationManager.getNotification()
// 可以访问 notifyUser() 函数
// 这里不能访问 message 属性, 因为返回类型为 Notification
val emailNotification = notificationManager.getEmailNotification()
emailNotification.notifyUser()
// 输出结果为: Sending email notification
// 这里不能访问 notifyUser() 函数和 message 属性, 因为返回类型为 DetailedNotification
// 这里不会产生输出
val detailedNotification = notificationManager.getDetailedNotification()
}
```
### 通过匿名对象访问变量
对象表达式 body 部之内的代码, 可以访问创建这个对象的代码范围内的变量:
```KOTLIN
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
fun countClicks(window: JComponent) {
var clickCount = 0
var enterCount = 0
// MouseAdapter provides default implementations for mouse event functions
// Simulates MouseAdapter handling mouse events
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
clickCount++
}
override fun mouseEntered(e: MouseEvent) {
enterCount++
}
})
// 在对象表达式内部, 可以访问 clickCount 和 enterCount 变量
}
```
## 对象声明与对象表达式在行为上的区别
对象声明与对象表达式在初始化上存在一些区别:
* 对象表达式则会在使用处 立即 执行(并且初始化).
* 对象声明是 延迟(lazily) 初始化的, 只会在首次访问时才会初始化.
* 同伴对象会在对应的类被装载(解析)时初始化, 语义上等价于 Java 的静态初始化代码块(static initializer).
# 封闭类(Sealed Class)与封闭接口(Sealed Interface)
封闭 类和接口提供了对类的继承关系进行控制的方式.
一个封闭类的所有的直接子类(Direct Subclass)在编译时刻就能够确定.
在定义封闭类的模块和包之外, 不可能再出现其他子类.
对于封闭接口和它们的实现类也是如此: 在含有封闭接口的模块编译完成之后, 就不可能再创建新的实现类.
Note:
直接子类(Direct Subclass) 是指直接从父类继承的那些类.
间接子类(Indirect Subclass) 是指从父类继承, 但继承关系超过一层的那些类.
当你将封闭类和接口与 `when` 表达式一起使用时,
你可以覆盖所有可能的子类的行为, 并保证不会有新的子类创建出来, 对你的代码产生不利的影响.
封闭类最适合使用的场景是:
* 期望限制类的继承: 你有一组预定义的, 有限的子类, 来扩展一个类, 所有这些子类在编译期都是已知的.
* 需要实现类型安全的设计: 安全性和模式匹配在你的项目中至关重要. 特别是对于状态管理, 或处理复杂的条件逻辑. 例如, 请参见 [和 when 表达式一起使用封闭类](#use-sealed-classes-with-when-expression).
* 使用封闭的 API: 你希望为库提供健壮而且可维护的公开 API, 确保第三方客户端按预期的方式使用 API.
更详细的实际应用, 请参见 [使用场景](#use-case-scenarios).
Tip:
Java 15 引入了 [一个类似的概念](https://docs.oracle.com/en/java/javase/15/language/sealed-classes-and-interfaces.html#GUID-0C709461-CC33-419A-82BF-61461336E65F),
它的封闭类使用 `sealed` 关键字和 `permits` 子句, 来定义受限制的层级结构.
## 声明封闭类或接口
要声明一个封闭类或接口, 请使用 `sealed` 修饰符:
```KOTLIN
// 创建一个封闭接口
sealed interface Error
// 创建一个封闭类, 实现封闭接口 'Error'
sealed class IOError(): Error
// 定义子类, 继承封闭类 'IOError'
class FileReadError(val file: File): IOError()
class DatabaseError(val source: DataSource): IOError()
// 创建一个单子对象, 实现封闭接口 'Error'
object RuntimeError : Error
```
这个示例可以代表一个库的 API, 其中包含很多错误类, 以便类的使用者能够处理库可能抛出的错误.
如果这些错误类的继承层级包含在公开 API 可见的接口或抽象类,
那么就不能禁止其他开发者在他们的代码中实现这些接口或扩展这些抽象类.
由于库不知道在它外部定义的错误类, 因此库不能像它自己定义的类那样一致的处理这些外部定义的类.
如果将错误类的继承阶层封闭起来, 库的作者就能够确定的知道所有可能的错误类型, 并且能够确定以后不会出现其他错误类型.
但是, 使用 封闭的 错误类层级结构, 库的作者就能够确定他们知道了所有可能的错误类型, 而且之后也不会出现其他的错误类型.
示例代码中的层级关系如下:

### 构造器
封闭类本身永远是 [抽象(abstract)类](classes.html#abstract-classes), 因此, 不能直接生成它的实例.
但是, 它可以包含或继承构造器.
这些构造器不是用来创建封闭类自身的实例, 而是用来创建它的子类.
我们来看看下面的例子, 有一个封闭类 `Error`, 以及它的几个子类, 我们创建这些子类的实例:
```KOTLIN
sealed class Error(val message: String) {
class NetworkError : Error("Network failure")
class DatabaseError : Error("Database cannot be reached")
class UnknownError : Error("An unknown error has occurred")
}
fun main() {
val errors = listOf(Error.NetworkError(), Error.DatabaseError(), Error.UnknownError())
errors.forEach { println(it.message) }
}
// 输出结果为
// Network failure
// Database cannot be reached
// An unknown error has occurred
```
你可以在你的封闭类中使用 [enum](enum-classes.html) 类, 用枚举常数来表示状态, 并提供更多细节信息.
每个枚举常数只存在 单个 实例, 而封闭类的子类可以有 多个 实例.
在下面的示例中, `sealed class Error` 和它的几个子类, 使用 `enum` 来表示错误的严重级别.
每个子类的构造器会初始化 `severity`, 并改变它的状态:
```KOTLIN
enum class ErrorSeverity { MINOR, MAJOR, CRITICAL }
sealed class Error(val severity: ErrorSeverity) {
class FileReadError(val file: File): Error(ErrorSeverity.MAJOR)
class DatabaseError(val source: DataSource): Error(ErrorSeverity.CRITICAL)
object RuntimeError : Error(ErrorSeverity.CRITICAL)
// 这里可以添加更多错误类型
}
```
封闭类的构造器的 [可见度](visibility-modifiers.html) 必须是: `protected` (默认值) 或 `private`:
```KOTLIN
sealed class IOError {
// 封闭类的构造器默认可见度为 protected. 构造器在这个类和它的子类中可见.
constructor() { /*...*/ }
// private 构造器, 只在这个类中可见.
// 在封闭类中使用 private 构造器, 可以更加严格的控制实例的创建, 实现类中特定的初始化过程.
private constructor(description: String): this() { /*...*/ }
// 这里会发生错误, 因为在封闭类中不允许使用 public 和 internal 构造器
// public constructor(code: Int): this() {}
}
```
## 继承
封闭类和接口的直接子类必须定义在同一个包之内. 可以是顶级位置, 也可以嵌套在任意多的其他有名称的类, 有名称的接口, 或有名称的对象之内.
子类可以设置为任意的 [可见度](visibility-modifiers.html), 只要它们符合 Kotlin 中通常的类继承规则.
封闭类的子类必须拥有一个适当的限定名称. 不能是局部对象或匿名对象.
Note:
`enum` 类不能扩展封闭类, 也不能扩展任何其他类. 但是, 它们可以实现封闭接口:
```KOTLIN
sealed interface Error
// 枚举类扩展封闭接口 'Error'
enum class ErrorType : Error {
FILE_ERROR, DATABASE_ERROR
}
```
这些限制不适用于非直接子类. 如果封闭的类一个直接子类没有标记为封闭,
那么它可以按照其修饰符允许的方式任意扩展:
```KOTLIN
// 封闭接口 'Error' 只在相同的模块和包中存在实现类
sealed interface Error
// 封闭类 'IOError' 扩展 'Error', 只能在相同的包中扩展 'IOError'
sealed class IOError(): Error
// 开放类 'CustomError' 扩展 'Error', 可以在 'CustomError' 可见的任何地方扩展这个类
open class CustomError(): Error
```
### 跨平台项目中的继承
在 [跨平台项目](get-started.html) 中还存在一种继承限制:
封闭类的直接子类必须放在同一个 [源代码集(Source Set)](multiplatform-discover-project.html#source-sets) 中.
这个限制适用于没有使用 [expect 和 actual 修饰符](multiplatform-expect-actual.html) 的封闭类.
如果封闭类声明为共通源代码集(common source set)中的 `expect`, 并且在平台相关的代码集内拥有 `actual` 实现类,
那么 `expect` 和 `actual` 的版本在各自的源代码集内都可以拥有子类.
此外, 如果你使用了层级结构(hierarchical structure),
你可以在 `expect` 和 `actual` 声明之间的任何源代码集内创建子类.
更多详情请参见 [跨平台项目的层级结构(hierarchical structure)](multiplatform-hierarchy.html).
## 和 when 表达式一起使用封闭类
使用封闭类的主要好处, 是在 [when](control-flow.html#when-expressions-and-statements) 表达式中的使用场景.
`when` 表达式和封闭类一起使用时, Kotlin 编译器能够进行穷尽的检查, 是否覆盖了所有可能的情况.
这样的情况下, 你可以不必添加 `else` 分支:
```KOTLIN
// 封闭类和它的子类
sealed class Error {
class FileReadError(val file: String): Error()
class DatabaseError(val source: String): Error()
object RuntimeError : Error()
}
//sampleStart
// 将错误输出到日志的函数
fun log(e: Error) = when(e) {
is Error.FileReadError -> println("Error while reading file ${e.file}")
is Error.DatabaseError -> println("Error while reading from database ${e.source}")
Error.RuntimeError -> println("Runtime error")
// 不需要 `else` 分支, 因为已经覆盖了所有的可能情况
}
//sampleEnd
// 所有错误的列表
fun main() {
val errors = listOf(
Error.FileReadError("example.txt"),
Error.DatabaseError("usersDatabase"),
Error.RuntimeError
)
errors.forEach { log(it) }
}
```
Tip:
为了减少 `when` 表达式中的重复代码, 请试用上下文敏感的解析(Context-Sensitive Resolution)功能 (目前是预览版).
在匹配封闭类成员时, 如果预期的类型已知, 这个功能允许省略类型名称.
详情请参见 [预览版功能: 上下文敏感的解析(Context-Sensitive Resolution)](whatsnew22.html#preview-of-context-sensitive-resolution),
或相关的 [KEEP 提案](https://github.com/Kotlin/KEEP/blob/improved-resolution-expected-type/proposals/context-sensitive-resolution.md).
`when` 表达式和封闭类一起使用时, 你也可以添加保护条件(Guard Condition), 在单个分支中指定额外的检查.
详情请参见 [when 表达式中的保护条件(Guard Condition)](control-flow.html#guard-conditions-in-when-expressions).
Note:
在跨平台项目中, 如果与 `when` 表达式一起使用的封闭类, 是你的共通代码中的 [预期声明](multiplatform-expect-actual.html),
那么仍然需要 `else` 分支.
这是因为, `actual` 平台实现中的子类可以扩展封闭类, 但在共通代码中, 无法确定这些子类.
## 使用场景
我们来看看一些实际的使用场景, 封闭类和封闭接口可以非常有用.
### UI 应用程序中的状态管理
你可以使用封闭类来表示应用程序中的不同 UI 状态.
这种方法可以实现结构化并且安全的 UI 变更管理.
下面的例子演示如何管理不同的 UI 状态:
```KOTLIN
sealed class UIState {
data object Loading : UIState()
data class Success(val data: String) : UIState()
data class Error(val exception: Exception) : UIState()
}
fun updateUI(state: UIState) {
when (state) {
is UIState.Loading -> showLoadingIndicator()
is UIState.Success -> showData(state.data)
is UIState.Error -> showError(state.exception)
}
}
```
### 处理支付方式
在一些实际的商业应用程序中, 高效的处理各种支付方式是一种常见的需求.
你可以使用封闭类 和 `when` 表达式来实现这样的业务逻辑.
将不同的支付方式表达为封闭类的子类, 可以为交易过程的处理实现一个清晰而且易于管理的结构:
```KOTLIN
sealed class Payment {
data class CreditCard(val number: String, val expiryDate: String) : Payment()
data class PayPal(val email: String) : Payment()
data object Cash : Payment()
}
fun processPayment(payment: Payment) {
when (payment) {
is Payment.CreditCard -> processCreditCardPayment(payment.number, payment.expiryDate)
is Payment.PayPal -> processPayPalPayment(payment.email)
is Payment.Cash -> processCashPayment()
}
}
```
`Payment` 是一个封闭类, 表示电子商务系统中的各种支付方式: `CreditCard`, `PayPal`, 和 `Cash`.
每个子类可以拥有它独自的属性, 例如 `CreditCard` 有 `number` 和 `expiryDate`, `PayPal` 有 `email`.
`processPayment()` 函数演示如何处理不同的支付方式.
这种方案可以确保考虑到了所有可能的支付类型, 而且系统保持了灵活性, 可以在将来添加新的支付方式.
### 处理 API 请求/应答
你可以使用封闭类和封闭接口来实现一个用户认证系统, 它处理 API 的请求和应答.
用户认证系统有登入和登出功能.
`ApiRequest` 封闭接口定义了特定的请求类型: `LoginRequest` 用于登入操作, `LogoutRequest` 用于登出操作.
封闭类, `ApiResponse`, 包括不同的应答场景: `UserSuccess`, 其中包含用户数据, `UserNotFound`, 表示用户不存在, `Error`, 表示失败.
`handleRequest` 函数使用 `when` 表达式, 以一种类型安全的方式处理这些请求, `getUserById` 函数模拟用户检索:
```KOTLIN
// 引入必须的模块
import io.ktor.server.application.*
import io.ktor.server.resources.*
import kotlinx.serialization.*
// 定义封闭接口, 表示使用 Ktor 资源的 API 请求
@Resource("api")
sealed interface ApiRequest
@Serializable
@Resource("login")
data class LoginRequest(val username: String, val password: String) : ApiRequest
@Serializable
@Resource("logout")
object LogoutRequest : ApiRequest
// 定义封闭类 ApiResponse, 包括具体的应答类型
sealed class ApiResponse {
data class UserSuccess(val user: UserData) : ApiResponse()
data object UserNotFound : ApiResponse()
data class Error(val message: String) : ApiResponse()
}
// 用户数据类, 在成功应答中使用
data class UserData(val userId: String, val name: String, val email: String)
// 这个函数校验用户凭证 (只为演示用)
fun isValidUser(username: String, password: String): Boolean {
// 使用固定的校验逻辑 (这只是一段演示代码)
return username == "validUser" && password == "validPass"
}
// 这个函数使用具体的应答来处理 API 请求
fun handleRequest(request: ApiRequest): ApiResponse {
return when (request) {
is LoginRequest -> {
if (isValidUser(request.username, request.password)) {
ApiResponse.UserSuccess(UserData("userId", "userName", "userEmail"))
} else {
ApiResponse.Error("Invalid username or password")
}
}
is LogoutRequest -> {
// 这个示例假设 logout 操作永远成功
ApiResponse.UserSuccess(UserData("userId", "userName", "userEmail")) // 演示用
}
}
}
// 这个函数模拟一个 getUserById 调用
fun getUserById(userId: String): ApiResponse {
return if (userId == "validUserId") {
ApiResponse.UserSuccess(UserData("validUserId", "John Doe", "john@example.com"))
} else {
ApiResponse.UserNotFound
}
// 错误处理也会生成错误应答.
}
// 主函数, 演示使用方法
fun main() {
val loginResponse = handleRequest(LoginRequest("user", "pass"))
println(loginResponse)
val logoutResponse = handleRequest(LogoutRequest)
println(logoutResponse)
val userResponse = getUserById("validUserId")
println(userResponse)
val userNotFoundResponse = getUserById("invalidId")
println(userNotFoundResponse)
}
```
# 枚举类
枚举类最基本的使用场景, 就是实现类型安全的枚举值:
```KOTLIN
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
```
每个枚举常数都是一个对象. 枚举常数之间用逗号分隔.
由于每个枚举值都是枚举类的一个实例, 因此枚举值可以这样初始化:
```KOTLIN
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
```
## 匿名类
枚举常数可以定义它自己的匿名类, 这些匿名类可以拥有各自的方法, 也可以覆盖基类的方法:
```KOTLIN
enum class ProtocolState {
WAITING {
override fun signal() = TALKING
},
TALKING {
override fun signal() = WAITING
};
abstract fun signal(): ProtocolState
}
```
如果枚举类中定义了任何成员, 需要用分号将枚举常数的定义与枚举类的成员定义分隔开.
## 在枚举类中实现接口
枚举类也可以实现接口 (但不能继承其他类), 对于接口的成员函数,
可以为所有的枚举常数提供一个共同的实现, 也可以在不同的枚举常数的匿名类中提供不同的实现.
枚举类实现接口时, 只需要在枚举类的声明中加入希望实现的接口名, 示例如下:
```KOTLIN
import java.util.function.BinaryOperator
import java.util.function.IntBinaryOperator
//sampleStart
enum class IntArithmetics : BinaryOperator, IntBinaryOperator {
PLUS {
override fun apply(t: Int, u: Int): Int = t + u
},
TIMES {
override fun apply(t: Int, u: Int): Int = t * u
};
override fun applyAsInt(t: Int, u: Int) = apply(t, u)
}
//sampleEnd
fun main() {
val a = 13
val b = 31
for (f in IntArithmetics.entries) {
println("$f($a, $b) = ${f.apply(a, b)}")
}
}
```
所有的枚举类都默认实现了 [Comparable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-comparable/index.html) 接口.
枚举常数值的大小顺序, 等于它在枚举类中的定义顺序.
详情请参见 [排序(Ordering)](collection-ordering.html).
## 使用枚举常数
Kotlin 中的枚举类拥有编译器添加的合成的(synthetic)属性和方法, 可以列出枚举类中定义的所有枚举常数值, 可以通过枚举常数值的名称字符串得到对应的枚举常数值.
这些方法的签名如下(这里假设枚举类名称为 `EnumClass`):
```KOTLIN
EnumClass.valueOf(value: String): EnumClass
EnumClass.entries: EnumEntries // 专门的 List
```
下面是这些属性和方法的使用示例:
```KOTLIN
enum class RGB { RED, GREEN, BLUE }
fun main() {
for (color in RGB.entries) println(color.toString()) // 输出结果为 RED, GREEN, BLUE
println("The first color is: ${RGB.valueOf("RED")}") // 输出结果为 "The first color is: RED"
}
```
如果给定的名称不能匹配枚举类中定义的任何一个枚举常数值, `valueOf()` 方法会抛出 `IllegalArgumentException` 异常.
在 Kotlin 1.9.0 引入 `entries` 之前, 是使用 `values()` 函数来取得枚举常数的数组.
每个枚举常数值也拥有属性:
[name](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-enum/name.html)
和
[ordinal](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-enum/ordinal.html),
可以取得它的名称, 以及在枚举类中声明的顺序(从 0 开始):
```KOTLIN
enum class RGB { RED, GREEN, BLUE }
fun main() {
//sampleStart
println(RGB.RED.name) // 输出结果为: RED
println(RGB.RED.ordinal) // 输出结果为: 0
//sampleEnd
}
```
Tip:
为了减少使用枚举常数时的重复代码, 请试用上下文敏感的解析(Context-Sensitive Resolution)功能 (目前是预览版).
如果预期的类型已知, 这个功能允许省略枚举类名称, 例如在 `when` 表达式中使用时, 或者赋值给有类型的变量时.
详情请参见 [预览版功能: 上下文敏感的解析(Context-Sensitive Resolution)](whatsnew22.html#preview-of-context-sensitive-resolution),
或相关的 [KEEP 提案](https://github.com/Kotlin/KEEP/blob/improved-resolution-expected-type/proposals/context-sensitive-resolution.md).
你可以通过
[enumValues<T>()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/enum-values.html)
和
[enumValueOf<T>()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/enum-value-of.html)
函数, 以泛型方式取得枚举类中的常数.
在 Kotlin 2.0.0 中, 引入了 [enumEntries<T>()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.enums/enum-entries.html)
函数, 作为
[enumValues<T>()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/enum-values.html)
函数的替代.
`enumEntries()` 函数会对指定的枚举类型 `T` 返回一个 List, 包含所有的枚举值.
Kotlin 仍然支持 `enumValues()` 函数, 但我们推荐你改为使用 `enumEntries()` 函数, 因为它的性能损失较少.
每次调用 `enumValues()` 都会创建一个新的数组, 而每次调用 `enumEntries()` 都会返回相同的 List, 这样要高效得多.
例如:
```KOTLIN
enum class RGB { RED, GREEN, BLUE }
inline fun > printAllValues() {
println(enumEntries().joinToString { it.name })
}
printAllValues()
// 输出结果为 RED, GREEN, BLUE
```
Tip:
关于内联函数(inline function)和实体化的类型参数(Reified type parameter), 详情请参见 [内联函数](inline-functions.html).
# 内联的值类(Inline value class)
如果将值封装到类中, 创建一些特定领域的类型, 有时候会非常有用. 但是, 这就会产生堆上的内存分配, 带来运行时的性能损失.
更坏的情况下, 如果被包装的类是基本类型, 那么性能损失会非常严重, 因为在运行时对基本类型本来可以进行极大地性能优化, 而它的包装类却不能享受这种好处.
为了解决这类问题, Kotlin 引入了一种特别的类, 称为 内联类(inline class),
内联类是 [基于值的类(value-based class)](https://github.com/Kotlin/KEEP/blob/master/notes/value-classes.md)的一个子集.
这种类没有标识符, 只用于包含值.
声明内联类时, 在类名称之前添加 `value` 修饰符:
```KOTLIN
value class Password(private val s: String)
```
要在 JVM 后端上声明内联类, 需要在类的定义之前使用 `value` 修饰符和 `@JvmInline` 注解:
```KOTLIN
// 针对 JVM 后端
@JvmInline
value class Password(private val s: String)
```
内联类必须拥有唯一的一个属性, 并在主构造器中初始化这个属性.
在运行期, 会使用这个唯一的属性来表达内联类的实例(关于运行期的内部表达, 请参见 [下文](#representation)):
```KOTLIN
// 'Password' 类的实例不会真实存在
// 在运行期, 'securePassword' 只包含 'String'
val securePassword = Password("Don't try this in production")
```
这就是内联类的主要功能, 受 内联 这个名称的启发而来: 类中的数据被 内联 到使用它的地方
(类似于 [内联函数](inline-functions.html) 的内容被内联到调用它的地方).
## 成员
内联类支持与通常的类相同的功能.
具体来说, 内联类可以声明属性和函数, 也可以有 `init` 代码段和 [次级构造器(secondary constructor)](classes.html#secondary-constructors):
```KOTLIN
@JvmInline
value class Person(private val fullName: String) {
init {
require(fullName.isNotEmpty()) {
"Full name shouldn't be empty"
}
}
constructor(firstName: String, lastName: String) : this("$firstName $lastName") {
require(lastName.isNotBlank()) {
"Last name shouldn't be empty"
}
}
val length: Int
get() = fullName.length
fun greet() {
println("Hello, $fullName")
}
}
fun main() {
val name1 = Person("Kotlin", "Mascot")
val name2 = Person("Kodee")
name1.greet() // `greet()` 函数会作为静态方法来调用
println(name2.length) // 属性的取值函数会作为静态方法来调用
}
```
内联类的属性不能拥有 [后端域变量](properties.html#backing-fields).
只能拥有简单的计算属性 (不能拥有 `lateinit` 属性或委托属性)
## 继承
内联类允许继承接口:
```KOTLIN
interface Printable {
fun prettyPrint(): String
}
@JvmInline
value class Name(val s: String) : Printable {
override fun prettyPrint(): String = "Let's $s!"
}
fun main() {
val name = Name("Kotlin")
println(name.prettyPrint()) // 仍然是调用静态方法
}
```
禁止内联类参与类继承. 也就是说, 内联类不能继承其他类, 而且它永远是 `final` 类, 不能被其他类继承.
## 内部表达
在通常的代码中, Kotlin 编译器会对每个内联类保留一个 包装.
内联类的实例在运行期可以表达为这个包装, 也可以表达为它的底层类型.
类似于 `Int` 可以 [表达](numbers.html#boxing-and-caching-numbers-on-the-jvm) 为基本类型 `int`,
也可以表达为包装类 `Integer`.
Kotlin 编译器会优先使用底层类型而不是包装类, 这样可以产生最优化的代码, 运行时的性能也会最好.
但是, 有些时候会需要保留包装类. 一般来说, 当内联类被用作其他类型时, 它会被装箱(box).
```KOTLIN
interface I
@JvmInline
value class Foo(val i: Int) : I
fun asInline(f: Foo) {}
fun asGeneric(x: T) {}
fun asInterface(i: I) {}
fun asNullable(i: Foo?) {}
fun id(x: T): T = x
fun main() {
val f = Foo(42)
asInline(f) // 拆箱: 用作 Foo 本身
asGeneric(f) // 被装箱: 被用作泛型类型 T
asInterface(f) // 被装箱: 被用作类型 I
asNullable(f) // 被装箱: 被用作 Foo?, 这个类型与 Foo 不同
// 下面的例子中, 'f' 首先被装箱(传递给 'id' 函数), 然后被拆箱 (从 'id' 函数返回)
// 最终, 'c' 中包含拆箱后的表达(也就是 '42'), 与 'f' 一样
val c = id(f)
}
```
由于内联类可以表达为底层类型和包装类两种方式, [引用相等性](equality.html#referential-equality) 对于内联类是毫无意义的,
因此禁止对内联类进行引用相等性判断操作.
内联类也可以使用泛型类型参数作为底层类型. 这种情况下, 编译器将它映射为 `Any?`,
或者更一般的说, 映射为类型参数的上界(Upper Bound).
```KOTLIN
@JvmInline
value class UserId(val value: T)
fun compute(s: UserId) {} // 编译器生成的代码是 fun compute-(s: Any?)
```
### 函数名称混淆
由于内联类被编译为它的底层类型, 因此可能会导致一些令人难以理解的错误, 比如, 意料不到的平台签名冲突:
```KOTLIN
@JvmInline
value class UInt(val x: Int)
// 在 JVM 平台上表达为 'public final void compute(int x)'
fun compute(x: Int) { }
// 在 JVM 平台上也表达为 'public final void compute(int x)'!
fun compute(x: UInt) { }
```
为了解决这种问题, 使用内联类的函数会被进行名称 混淆, 方法是对函数名添加一些稳定的哈希值.
因此, `fun compute(x: UInt)` 会表达为 `public final void compute-(int x)`,
然后就解决了函数名称的冲突问题.
### 在 Java 代码中调用
你可以在 Java 代码中调用接受内联类为参数的函数. 为了实现这一点, 你需要手动禁止函数名称混淆:
在函数声明之前添加 `@JvmName` 注解:
```KOTLIN
@JvmInline
value class UInt(val x: Int)
fun compute(x: Int) { }
@JvmName("computeUInt")
fun compute(x: UInt) { }
```
默认情况下, Kotlin 使用 未装箱的表达形式(unboxed representation) 来编译内联类, 因此在 Java 中难以访问.
关于如何将内联类编译为 装箱的表达形式(boxed representation), 使得 Java 中可以访问,
请参见向导 [在 Java 中调用 Kotlin](java-to-kotlin-interop.html#inline-value-classes).
## 内联类与类型别名
初看起来, 内联类似乎非常像 [类型别名](type-aliases.html). 确实, 它们都声明了一个新的类型, 并且在运行期都表达为各自的底层类型.
但是, 主要的差别在于, 类型别名与它的底层类型是 赋值兼容 的 (与同一个底层类型的另一个类型别名, 也是兼容的), 而内联类不是如此.
也就是说, 内联类会生成一个真正的 新 类型,
相反, 类型别名只是给既有的类型定义了一个新的名字(也就是别名):
```KOTLIN
typealias NameTypeAlias = String
@JvmInline
value class NameInlineClass(val s: String)
fun acceptString(s: String) {}
fun acceptNameTypeAlias(n: NameTypeAlias) {}
fun acceptNameInlineClass(p: NameInlineClass) {}
fun main() {
val nameAlias: NameTypeAlias = ""
val nameInlineClass: NameInlineClass = NameInlineClass("")
val string: String = ""
acceptString(nameAlias) // 正确: 需要底层类型的地方, 可以传入类型别名
acceptString(nameInlineClass) // 错误: 需要底层类型的地方, 不能传入内联类
// 反过来:
acceptNameTypeAlias(string) // 正确: 需要类型别名的地方, 可以传入底层类型
acceptNameInlineClass(string) // 错误: 需要内联类的地方, 不能传入底层类型
}
```
## 内联类与代理
对于接口, 允许将它的实现代理给内联类的内联值:
```KOTLIN
interface MyInterface {
fun bar()
fun foo() = "foo"
}
@JvmInline
value class MyInterfaceWrapper(val myInterface: MyInterface) : MyInterface by myInterface
fun main() {
val my = MyInterfaceWrapper(object : MyInterface {
override fun bar() {
// 函数体
}
})
println(my.foo()) // 输出为 "foo"
}
```
# 嵌套类与内部类
类可以嵌套在另一个类之内:
```KOTLIN
class Outer {
private val bar: Int = 1
class Nested {
fun foo() = 2
}
}
val demo = Outer.Nested().foo() // == 2
```
你也可以对接口进行嵌套. 类和接口的所有组合都是允许的: 可以在类中嵌套接口, 在接口中嵌套类, 以及在接口中嵌套接口.
```KOTLIN
interface OuterInterface {
class InnerClass
interface InnerInterface
}
class OuterClass {
class InnerClass
interface InnerInterface
}
```
## 内部类(Inner class)
嵌套类可以使用 `inner` 关键字来标记, 然后就可以访问它的外部类(outer class)的成员. 内部类会保存一个引用, 指向外部类的对象实例:
```KOTLIN
class Outer {
private val bar: Int = 1
inner class Inner {
fun foo() = bar
}
}
val demo = Outer().Inner().foo() // == 1
```
在内部类中使用 `this` 关键字会产生歧义, 关于如何消除这种歧义, 请参见 [带限定符的 this 表达式](this-expressions.html).
## 匿名内部类(Anonymous inner class)
匿名内部类的实例使用 [对象表达式(object expression)](object-declarations.html#object-expressions) 来创建:
```KOTLIN
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { ... }
override fun mouseEntered(e: MouseEvent) { ... }
})
```
Note:
对于 JVM 平台, 如果这个对象是一个 Java 函数式接口的实例(也就是, 只包含唯一一个抽象方法的 Java 接口),
那么你可以使用带接口类型前缀的 Lambda 表达式来创建这个对象:
```KOTLIN
val listener = ActionListener { println("clicked") }
```
# 函数式 (SAM) 接口
只有一个抽象成员函数的接口称为 函数式接口 (Functional Interface), 或者叫做 单抽象方法(SAM, Single Abstract Method) 接口.
函数式接口可以拥有多个非抽象的成员函数, 但只能拥有一个抽象成员函数.
在 Kotlin 中声明函数式接口时, 请使用 `fun` 修饰符.
```KOTLIN
fun interface KRunnable {
fun invoke()
}
```
## SAM 转换功能
对于函数式接口, 可以通过 SAM 转换功能,
使用 [Lambda 表达式](lambdas.html#lambda-expressions-and-anonymous-functions), 让你的代码更加简洁易读.
你可以使用 Lambda 表达式, 而不必手动的创建一个类, 实现函数式接口.
只要 Lambda 表达式的签名与接口的唯一方法的签名相匹配, Kotlin 可以通过 SAM 转换功能,
将任意的 Lambda 表达式转换为一段代码, 创建一个实现接口的类的实例.
比如, 对于下面的 Kotlin 函数式接口:
```KOTLIN
fun interface IntPredicate {
fun accept(i: Int): Boolean
}
```
如果不使用 SAM 转换功能, 那么就需要编写这样的代码:
```KOTLIN
// 创建类的实例
val isEven = object : IntPredicate {
override fun accept(i: Int): Boolean {
return i % 2 == 0
}
}
```
使用 Kotlin 的 SAM 转换功能, 就可以编写下面的代码, 效果相同:
```KOTLIN
// 使用 Lambda 表达式创建实例
val isEven = IntPredicate { it % 2 == 0 }
```
这样, 就通过更加简短的 Lambda 表达式代替了所有其他不必要的代码.
```KOTLIN
fun interface IntPredicate {
fun accept(i: Int): Boolean
}
val isEven = IntPredicate { it % 2 == 0 }
fun main() {
println("Is 7 even? - ${isEven.accept(7)}")
}
```
也可以使用 [对 Java 接口的 SAM 转换功能](java-interop.html#sam-conversions).
## 从带构造器函数的接口迁移到函数式接口
从 1.6.20 开始, Kotlin 支持对函数式接口构造器的 [可调用的引用](reflection.html#callable-references),
因此增加了一种源代码兼容的方式, 可以从带构造器函数的接口迁移到函数式接口.
我们来看看以下代码:
```KOTLIN
interface Printer {
fun print()
}
fun Printer(block: () -> Unit): Printer = object : Printer {
override fun print() = block()
}
```
由于可以使用对函数式接口构造器的可调用的引用, 这段代码可以替换为函数式接口声明:
```KOTLIN
fun interface Printer {
fun print()
}
```
它的构造器会隐含的创建, 使用 `::Printer` 函数引用的任何代码都可以正确编译. 比如:
```KOTLIN
documentsStorage.addPrinter(::Printer)
```
如果要保留二进制兼容性, 可以对过去的函数 `Printer` 标记
[@Deprecated](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-deprecated/)
注解, 注解参数是 `DeprecationLevel.HIDDEN`:
```KOTLIN
@Deprecated(message = "Your message about the deprecation", level = DeprecationLevel.HIDDEN)
fun Printer(...) {...}
```
## 函数式接口 与 类型别名(Type Alias)
你也可以对函数类型使用 [类型别名(Type Alias)](type-aliases.html), 简单的重写上面的代码:
```KOTLIN
typealias IntPredicate = (i: Int) -> Boolean
val isEven: IntPredicate = { it % 2 == 0 }
fun main() {
println("Is 7 even? - ${isEven(7)}")
}
```
但是, 函数式接口 与 [类型别名(Type Alias)](type-aliases.html) 服务于不同的目的.
类型别名只是对已有的类型提供一个新的名称 – 它不会创建新的类型, 而函数式接口会.
对某个特定的函数式接口, 你可以提供扩展, 但对通常的函数或函数的类型别名则不可以.
类型别名只能拥有一个成员, 而函数式接口可以拥有多个非抽象的成员函数和一个抽象成员函数.
函数式接口也可以实现或继承其他接口.
函数式接口比类型别名更加灵活, 也提供了更多功能,
但语法上以及在运行时刻都存在更多代价, 因为需要转换为特定的接口.
当在你的代码中需要选择使用哪一种时, 应该考虑你的需求:
* 如果你的 API 需要接受一个函数 (任意的函数), 带有某些特定参数和返回类型 – 可以使用简单的函数类型, 或者为这个函数类型定义一个类型别名, 使得它的名称更简短.
* 如果你的 API 需要接受比函数更加复杂的实体 – 比如, 它带有比较重要的规约 和/或 操作, 无法表达为函数类型的签名 – 那么需要为它定义一个单独的函数式接口.
# 属性(Property)
在 Kotlin 中, 属性可以用来存储和管理数据, 而不需要编写用于访问或修改数据的函数.
你可以在 [类](classes.html), [接口](interfaces.html), [对象](object-declarations.html), [同伴对象](object-declarations.html#companion-objects) 中使用属性,
甚至可以在这些结构之外, 以顶级属性的形式使用.
每个属性都有一个名称, 一个类型, 以及自动生成的 `get()` 函数, 称为 getter.
你可以使用 getter 来读取属性的值.
如果属性是可变的, 它还有一个 `set()` 函数, 称为 setter, 可以修改属性的值.
Tip:
getter 和 setter 统称为 访问器(accessor).
## 声明属性
属性可以是可变属性(`var`), 或只读属性(`val`).
你可以在 `.kt` 文件中将它们声明为顶级属性. 顶级属性可以看作一个属于包的全局变量:
```KOTLIN
// 文件: Constants.kt
package my.app
val pi = 3.14159
var counter = 0
```
也可以在类, 接口或对象内声明属性:
```KOTLIN
// 包含属性的类
class Address {
var name: String = "Holmes, Sherlock"
var street: String = "Baker"
var city: String = "London"
}
// 包含属性的接口
interface ContactInfo {
val email: String
}
// 包含属性的对象
object Company {
var name: String = "Detective Inc."
val country: String = "UK"
}
// 实现接口的类
class PersonContact : ContactInfo {
override val email: String = "sherlock@example.com"
}
```
使用属性时, 只需通过属性名来引用它:
```KOTLIN
class Address {
var name: String = "Holmes, Sherlock"
var street: String = "Baker"
var city: String = "London"
}
interface ContactInfo {
val email: String
}
object Company {
var name: String = "Detective Inc."
val country: String = "UK"
}
class PersonContact : ContactInfo {
override val email: String = "sherlock@example.com"
}
//sampleStart
fun copyAddress(address: Address): Address {
val result = Address()
// 访问 result 实例的属性
result.name = address.name
result.street = address.street
result.city = address.city
return result
}
fun main() {
val sherlockAddress = Address()
val copy = copyAddress(sherlockAddress)
// 访问 copy 实例的属性
println("Copied address: ${copy.name}, ${copy.street}, ${copy.city}")
// 输出结果为: Copied address: Holmes, Sherlock, Baker, London
// 访问 Company 对象的属性
println("Company: ${Company.name} in ${Company.country}")
// 输出结果为: Company: Detective Inc. in UK
val contact = PersonContact()
// 访问 contact 实例的属性
println("Email: ${contact.email}")
// 输出结果为: Email: sherlock@email.com
}
//sampleEnd
```
在 Kotlin 中, 我们推荐在声明属性的同时进行初始化, 以保证代码的安全性和可读性.
但是, 在某些特殊情况下, 可以 [延迟初始化](#late-initialized-properties-and-variables).
如果编译器能够从初始化代码或 getter 的返回类型推断出属性的类型, 那么可以省略属性类型的声明:
```KOTLIN
var initialized = 1 // 推断类型为 Int
var allByDefault // 错误: 属性必须初始化.
```
## 自定义 getter 与 setter
默认情况下, Kotlin 会自动生成 getter 和 setter.
如果你需要额外的逻辑, 例如校验, 格式化, 或根据其他属性进行计算, 可以定义自定义访问器.
自定义 getter 在每次访问属性时执行:
```KOTLIN
//sampleStart
class Rectangle(val width: Int, val height: Int) {
val area: Int
get() = this.width * this.height
}
//sampleEnd
fun main() {
val rectangle = Rectangle(3, 4)
println("Width=${rectangle.width}, height=${rectangle.height}, area=${rectangle.area}")
}
```
如果编译器能够从 getter 推断出属性类型, 则可以省略类型:
```KOTLIN
val area get() = this.width * this.height
```
自定义 setter 在每次向属性赋值时执行, 初始化时除外.
按照惯例, setter 的参数名称为 `value`, 但你也可以选择不同的名称:
```KOTLIN
class Point(var x: Int, var y: Int) {
var coordinates: String
get() = "$x,$y"
set(value) {
val parts = value.split(",")
x = parts[0].toInt()
y = parts[1].toInt()
}
}
fun main() {
val location = Point(1, 2)
println(location.coordinates)
// 输出结果为: 1,2
location.coordinates = "10,20"
println("${location.x}, ${location.y}")
// 输出结果为: 10, 20
}
```
### 修改可见度或添加注解
在 Kotlin 中, 你可以修改访问器的可见度, 或者添加 [注解](annotations.html), 而不需要替换默认实现.
这些修改不必在方法 body 部 `{}` 内进行.
要修改访问器的可见度, 请在 `get` 或 `set` 关键字之前使用可见度修饰符:
```KOTLIN
class BankAccount(initialBalance: Int) {
var balance: Int = initialBalance
// 只有类自身能够修改 balance
private set
fun deposit(amount: Int) {
if (amount > 0) balance += amount
}
fun withdraw(amount: Int) {
if (amount > 0 && amount <= balance) balance -= amount
}
}
fun main() {
val account = BankAccount(100)
println("Initial balance: ${account.balance}")
// 输出结果为: 100
account.deposit(50)
println("After deposit: ${account.balance}")
// 输出结果为: 150
account.withdraw(70)
println("After withdrawal: ${account.balance}")
// 输出结果为: 80
// account.balance = 1000
// 错误: 无法赋值, 因为 setter 的可见度是 private
}
```
要对访问器添加注解, 请在 `get` 或 `set` 关键字之前使用注解:
```KOTLIN
// 定义一个可应用于 getter 的注解
@Target(AnnotationTarget.PROPERTY_GETTER)
annotation class Inject
class Service {
var dependency: String = "Default Service"
// 对 getter 添加注解
@Inject get
}
fun main() {
val service = Service()
println(service.dependency)
// 输出结果为: Default service
println(service::dependency.getter.annotations)
// 输出结果为: [@Inject()]
println(service::dependency.setter.annotations)
// 输出结果为: []
}
```
这个示例使用 [反射](reflection.html) 来显示 getter 和 setter 上存在的注解.
## 后端域变量(Backing Field)
如果属性的值需要存储在内存中, 编译器会自动为属性生成后端域变量(Backing Field).
例如, 当你使用默认的 `get()` 和 `set()` 函数时, 编译器会创建后端域变量, 因为它们需要读写存储的值:
```KOTLIN
var count = 0
```
在 [自定义 get() 或 set() 函数](#custom-getters-and-setters) 中, 可以使用 `field` 关键字来访问后端域变量.
例如, 可以向 getter 或 setter 中添加额外的逻辑, 或者在属性发生变化时触发额外的操作.
在下面的示例中, `score` 属性在 `set()` 函数中使用后端域变量, 使得更新值时同时触发一个日志事件:
```KOTLIN
class Scoreboard {
var score: Int = 0
set(value) {
field = value
// 更新值时添加日志
println("Score updated to $field")
}
}
fun main() {
val board = Scoreboard()
board.score = 10
// 输出结果为: Score updated to 10
board.score = 20
// 输出结果为: Score updated to 20
}
```
并不是所有属性都会默认创建后端域变量, 因为有些属性可能不需要.
例如, `isEmpty` 属性没有后端域变量, 因为每次访问时, 它的值都会从 `size` 属性计算得到:
```KOTLIN
val isEmpty: Boolean
get() = this.size == 0
```
### 明确的后端域变量(Explicit Backing Field)
有时你可能需要更多的灵活性. 例如, 如果你有一个 API, 希望能够在内部修改属性, 但不允许外部修改.
这种情况下, 可以使用 明确的后端域变量(Explicit Backing Field).
在下面的示例中, `ShoppingCart` 类有一个 `items` 属性, 代表购物车中的所有商品.
这个类将 `items` 属性公开为只读的字符串列表, 但在内部通过明确的后端域变量, 将数据存储在一个可变的列表中:
```KOTLIN
class ShoppingCart {
// 使用明确的后端域变量的公开只读视图
val items: List
field = mutableListOf()
fun addItem(item: String) {
items.add(item)
}
fun removeItem(item: String) {
items.remove(item)
}
}
fun main() {
val cart = ShoppingCart()
cart.addItem("Apple")
cart.addItem("Banana")
println(cart.items)
// 输出结果为: [Apple, Banana]
cart.removeItem("Apple")
println(cart.items)
// 输出结果为: [Banana]
}
```
在这个示例中, 编译器从 `mutableListOf()` 调用推断后端域变量的类型: `MutableList`.
你也可以明确的声明后端域变量的类型:
```KOTLIN
val items: List
// 具有明确类型的明确后端域变量
field: MutableList = mutableListOf()
```
在 `ShoppingCart` 类的示例中, 编译器将 `items` 属性智能转换(smart cast)为 `MutableList` 类型,
因此类可以通过 `add()` 和 `remove()` 函数向购物车中添加和删除商品.
在类的外部, 编译器使用公开的属性类型 `List`, 因此 API 使用者只能读取 `items` 列表中的内容.
#### 限制
使用明确的后端域变量时, 属性和后端域变量本身必须遵循一定的规则.
属性要使用明确的后端域变量, 必须满足以下条件:
* 没有自定义 getter.
* 是只读属性(`val`).
* 不是 `open` 的.
* 不是 [委托属性](delegated-properties.html).
* 不是 [编译期常数值](#compile-time-constants).
此外, 后端域变量的类型必须是属性类型的子类型, 且必须具有 [private 可见度](visibility-modifiers.html).
要绕过这些限制, 可以改为使用后端属性.
### 后端属性(Backing Property)
如果明确的后端域变量不适合你的使用场景, 你可以尝试使用一种名为 后端属性(Backing Property) 的编程模式.
例如, 如果你的属性需要自定义 getter:
```KOTLIN
class UserDirectory {
private val _users = mutableListOf(
"sarah",
"mike",
"emma"
)
val users: List
get() = _users.sorted()
fun addUser(username: String) {
_users.add(username)
}
}
fun main() {
val directory = UserDirectory()
directory.addUser("alex")
println(directory.users)
// 输出结果为: [alex, emma, mike, sarah]
}
```
Tip:
命名后端属性时, 请使用下划线前缀, 以符合 Kotlin [编码规约](coding-conventions.html#names-for-backing-properties).
在这个示例中, `UserDirectory` 类有一个只读属性 `users`, 列出目录中的所有用户.
`_users` 变量是 private 的后端属性, 包含真实的列表.
public 属性 `users` 的 getter 先对列表进行排序, 然后返回结果.
## 编译期常数值
如果只读属性的值在编译期间就能确定, 请使用 `const` 修饰符, 将它标记为 编译期常数值(Compile-Time Constant).
编译期常数值会在编译时内联(inline), 因此每处引用都会被替换为实际的值. 由于不会调用 getter, 因此访问效率更高:
```KOTLIN
// 文件: AppConfig.kt
package com.example
// 编译期常数值
const val MAX_LOGIN_ATTEMPTS = 3
```
编译期常数值必须满足以下所有条件:
* 必须是顶级属性, 或者是 [object 声明](object-declarations.html#object-declarations-overview) 的成员, 或者是 [同伴对象](object-declarations.html#companion-objects) 的成员.
* 值必须初始化为 `String` 类型或 [基本类型](types-overview.html).
* 不能有自定义 getter.
编译期常数值仍然有后端域变量, 因此你可以使用 [反射](reflection.html) 与它进行交互.
这类属性也可以在注解内使用:
```KOTLIN
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"
@Deprecated(SUBSYSTEM_DEPRECATED) fun processLegacyOrders() { ... }
```
## 延迟初始化的(Late-Initialized)属性和变量
通常, 属性必须在构造器中进行初始化. 但是, 并不总是方便这样做.
例如, 你可能通过依赖注入来初始化属性, 或者在单元测试的 setup 方法中初始化属性.
要处理这些情况, 请为属性添加 `lateinit` 修饰符:
```KOTLIN
public class OrderServiceTest {
lateinit var orderService: OrderService
@SetUp fun setup() {
orderService = OrderService()
}
@Test fun processesOrderSuccessfully() {
// 直接调用 orderService, 无需检查 null, 或初始化状态
orderService.processOrder()
}
}
```
你可以对以下声明为 `var` 的属性使用 `lateinit` 修饰符:
* 顶级属性.
* 局部变量.
* 类 body 部之内的属性.
对于类属性:
* 不能在主构造器中声明.
* 不能有自定义 getter 或 setter.
在所有情况下, 属性或变量的类型必须是非 null 的, 而且不能是 [基本类型](types-overview.html).
如果在初始化之前访问 `lateinit` 属性, Kotlin 会抛出一个特定的异常, 指明被访问的属性未初始化:
```KOTLIN
class ReportGenerator {
lateinit var report: String
fun printReport() {
// 在初始化之前访问, 会抛出异常
println(report)
}
}
fun main() {
val generator = ReportGenerator()
generator.printReport()
// 发生错误: Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property report has not been initialized
}
```
要检查 `lateinit var` 是否已完成初始化, 请对 [属性的引用](reflection.html#property-references) 使用
[isInitialized](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/is-initialized.html) 属性:
```KOTLIN
class WeatherStation {
lateinit var latestReading: String
fun printReading() {
// 检查属性是否已初始化
if (this::latestReading.isInitialized) {
println("Latest reading: $latestReading")
} else {
println("No reading available")
}
}
}
fun main() {
val station = WeatherStation()
station.printReading()
// 输出结果为: No reading available
station.latestReading = "22°C, sunny"
station.printReading()
// 输出结果为: Latest reading: 22°C, sunny
}
```
只有在代码中已经可以访问某个属性时, 才能对这个属性使用 `isInitialized`.
该属性必须声明在同一个类中, 在外部类中, 或者是同一文件中的顶级属性.
## 属性的覆盖
参见 [属性的覆盖](inheritance.html#overriding-properties).
## 委托属性(Delegated Property)
为了重用逻辑并减少代码重复, 你可以将获取和设置属性的任务委托给另一个单独的对象.
将访问器的行为委托出去, 能使属性的访问器逻辑集中化, 更易于重用.
这种方案在实现以下行为时很有用:
* 延迟计算属性值.
* 通过指定的键值从 map 中读取数据.
* 访问数据库.
* 在属性被访问时通知监听器.
你可以自己在库中实现这些常见行为, 也可以使用外部库提供的现有委托.
详情请参见 [委托属性](delegated-properties.html).
# 委托属性
有许多非常具有共性的属性, 虽然你可以在每个需要这些属性的类中手工地实现它们,
但是, 如果能够只实现一次, 然后将它放在库中, 供所有需要者重复使用, 那将会很有帮助.
例如:
* 延迟加载(lazy) 属性: 属性值只在初次访问时才会计算.
* 可观察(observable) 属性: 属性发生变化时, 监听器会收到通知.
* 将多个属性保存在一个 map 内, 而不是将每个属性保存在一个独立的域内.
为了解决这些问题(以及其它问题), Kotlin 允许 委托属性(delegated property):
```KOTLIN
class Example {
var p: String by Delegate()
}
```
委托属性的语法是: `val/var : by `.
其中 `by` 关键字之后的表达式就是 委托,
属性的 `get()` 方法(以及 `set()` 方法) 将被委托给这个对象的 `getValue()` 和 `setValue()` 方法.
属性委托不必实现接口, 但必须提供 `getValue()` 函数(对于 `var` 属性, 还需要 `setValue()` 函数).
示例:
```KOTLIN
import kotlin.reflect.KProperty
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name}' in $thisRef.")
}
}
```
如果属性 `p` 委托给一个 `Delegate` 的实例, 那么当你读取属性值时, 就会调用到 `Delegate` 的 `getValue()` 函数.
此时函数收到的第一个参数将是你访问的属性 `p` 所属的对象实例,
第二个参数将是 `p` 属性本身的描述信息(比如, 你可以从这里得到属性名称).
```KOTLIN
val e = Example()
println(e.p)
```
这段代码的打印结果将是:
```
Example@33a17727, thank you for delegating 'p' to me!
```
类似的, 当你向属性 `p` 赋值时, 将会调用到 `setValue()` 函数.
这个函数收到的前两个参数与 `getValue()` 函数相同, 第三个参数将是即将赋给属性的新值:
```KOTLIN
e.p = "NEW"
```
这段代码的打印结果将是:
```
NEW has been assigned to 'p' in Example@33a17727.
```
对属性委托对象的要求, 详细的说明请参见[下文](#property-delegate-requirements).
你可以在函数内, 或者一个代码段内定义委托属性, 委托属性不一定需要是类的成员.
参见 [示例](#local-delegated-properties).
## 标准委托
Kotlin 标准库中提供了一些工厂方法, 可以实现几种很有用的委托.
### 延迟加载(Lazy)属性
[lazy()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/lazy.html) 是一个函数, 接受一个 Lambda 表达式作为参数,
返回一个 `Lazy` 类型的实例, 这个实例可以作为一个委托, 实现延迟加载(lazy)属性.
第一次调用 `get()` 时, 将会执行 `lazy()` 函数受到的 Lambda 表达式, 然后会记住这次执行的结果.
以后所有对 `get()` 的调用都只会简单地返回以前记住的结果.
```KOTLIN
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main() {
println(lazyValue)
println(lazyValue)
}
```
默认情况下, 延迟加载(lazy)属性的计算是 同步的(synchronized):
属性值只会在唯一一个线程内计算, 但所有线程都将得到同样的属性值.
如果委托的初始化计算不需要同步, 多个线程可以同时执行初始化计算, 那么可以向`lazy()` 函数传入一个 `LazyThreadSafetyMode.PUBLICATION` 参数.
如果你确信初期化计算只可能发生在你访问属性的相同线程之内, 那么可以使用 `LazyThreadSafetyMode.NONE` 模式.
这种模式不会保持线程同步, 因此不会带来这方面的性能损失.
### 可观察(Observable)属性
[Delegates.observable()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.properties/-delegates/observable.html) 函数接受两个参数:
第一个是初始化值, 第二个是属性值变化事件的响应器(handler).
每次你向属性赋值时, 响应器(handler)都会被调用(在属性赋值处理完成 之后).
响应器收到三个参数: 被赋值的属性, 赋值前的旧属性值, 以及赋值后的新属性值:
```KOTLIN
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("") {
prop, old, new ->
println("$old -> $new")
}
}
fun main() {
val user = User()
user.name = "first"
user.name = "second"
}
```
如果你希望拦截属性的赋值操作, 并且还能够 否决 赋值操作, 那么不要使用 `observable()` 函数,
而应该改用 [vetoable()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.properties/-delegates/vetoable.html) 函数.
传递给 `vetoable` 函数的事件响应器, 会在属性赋值处理执行 之前 被调用.
## 委托给另一个属性
属性可以将它的 get 和 set 方法委托到另一个属性.
这种委托可以用于顶级属性和类属性 (包括成员属性和扩展属性).
委托属性可以是:
* 顶级属性
* 同一个类的成员属性, 或扩展属性
* 另一个类的成员属性, 或扩展属性
要将一个属性委托到另一个属性, 请在委托名称中使用 `::` 限定符,
比如, `this::delegate` 或 `MyClass::delegate`.
```KOTLIN
var topLevelInt: Int = 0
class ClassWithDelegate(val anotherClassInt: Int)
class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
var delegatedToMember: Int by this::memberInt
var delegatedToTopLevel: Int by ::topLevelInt
val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt
```
这种功能的用途是, 比如, 如果你希望修改属性名称, 同时又保持向后兼容:
这时可以引入一个新的属性, 将旧的属性标注 `@Deprecated` 注解, 然后将它的实现委托给新属性.
```KOTLIN
class MyClass {
var newName: Int = 0
@Deprecated("Use 'newName' instead", ReplaceWith("newName"))
var oldName: Int by this::newName
}
fun main() {
val myClass = MyClass()
// 注意: 'oldName: Int' 已废弃.
// 请改为使用 'newName'
myClass.oldName = 42
println(myClass.newName) // 42
}
```
## 将多个属性保存在一个 Map 内
有一种常见的使用场景是将多个属性的值保存在一个 map 之内.
在应用程序解析 JSON, 或者执行某些动态(dynamic)任务时, 经常会出现这样的需求.
这种情况下, 你可以使用 map 实例本身作为属性的委托.
```KOTLIN
class User(val map: Map) {
val name: String by map
val age: Int by map
}
```
上例中, 类的构造器接受一个 map 实例作为参数:
```KOTLIN
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
```
委托属性将从这个 map 中读取属性值, 使用属性名称字符串作为 key 值:
```KOTLIN
class User(val map: Map) {
val name: String by map
val age: Int by map
}
fun main() {
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
//sampleStart
println(user.name) // 打印结果为: "John Doe"
println(user.age) // 打印结果为: 25
//sampleEnd
}
```
如果不用只读的 `Map`, 而改用值可变的 `MutableMap`, 那么也可以用作 `var` 属性的委托:
```KOTLIN
class MutableUser(val map: MutableMap) {
var name: String by map
var age: Int by map
}
```
## 局部的委托属性(Local Delegated Property)
你可以将局部变量声明为委托属性.
比如, 你可以为局部变量添加延迟加载的能力:
```KOTLIN
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
```
`memoizedFoo` 变量直到初次访问时才会被计算.
如果 `someCondition` 的判定结果为 false, 那么 `memoizedFoo` 变量完全不会被计算.
## 属性委托的前提条件
对于一个 只读 属性 (`val` 属性), 它的委托应该提供 `getValue` 操作符函数, 参数如下:
* `thisRef` 参数, 类型必须与 属性所属的类 相同, 或者是它的基类 (对于扩展属性, 参数类型必须与被扩展的类型相同, 或者是它的基类).
* `property` 参数, 类型必须是 `KProperty<*>`, 或者是它的基类.
`getValue()` 函数的返回值类型必须与属性类型相同(或者是它的子类型).
```KOTLIN
class Resource
class Owner {
val valResource: Resource by ResourceDelegate()
}
class ResourceDelegate {
operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
return Resource()
}
}
```
对于一个 值可变(mutable) 属性(`var` 属性), 除 `getValue` 函数之外, 它的委托还必须另外再提供一个 `setValue` 操作符函数,
参数如下:
* `thisRef` 参数, 类型必须与 属性所属的类 相同, 或者是它的基类 (对于扩展属性, 参数类型必须与被扩展的类型相同, 或者是它的基类).
* `property` 参数, 类型必须是 `KProperty<*>`, 或者是它的基类.
* `value` 参数, 类型必须与属性类型相同(或者是它的基类).
```KOTLIN
class Resource
class Owner {
var varResource: Resource by ResourceDelegate()
}
class ResourceDelegate(private var resource: Resource = Resource()) {
operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
return resource
}
operator fun setValue(thisRef: Owner, property: KProperty<*>, value: Any?) {
if (value is Resource) {
resource = value
}
}
}
```
`getValue()` 和 `setValue()` 函数可以是委托类的成员函数, 也可以是它的扩展函数.
如果你需要将属性委托给一个对象, 而这个对象本来没有提供这些函数, 这时使用扩展函数会更便利一些.
这两个函数都需要标记为 `operator`.
通过使用 Kotlin 标准库中的 `ReadOnlyProperty` 和 `ReadWriteProperty` 接口,
可以用匿名对象的方式创建委托, 而不必创建新类.
这些接口提供了需要的方法: `getValue()` 声明在 `ReadOnlyProperty` 接口中;
`ReadWriteProperty` 继承了这个接口, 然后增加了 `setValue()` 方法.
因此在需要 `ReadOnlyProperty` 的地方, 你也可以使用 `ReadWriteProperty`.
```KOTLIN
fun resourceDelegate(resource: Resource = Resource()): ReadWriteProperty =
object : ReadWriteProperty {
var curValue = resource
override fun getValue(thisRef: Any?, property: KProperty<*>): Resource = curValue
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Resource) {
curValue = value
}
}
val readOnlyResource: Resource by resourceDelegate() // 此处 ReadWriteProperty 被转换为 val
var readWriteResource: Resource by resourceDelegate()
```
## 编译器对委托属性的翻译规则
委托属性的底层实现是, 对某些类型的委托属性, Kotlin 编译器会生成辅助属性, 并将目标属性的存取操作委托给这些辅助属性.
Note:
为了优化的目的, 编译器 [对有些情况 不会 生成辅助属性](#optimized-cases-for-delegated-properties).
关于优化, 详情请参见 [委托到另一个属性](#translation-rules-when-delegating-to-another-property) 中的示例.
比如, 对于属性 `prop`, 编译器会生成一个隐藏的 `prop$delegate` 属性, 然后属性 `prop` 的访问器代码会将存取操作委托给这个新增的属性:
```KOTLIN
class C {
var prop: Type by MyDelegate()
}
// 编译器实际生成的代码如下:
class C {
private val prop$delegate = MyDelegate()
var prop: Type
get() = prop$delegate.getValue(this, this::prop)
set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}
```
Kotlin 编译器通过参数来提供关于 `prop` 属性的所有必须信息: 第一个参数 `this` 指向外层类 `C` 的实例,
第二个参数 `this::prop` 是一个反射对象, 类型为 `KProperty`, 它将描述 `prop` 属性本身.
### 对委托属性优化的场景
如果委托属性是以下几种情况, 域成员 `$delegate` 会被省略:
* 属性的引用: ```KOTLIN class C { private var impl: Type = ... var prop: Type by ::impl } ```
* 命名对象 ```KOTLIN object NamedObject { operator fun getValue(thisRef: Any?, property: KProperty<*>): String = ... } val s: String by NamedObject ```
* 同一模块内, 带有后端域和默认的 getter 的 final `val` 属性: ```KOTLIN val impl: ReadOnlyProperty = ... class A { val s: String by impl } ```
* 常数表达式, 枚举值(Enum Entry), `this`, `null`. 以下是 `this` 的例子: ```KOTLIN class A { operator fun getValue(thisRef: Any?, property: KProperty<*>) ... val s by this } ```
### 委托到另一个属性时的翻译规则
委托到另一个属性时, Kotlin 编译器生成的代码会直接访问被参照的属性.
也就是说, 编译器不会生成域变量 `prop$delegate`. 这样的代码优化可以节约内存.
示例:
```KOTLIN
class C {
private var impl: Type = ...
var prop: Type by ::impl
}
```
`prop` 变量的属性访问器直接调用 `impl` 变量, 跳过被代理属性的 `getValue` 和 `setValue` 操作,
因此也不需要 `KProperty` 引用对象.
对于上面的代码, 编译器生成以下代码:
```KOTLIN
class C {
private var impl: Type = ...
var prop: Type
get() = impl
set(value) {
impl = value
}
fun getProp$delegate(): Type = impl // 需要这个方法, 只是为了反射功能
}
```
## 控制属性委托的创建逻辑
通过定义一个 `provideDelegate` 操作符, 你可以控制属性委托对象的创建逻辑.
如果在 `by` 右侧的对象中定义了名为 `provideDelegate` 的成员函数或扩展函数,
那么这个函数将被调用, 用来创建属性委托对象的实例.
`provideDelegate` 的一种可能的使用场景, 是在属性初始化时检查属性的一致性.
比如, 如果要在(属性与其委托对象)绑定之前检查属性名称, 你可以编写这样的代码:
```KOTLIN
class ResourceDelegate : ReadOnlyProperty {
override fun getValue(thisRef: MyUI, property: KProperty<*>): T { ... }
}
class ResourceLoader(id: ResourceID) {
operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty {
checkProperty(thisRef, prop.name)
// 创建委托
return ResourceDelegate()
}
private fun checkProperty(thisRef: MyUI, name: String) { ... }
}
class MyUI {
fun bindResource(id: ResourceID): ResourceLoader { ... }
val image by bindResource(ResourceID.image_id)
val text by bindResource(ResourceID.text_id)
}
```
`provideDelegate` 函数的参数与 `getValue` 相同:
* `thisRef` 参数, 类型必须与 属性所属的类 相同, 或者是它的基类 (对于扩展属性, 参数类型必须与被扩展的类型相同, 或者是它的基类);
* `property` 参数, 类型必须是 `KProperty<*>`, 或者是它的基类.
在 `MyUI` 的实例创建过程中, 将会对各个属性调用 `provideDelegate` 函数, 然后这个函数立即执行必要的验证.
如果不能对属性与其委托对象的绑定过程进行拦截, 要实现同样的功能, 你就必须在参数中明确地传递属性名称, 这就不太方便了:
```KOTLIN
// 如果没有 "provideDelegate" 功能, 我们需要这样来检查属性名称
class MyUI {
val image by bindResource(ResourceID.image_id, "image")
val text by bindResource(ResourceID.text_id, "text")
}
fun MyUI.bindResource(
id: ResourceID,
propertyName: String
): ReadOnlyProperty {
checkProperty(this, propertyName)
// 创建委托
}
```
在编译器生成的代码中, 会调用 `provideDelegate` 方法, 用来初始化辅助属性 `prop$delegate`.
请看属性声明 `val prop: Type by MyDelegate()` 对应的生成代码,
并和[上例](#translation-rules-for-delegated-properties)(没有 `provideDelegate` 方法的情况) 的代码对比以下:
```KOTLIN
class C {
var prop: Type by MyDelegate()
}
// 当 'provideDelegate' 函数存在时
// 编译器生成以下代码:
class C {
// 调用 "provideDelegate" 来创建 "delegate" 辅助属性
private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
var prop: Type
get() = prop$delegate.getValue(this, this::prop)
set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}
```
注意, `provideDelegate` 函数只影响辅助属性的创建, 而不会影响编译产生的属性取值方法和设值方法代码.
使用标准库中的 `PropertyDelegateProvider` 接口, 可以创建委托提供者(provider), 而不必创建新的类.
```KOTLIN
val provider = PropertyDelegateProvider { thisRef: Any?, property ->
ReadOnlyProperty {_, property -> 42 }
}
val delegate: Int by provider
```
# Null 值安全性
Null 值安全性是 Kotlin 的一个功能特性, 它的设计目的是为了极大的减少 null 引用带来的危险,
也就是所谓的 [造成十亿美元损失的大错误](https://en.wikipedia.org/wiki/Tony_Hoare#Apologies_and_retractions).
在许多编程语言(包括 Java)中, 最常见的陷阱之一就是, 对一个指向 null 值的对象访问它的成员, 导致一个 null 引用异常.
在 Java 中, 就是 `NullPointerException`, 简称 NPE.
Kotlin 明确的支持可空性, 这是它类型系统的一部分, 也就是说, 你可以明确的声明哪些变量或属性可以为 `null`.
而且, 当你声明非 null 变量时, 编译器会强制这些变量不能保存 `null` 值, 防止出现 NPE.
Kotlin 的 Null 值安全性通过在编译期发现与 null 相关的潜在问题, 而不是在运行期, 保证代码更加安全.
这个功能通过明确表达 `null` 值, 让代码更加易于理解和维护, 能够改善代码的健壮性, 可读性, 以及可维护性.
在 Kotlin 中只有以下情况可能导致 NPE:
* 明确调用 [throw NullPointerException()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-null-pointer-exception/).
* 使用 [非 null 断言操作符 !!](#not-null-assertion-operator).
* 初始化过程中存在数据不一致, 比如: * 在构造器中可以访问的未初始化的 `this`, 被其它代码访问(也就是 ["this 泄露"](https://youtrack.jetbrains.com/issue/KTIJ-9751)). * [基类的构造器调用了 open 的成员函数](inheritance.html#derived-class-initialization-order), 但这个成员函数在子类中的实现使用了未初始化的状态数据.
* Java 互操作: * 试图对一个 [平台类型](java-interop.html#null-safety-and-platform-types)的 `null` 引用访问其成员函数. * 泛型类型的可空性存在问题. 比如, 一段 Java 代码向一个 Kotlin `MutableList` 中添加一个 `null` 值, 对这种情况应该使用 `MutableList` 才能正确处理. * 外部 Java 代码导致的其他问题.
Tip:
除了 NPE 之外, 另一个与 null 安全性有关的异常是 [UninitializedPropertyAccessException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-uninitialized-property-access-exception/).
当你试图访问一个还没有初始化的属性时, Kotlin 会抛出这个异常, 以确保非 null 属性在初始化之后才能访问.
这种情况通常发生在 [lateinit 属性](properties.html#late-initialized-properties-and-variables) 中.
## 可为 null 的类型与不可为 null 的类型
在 Kotlin 中, 类型系统明确区分可以为 `null` 的类型 (可为 null 类型) 与不可以为 null 的类型 (非 null 类型).
比如, 一个通常的 `String` 类型变量不可以指向 `null`:
```KOTLIN
fun main() {
//sampleStart
// 将一个非 null 字符串赋值给一个变量
var a: String = "abc"
// 试图将非 null 变量再次赋值为 null
a = null
print(a)
// 编译错误: Null can not be a value of a non-null type String
//sampleEnd
}
```
你可以安全的对 `a` 调用方法, 或访问属性. 可以保证不会出现 NPE, 因为 `a` 是一个非 null 变量.
编译器确保 `a` 永远保存一个有效的 `String` 值, 因此不存在当它为 `null` 值时访问属性或方法的危险:
```KOTLIN
fun main() {
//sampleStart
// 将一个非 null 字符串赋值给一个变量
val a: String = "abc"
// 返回非 null 变量的 length
val l = a.length
print(l)
// 输出结果为: 3
//sampleEnd
}
```
要允许 `null` 值, 声明变量时请在变量类型之后添加一个 `?` 符号.
例如, 通过 `String?` 可以声明一个可为 null 的字符串.
这个表达式表示可以接受 `null` 值的 `String` 类型:
```KOTLIN
fun main() {
//sampleStart
// 将可为 null 的字符串赋值给一个变量
var b: String? = "abc"
// 将可为 null 的变量再次赋值为 null, 成功
b = null
print(b)
// 输出结果为: null
//sampleEnd
}
```
如果你试图直接对 `b` 访问 `length`, 编译器会报告错误. 这是因为 `b` 被声明为可为 null 的变量, 可以保存 `null` 值.
试图对可为 null 的值直接访问属性会导致 NPE:
```KOTLIN
fun main() {
//sampleStart
// 将可为 null 的字符串赋值给一个变量
var b: String? = "abc"
// 将可为 null 的变量再次赋值为 null
b = null
// 试图直接返回可为 null 的变量的 length
val l = b.length
print(l)
// 编译错误: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
//sampleEnd
}
```
在上面的示例中, 编译器要求你在访问属性或执行操作之前使用安全调用来检查是否为 null.
处理可为 null 的值有几种方法:
* [使用 if 条件进行 null 检查](#check-for-null-with-the-if-conditional)
* [安全调用操作符 ?.](#safe-call-operator)
* [Elvis 操作符 ?:](#elvis-operator)
* [非 null 断言操作符 !!](#not-null-assertion-operator)
* [可为 null 的接受者](#nullable-receiver)
* [let 函数](#let-function)
* [安全类型转换 as?](#safe-casts)
* [可为 null 的类型构成的集合](#collections-of-a-nullable-type)
关于 `null` 处理的各种工具和技术的详情, 以及示例, 请阅读下面的章节.
## 使用 if 条件进行 null 检查
在使用可为 null 的类型时, 你需要安全的处理 null 值, 以避免 NPE.
一种方法是使用 `if` 条件表达式明确的检查是否为 null.
例如, 先检查 `b` 是否为 `null`, 然后再访问 `b.length`:
```KOTLIN
fun main() {
//sampleStart
// 将 null 赋值给一个可为 null 的变量
val b: String? = null
// 先检查是否为 null, 然后再访问 length
val l = if (b != null) b.length else -1
print(l)
// 输出结果为: -1
//sampleEnd
}
```
在上面的示例中, 编译器执行一个 [智能转换](typecasts.html#smart-casts), 将类型从可为 null 的 `String?` 变为不可为 null 的 `String`.
它还会追踪你执行过哪些检查, 因此允许在 `if` 条件内访问 `length`.
更复杂的条件也是支持的:
```KOTLIN
fun main() {
//sampleStart
// 将可为 null 的字符串赋值给一个变量
val b: String? = "Kotlin"
// 先检查是否为 null, 然后再访问 length
if (b != null && b.length > 0) {
print("String of length ${b.length}")
// 输出结果为: String of length 6
} else {
// 如果条件不满足, 提供一个替代结果
print("Empty string")
}
//sampleEnd
}
```
注意, 只有在编译器能够确保 `b` 在检查与使用之间不会变化的情况下, 上面的示例才能正常工作,
[智能类型转换的前提条件](typecasts.html#smart-cast-prerequisites) 一样.
## 安全调用操作符
使用安全调用操作符 `?.`, 你可以用更短的方式安全的处理 null 值.
如果对象为 `null`, `?.` 直接返回 `null`, 而不会抛出 NPE:
```KOTLIN
fun main() {
//sampleStart
// 将可为 null 的字符串赋值给一个变量
val a: String? = "Kotlin"
// 将 null 赋值给一个可为 null 的变量
val b: String? = null
// 检查是否为 null, 返回 length, 或返回 null
println(a?.length)
// 输出结果为: 6
println(b?.length)
// 输出结果为: null
//sampleEnd
}
```
`b?.length` 表达式会检查是否为 null, 如果 `b` 不是 null, 返回 `b.length`, , 否则返回 `null`.
这个表达式本身的类型为 `Int?`.
在 Kotlin 中, 对 [var 和 val 变量](basic-syntax.html#variables) 都可以使用 `?.` 操作符:
* 一个可为 null 的 `var` 可以保存 `null` 值 (例如, `var nullableValue: String? = null`) 或非 null 值 (例如, `var nullableValue: String? = "Kotlin"`). 如果它是非 null 值, 你随时都可以将它变为 `null`.
* 一个可为 null 的 `val` 可以保存 `null` 值 (例如, `val nullableValue: String? = null`) 或非 null 值 (例如, `val nullableValue: String? = "Kotlin"`). 如果它是非 null 值, 之后你就不能将它变为 `null`.
安全调用在链式调用的情况下非常有用.
比如, 雇员 Bob 可能被派属某个部门 Department (也可能不属于任何部门),
这个部门可能存在另一个雇员, 担任部门主管.
为了取得 Bob 所属部门的主管的名字, (如果存在的话), 你可以编写下面的代码:
```KOTLIN
bob?.department?.head?.name
```
只要链式调用中的任何一个属性是 `null`, 这个链式调用就会返回 `null` .
你也可以在赋值运算的左侧使用安全调用:
```KOTLIN
person?.department?.head = managersPool.getManager()
```
在上面的示例中, 如果链式安全调用中的任何一个接受者为 `null`, 赋值运算就会被跳过, 完全不会对赋值运算右侧的表达式进行计算.
例如, 如果 `person` 或 `person.department` 为 `null`, 函数就不会调用.
下面是这个安全调用使用 `if` 条件的等价写法:
```KOTLIN
if (person != null && person.department != null) {
person.department.head = managersPool.getManager()
}
```
## Elvis 操作符
在使用可为 null 的类型时, 你可以检查是否为 `null`, 并为 `null` 提供一个替代的值.
例如, 如果 `b` 不是 `null`, 访问 `b.length`. 否则, 返回一个替代的值:
```KOTLIN
fun main() {
//sampleStart
// 将 null 赋值给一个可为 null 的变量
val b: String? = null
// 检查是否为 null. 如果不是 null, 返回 length. 如果是 null, 返回 0
val l: Int = if (b != null) b.length else 0
println(l)
// 输出结果为: 0
//sampleEnd
}
```
除了上例这种完整的 `if` 表达式之外, 你还可以使用 Elvis 操作符 `?:`, 以更加简洁的方式来处理:
```KOTLIN
fun main() {
//sampleStart
// 将 null 赋值给一个可为 null 的变量
val b: String? = null
// 检查是否为 null. 如果不是 null, 返回 length. 如果是 null, 返回一个非 null 值
val l = b?.length ?: 0
println(l)
// 输出结果为: 0
//sampleEnd
}
```
如果 `?:` 左侧的表达式值不是 `null`, Elvis 操作符就会返回它的值. 否则, Elvis 操作符返回右侧表达式的值.
只有在左侧表达式值为 `null` 时, 才会计算右侧表达式.
由于在 Kotlin 中 `throw` 和 `return` 都是表达式, 因此, 你也可以在 Elvis 操作符的右侧使用它们.
这种用法很方便, 比如, 可以用来检查函数参数值是否合法:
```KOTLIN
fun foo(node: Node): String? {
// 检查 getParent(). 如果不是 null, 它会被赋值给 parent. 如果是 null, 返回 null
val parent = node.getParent() ?: return null
// 检查 getName(). 如果不是 null, 它会被赋值给 name. 如果是 null, 抛出异常
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ...
}
```
## 非 null 断言操作符
非 null 判定操作符 `!!` 可以将任何值转换为非 null 类型.
如果你对一个值不是 `null` 的变量使用 `!!` 操作符, 它会被安全的做为非 null 类型来处理, 代码会正常执行.
但是, 如果值是 `null`, `!!` 操作符强制将它当作非 null 类型处理, 结果会导致 NPE.
当 `b` 不是 `null`, `!!` 操作符要求它返回非 null 值 (这个示例中是 `String`), 就能正确的访问 `length`:
```KOTLIN
fun main() {
//sampleStart
// 将可为 null 的字符串赋值给一个变量
val b: String? = "Kotlin"
// 将 b 当作非 null 值, 并访问它的 length
val l = b!!.length
println(l)
// 输出结果为: 6
//sampleEnd
}
```
当 `b` 是 `null`, `!!` 操作符要求它返回非 null 值, 会发生 NPE:
```KOTLIN
fun main() {
//sampleStart
// 将 null 赋值给一个可为 null 的变量
val b: String? = null
// 将 b 当作非 null 值, 并尝试访问它的 length
val l = b!!.length
println(l)
// 错误: Exception in thread "main" java.lang.NullPointerException
//sampleEnd
}
```
当你确信一个值不是 `null`, 并且不可能发生 NPE, 但编译器由于某些规则无法确定这一点时, `!!` 操作符会非常有用.
在这种情况下, 你可以使用 `!!` 操作符来明确的告诉编译器, 值不是 `null`.
## 可为 null 的接受者
你可以使用带有 [可为 null 的接受者类型](extensions.html#nullable-receivers) 的扩展函数,
这样就允许对可能为 `null` 的变量调用这些函数.
通过对可为 null 的接受者类型定义扩展函数, 你可以在函数内部处理 `null` 值, 而不必在每次调用函数的时候检查 `null` 值.
例如, [.toString()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/to-string.html) 扩展函数, 可以对可为 null 的接受者调用.
在对 `null` 值调用时, 它会安全的返回字符串 `"null"`, 不会抛出异常:
```KOTLIN
//sampleStart
fun main() {
// 将 null 赋值给保存在 person 变量中的可为 null 的 Person 对象
val person: Person? = null
// 对可为 null 的 person 变量调用 .toString, 并打印输出结果字符串
println(person.toString())
// 输出结果为: null
}
// 定义一个简单的 Person 类
data class Person(val name: String)
//sampleEnd
```
在上面的示例中, 即使 `person` 是 `null`, `.toString()` 函数仍然安全的返回字符串 `"null"`.
对于调试和日志输出, 这会很有用.
如果你希望 `.toString()` 函数返回可为 null 的字符串 (要么是对象的字符串表达, 要么是 `null` 值),
请使用 [安全调用操作符 ?.](#safe-call-operator).
`?.` 操作符只有在对象不为 `null` 时才会调用 `.toString()`, 否则它返回 `null`:
```KOTLIN
//sampleStart
fun main() {
// 将可为 null 的 Person 对象赋值给一个变量
val person1: Person? = null
val person2: Person? = Person("Alice")
// 如果 person 为 null, 打印输出 "null"; 否则打印输出 person.toString() 的结果
println(person1?.toString())
// 输出结果为: null
println(person2?.toString())
// 输出结果为: Person(name=Alice)
}
// 定义一个 Person 类
data class Person(val name: String)
//sampleEnd
```
通过 `?.` 操作符, 你可以安全的处理潜在的 `null` 值, 同时仍然能够访问可能为 `null` 的对象的属性或函数.
## let 函数
要处理 `null` 值, 并且只对非 null 的情况执行操作,
你可以将安全调用操作符 `?.` 和 [let 函数](scope-functions.html#let) 一起使用.
如果要计算一个表达式, 检查结果是否为 `null`, 然后只对非 `null` 的情况执行代码, 这样的组合会很有用, 能够避免手动的检查 null 值:
```KOTLIN
fun main() {
//sampleStart
// 声明一个可为 null 的字符串的 List
val listWithNulls: List = listOf("Kotlin", null)
// 遍历 List 中的每个元素
for (item in listWithNulls) {
// 检查元素是否为 null, 只打印非 null 的值
item?.let { println(it) }
// 输出结果为: Kotlin
}
//sampleEnd
}
```
## 安全类型转换
用于 [类型转换](typecasts.html#unsafe-cast-operator) 的通常的 Kotlin 操作符是 `as` 操作符.
但是, 如果对象不是我们期望的目标类型, 那么通常的类型转换就会导致异常.
你可以使用 `as?` 操作符进行安全类型转换.
它会尝试将一个值转换为指定的类型, 如果值不是这个类型, 则返回 `null`:
```KOTLIN
fun main() {
//sampleStart
// 声明一个 Any 类型的变量, 可以保存任何类型的值
val a: Any = "Hello, Kotlin!"
// 使用 'as?' 操作符, 安全转换为 Int
val aInt: Int? = a as? Int
// 使用 'as?' 操作符, 安全转换为 String
val aString: String? = a as? String
println(aInt)
// 输出结果为: null
println(aString)
// 输出结果为: "Hello, Kotlin!"
//sampleEnd
}
```
上面的代码打印输出 `null`, 因为 `a` 不是 `Int`, 因此转换会安全的失败.
代码还打印输出 `"Hello, Kotlin!"`, 因为它是 `String?` 类型, 因此安全转换成功.
## 可为 null 的类型构成的集合
如果你的有一个可为 null 的元素构成的集合, 并且只想保留其中非 null 值的元素,
可以使用 `filterNotNull()` 函数:
```KOTLIN
fun main() {
//sampleStart
// 声明一个 List, 包含一些 null 和非 null 的整数值
val nullableList: List = listOf(1, 2, null, 4)
// 过滤非 null 的值, 结果是一个非 null 整数构成的 list
val intList: List = nullableList.filterNotNull()
println(intList)
// 输出结果为: [1, 2, 4]
//sampleEnd
}
```
## 下一步做什么?
* 学习 [在 Java 和 Kotlin 中如何处理可空性(nullability)](java-to-kotlin-nullability-guide.html).
* 学习 [确定不含 null 值的泛型](generics.html#definitely-non-nullable-types).
# 相等判断
在 Kotlin 中, 存在两种相等判断:
* 结构相等 (`==`) - 使用 `equals()` 函数判断
* 引用相等 (`===`) - 判断两个引用指向同一个对象
## 结构相等
结构相等检查两个对象是否拥有相同的内容和结构.
结构相等使用 `==` 操作, 以及它的相反操作 `!=`, 来判断.
按照约定, `a == b` 这样的表达式将被转换为:
```KOTLIN
a?.equals(b) ?: (b === null)
```
如果 `a` 不为 `null`, 将会调用 `equals(Any?)` 函数.
否则(如果 `a` 为 `null`), 将会检查 `b` 是否指向 `null`:
```KOTLIN
fun main() {
var a = "hello"
var b = "hello"
var c = null
var d = null
var e = d
println(a == b)
// 输出结果为 true
println(a == c)
// 输出结果为 false
println(c == e)
// 输出结果为 true
}
```
注意, 当明确地与 `null` 进行比较时, 没有必要优化代码:
`a == null` 将会自动转换为 `a === null`.
在 Kotlin 中, 从 `Any` 开始的所有的类都会继承 `equals()` 函数.
默认情况下, `equals()` 函数实现 [引用相等判断](#referential-equality).
但是, Kotlin 中的类可以覆盖 `equals()` 函数, 实现一个自定义的相等判断逻辑, 并且通过这种方式, 实现结构相等判断.
值类(Value Class)和数据类(Data Class) 是两种特定的 Kotlin 类型, 它们会自动覆盖 `equals()` 函数.
因此它们默认会实现结构相等判断.
但是, 对于数据类的情况, 如果 `equals()` 函数在父类中被标记为 `final`, 那么它的行为会保持不变.
很明显, 非数据类 (没有使用 `data` 修饰符声明的类) 默认不会覆盖 `equals()` 函数.
相反, 非数据类实现引用相等判断, 继承自 `Any` 类.
实现结构相等判断, 非数据类需要用一个自定义的相等判断逻辑来覆盖 `equals()` 函数.
如果需要实现自定义的相等判断,
请覆盖 [equals(other: Any?): Boolean](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/equals.html) 函数:
```KOTLIN
class Point(val x: Int, val y: Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Point) return false
// 比较属性值, 实现结构相等判断
return this.x == other.x && this.y == other.y
}
}
```
Note:
在覆盖 equals() 函数时, 你还应该覆盖 [hashCode() 函数](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/hash-code.html),
以保持相等判断和 hash 值的一致性, 确保这些函数的行为正确.
同名但参数不同的其他函数 (比如 `equals(other: Foo)`) 不会影响到使用操作符 `==` 和 `!=` 进行的相等判断.
结构相等与 `Comparable<...>` 接口定义的比较操作没有关系,
因此, 只有 `equals(Any?)` 函数的自定义实现才会影响相等操作符的结果.
## 引用相等
引用相等检查两个对象的内存地址, 判断它们是不是相同的实例.
引用相等使用 `===` 操作, 以及它的相反操作 `!==`, 来判断.
当, 且仅当, `a` 与`b` 指向同一个对象时, `a === b` 结果为 `true`:
```KOTLIN
fun main() {
var a = "Hello"
var b = a
var c = "world"
var d = "world"
println(a === b)
// 输出结果为 true
println(a === c)
// 输出结果为 false
println(c === d)
// 输出结果为 true
}
```
对于运行时期表达为基本类型的那些值(比如, `Int`), `===` 判断等价于 `==` 判断.
Tip:
在 Kotlin/JS 中, 引用相等的实现方式是不同的. 关于相等判断, 更多详情请参见 [Kotlin/JS](js-interop.html#equality) 文档.
## 浮点数值的相等比较
如果相等比较的操作数类型可以静态地判定为 `Float` 或 `Double` (无论可否为 null),
那么相等判断将使用 [IEEE 754 浮点数运算标准](https://en.wikipedia.org/wiki/IEEE_754).
对于不是浮点值静态类型的操作数, 行为会不同. 对这样的情况, 将会使用结构相等判定.
因此, 对于不是浮点值静态类型的操作数, 判定不遵循 IEEE 标准.
在这种情况下:
* `NaN` 等于它自己
* `NaN` 认为大于任何其他元素 (包括 `POSITIVE_INFINITY`)
* `-0.0` 不等于 `0.0`
详情请参见: [浮点值的比较](numbers.html#floating-point-number-comparison).
## 数组的相等比较
要比较两个数组是否包含相同顺序的相同元素, 请使用 [contentEquals()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/content-equals.html).
详情请参见, [数组的比较](arrays.html#compare-arrays).
# 泛型(Generic): in, out, where
Kotlin 中的类也可以有类型参数, 与 Java 一样:
```KOTLIN
class Box(t: T) {
var value = t
}
```
要创建这样一个类的实例, 只需要指定类型参数:
```KOTLIN
val box: Box = Box(1)
```
但是, 如果类型参数可以通过推断得到, 比如, 通过构造器参数类型推断得到,
你可以省略类型参数:
```KOTLIN
val box = Box(1) // 1 的类型为 Int, 因此编译器知道类型为 Box
```
## 类型变异(Variance)
Java 的类型系统中, 最微妙最难于理解和使用的部分之一, 就是它的通配符类型(wildcard type) (参见 [Java 泛型 FAQ](http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html)).
Kotlin 中不存在这样的通配符类型. 而是使用声明处类型变异(declaration-site variance), 以及类型投射(type projection).
### Java 中的类型变异(Variance)和通配符(Wildcard)
让我们思考一下为什么 Java 需要这些神秘的通配符类型.
首先, Java 中的泛型类型是 不可变的(invariant), 也就是说 `List` 不是 `List