# 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](images/slack.svg) 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) * ![Icon 1](images/icon-1.svg) [Hello world](kotlin-tour-hello-world.html) ![Icon 2](images/icon-2.svg) [基本类型](kotlin-tour-basic-types.html) ![Icon 3](images/icon-3.svg) [集合(Collection)](kotlin-tour-collections.html) ![Icon 4](images/icon-4.svg) [控制流](kotlin-tour-control-flow.html) ![Icon 5](images/icon-5.svg) [函数](kotlin-tour-functions.html) ![Icon 6](images/icon-6.svg) [类](kotlin-tour-classes.html) ![Icon 7](images/icon-7.svg) [Null 值安全性](kotlin-tour-null-safety.html) * ![Icon 1](images/icon-1.svg) [扩展函数](kotlin-tour-intermediate-extension-functions.html) ![Icon 2](images/icon-2.svg) [作用域函数(Scope Function)](kotlin-tour-intermediate-scope-functions.html) ![Icon 3](images/icon-3.svg) [带接受者的 Lambda 表达式](kotlin-tour-intermediate-lambdas-receiver.html) ![Icon 4](images/icon-4.svg) [类与接口](kotlin-tour-intermediate-classes-interfaces.html) ![Icon 5](images/icon-5.svg) [对象](kotlin-tour-intermediate-objects.html) ![Icon 6](images/icon-6.svg) [开放类与特殊类](kotlin-tour-intermediate-open-special-classes.html) ![Icon 7](images/icon-7.svg) [属性](kotlin-tour-intermediate-properties.html) ![Icon 8](images/icon-8.svg) [Null 值安全性](kotlin-tour-intermediate-null-safety.html) ![Icon 9](images/icon-9.svg) [库与 API](kotlin-tour-intermediate-libraries-and-apis.html) # Hello world ![第 1 步](images/icon-1.svg) Hello world ![第 2 步](images/icon-2-todo.svg) [基本类型](kotlin-tour-basic-types.html) ![第 3 步](images/icon-3-todo.svg) [集合(Collection)](kotlin-tour-collections.html) ![第 4 步](images/icon-4-todo.svg) [控制流](kotlin-tour-control-flow.html) ![第 5 步](images/icon-5-todo.svg) [函数](kotlin-tour-functions.html) ![第 6 步](images/icon-6-todo.svg) [类](kotlin-tour-classes.html) ![第 7 步](images/icon-7-todo.svg) [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) # 基本类型 ![第 1 步](images/icon-1-done.svg) [Hello world](kotlin-tour-hello-world.html) ![第 2 步](images/icon-2.svg) 基本类型 ![第 3 步](images/icon-3-todo.svg) [集合(Collection)](kotlin-tour-collections.html) ![第 4 步](images/icon-4-todo.svg) [控制流](kotlin-tour-control-flow.html) ![第 5 步](images/icon-5-todo.svg) [函数](kotlin-tour-functions.html) ![第 6 步](images/icon-6-todo.svg) [类](kotlin-tour-classes.html) ![第 7 步](images/icon-7-todo.svg) [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) ![第 1 步](images/icon-1-done.svg) [Hello world](kotlin-tour-hello-world.html) ![第 2 步](images/icon-2-done.svg) [基本类型](kotlin-tour-basic-types.html) ![第 3 步](images/icon-3.svg) 集合(Collection) ![第 4 步](images/icon-4-todo.svg) [控制流](kotlin-tour-control-flow.html) ![第 5 步](images/icon-5-todo.svg) [函数](kotlin-tour-functions.html) ![第 6 步](images/icon-6-todo.svg) [类](kotlin-tour-classes.html) ![第 7 步](images/icon-7-todo.svg) [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) # 控制流 ![第 1 步](images/icon-1-done.svg) [Hello world](kotlin-tour-hello-world.html) ![第 2 步](images/icon-2-done.svg) [基本类型](kotlin-tour-basic-types.html) ![第 3 步](images/icon-3-done.svg) [集合](kotlin-tour-collections.html) ![第 4 步](images/icon-4.svg) 控制流 ![第 5 步](images/icon-5-todo.svg) [函数](kotlin-tour-functions.html) ![第 6 步](images/icon-6-todo.svg) [类](kotlin-tour-classes.html) ![第 7 步](images/icon-7-todo.svg) [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) # 函数 ![第 1 步](images/icon-1-done.svg) [Hello world](kotlin-tour-hello-world.html) ![第 2 步](images/icon-2-done.svg) [基本类型](kotlin-tour-basic-types.html) ![第 3 步](images/icon-3-done.svg) [集合(Collection)](kotlin-tour-collections.html) ![第 4 步](images/icon-4-done.svg) [控制流](kotlin-tour-control-flow.html) ![第 5 步](images/icon-5.svg) 函数 ![第 6 步](images/icon-6-todo.svg) [类](kotlin-tour-classes.html) ![第 7 步](images/icon-7-todo.svg) [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) # 类 ![第 1 步](images/icon-1-done.svg) [Hello world](kotlin-tour-hello-world.html) ![第 2 步](images/icon-2-done.svg) [基本类型](kotlin-tour-basic-types.html) ![第 3 步](images/icon-3-done.svg) [集合(Collection)](kotlin-tour-collections.html) ![第 4 步](images/icon-4-done.svg) [控制流](kotlin-tour-control-flow.html) ![第 5 步](images/icon-5-done.svg) [函数](kotlin-tour-functions.html) ![第 6 步](images/icon-6.svg) 类 ![第 7 步](images/icon-7-todo.svg) [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 值安全性 ![第 1 步](images/icon-1-done.svg) [Hello world](kotlin-tour-hello-world.html) ![第 2 步](images/icon-2-done.svg) [基本类型](kotlin-tour-basic-types.html) ![第 3 步](images/icon-3-done.svg) [集合(Collection)](kotlin-tour-collections.html) ![第 4 步](images/icon-4-done.svg) [控制流](kotlin-tour-control-flow.html) ![第 5 步](images/icon-5-done.svg) [函数](kotlin-tour-functions.html) ![第 6 步](images/icon-6-done.svg) [类](kotlin-tour-classes.html) ![第 7 步](images/icon-7.svg) 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) # 中级教程: 扩展函数 ![First step](images/icon-1.svg) 扩展函数 ![Second step](images/icon-2-todo.svg) [作用域函数](kotlin-tour-intermediate-scope-functions.html) ![Third step](images/icon-3-todo.svg) [带接受者的 Lambda 表达式](kotlin-tour-intermediate-lambdas-receiver.html) ![Fourth step](images/icon-4-todo.svg) [类与接口](kotlin-tour-intermediate-classes-interfaces.html) ![Fifth step](images/icon-5-todo.svg) [对象](kotlin-tour-intermediate-objects.html) ![Sixth step](images/icon-6-todo.svg) [开放类与特殊类](kotlin-tour-intermediate-open-special-classes.html) ![Seventh step](images/icon-7-todo.svg) [属性](kotlin-tour-intermediate-properties.html) ![Eighth step](images/icon-8-todo.svg) [Null 值安全性](kotlin-tour-intermediate-null-safety.html) ![Ninth step](images/icon-9-todo.svg) [库与 API](kotlin-tour-intermediate-libraries-and-apis.html) 在这一章中, 你将探索一些特殊的 Kotlin 函数, 它们能够让你的代码更加简洁, 更加易读. 学习这些函数能够如何帮助你使用高效的设计模式, 将你的项目提升到更高水平. ## 扩展函数 在软件开发中, 你经常需要修改一个程序的行为, 但又不能修改原来的源代码. 例如, 你可能想要向一个来自第三方库的类添加额外的功能. 你可以通过添加 扩展函数 来扩展一个类. 调用扩展函数的方式与调用类的成员函数一样, 使用点号 `.`. 在介绍扩展函数的完整语法之前, 你需要理解什么是 接受者(Receiver). 接受者(Receiver)是指函数对哪个对象调用. 换句话说, 接受者就是共享信息的来源. ![发送者和接受者的示例](images/receiver-highlight.png) 在这个示例中, `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) ![First step](images/icon-1-done.svg) [扩展函数](kotlin-tour-intermediate-extension-functions.html) ![Second step](images/icon-2.svg) 作用域函数 ![Third step](images/icon-3-todo.svg) [带接受者的 Lambda 表达式](kotlin-tour-intermediate-lambdas-receiver.html) ![Fourth step](images/icon-4-todo.svg) [类与接口](kotlin-tour-intermediate-classes-interfaces.html) ![Fifth step](images/icon-5-todo.svg) [对象](kotlin-tour-intermediate-objects.html) ![Sixth step](images/icon-6-todo.svg) [开放类与特殊类](kotlin-tour-intermediate-open-special-classes.html) ![Seventh step](images/icon-7-todo.svg) [属性](kotlin-tour-intermediate-properties.html) ![Eighth step](images/icon-8-todo.svg) [Null 值安全性](kotlin-tour-intermediate-null-safety.html) ![Ninth step](images/icon-9-todo.svg) [库与 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 表达式 ![First step](images/icon-1-done.svg) [扩展函数](kotlin-tour-intermediate-extension-functions.html) ![Second step](images/icon-2-done.svg) [作用域函数](kotlin-tour-intermediate-scope-functions.html) ![Third step](images/icon-3.svg) 带接受者的 Lambda 表达式 ![Fourth step](images/icon-4-todo.svg) [类与接口](kotlin-tour-intermediate-classes-interfaces.html) ![Fifth step](images/icon-5-todo.svg) [对象](kotlin-tour-intermediate-objects.html) ![Sixth step](images/icon-6-todo.svg) [开放类与特殊类](kotlin-tour-intermediate-open-special-classes.html) ![Seventh step](images/icon-7-todo.svg) [属性](kotlin-tour-intermediate-properties.html) ![Eighth step](images/icon-8-todo.svg) [Null 值安全性](kotlin-tour-intermediate-null-safety.html) ![Ninth step](images/icon-9-todo.svg) [库与 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() fun item(name: String) { items.add(MenuItem(name)) } } ``` 我们使用一个带接受者的 Lambda 表达式, 将它作为函数参数 (`init`) 传递给 `menu()` 函数, 这个函数构建一个菜单, 作为开始点: ```KOTLIN fun menu(name: String, init: Menu.() -> Unit): Menu { // 创建 Menu 类的一个实例 val menu = Menu(name) // 在类实例上调用带接受者的 Lambda 表达式 init() menu.init() return menu } ``` 现在你可以使用这个 DSL 来配置一个菜单, 并创建一个 `printMenu()` 函数, 将菜单结构打印输出到控制台: ```KOTLIN class MenuItem(val name: String) class Menu(val name: String) { val items = mutableListOf() fun item(name: String) { items.add(MenuItem(name)) } } fun menu(name: String, init: Menu.() -> Unit): Menu { val menu = Menu(name) menu.init() return menu } //sampleStart fun printMenu(menu: Menu) { println("Menu: ${menu.name}") menu.items.forEach { println(" Item: ${it.name}") } } // 使用 DSL fun main() { // 创建菜单 val mainMenu = menu("Main Menu") { // 向菜单添加项目 item("Home") item("Settings") item("Exit") } // 打印菜单 printMenu(mainMenu) // 输出结果为: // Menu: Main Menu // Item: Home // Item: Settings // Item: Exit } //sampleEnd ``` 如你所见, 使用带接受者的 Lambda 表达式 大大的简化了创建你的菜单所需要的代码. Lambda 表达式不仅仅可以用于设置和创建, 也可以用于配置. 它们普遍用于构建 API, UI 框架, 以及配置构建器的 DSL, 以生成精简的代码, 让你能够更容易的专注于底层代码结构和逻辑. Kotlin 的生态环境中存在这种设计模式的很多例子, 例如标准库中的 [buildList()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/build-list.html) 和 [buildString()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/build-string.html) 函数. Tip: 在 Kotlin 中, 带接受者的 Lambda 表达式可以与 类型安全的构建器 结合, 创建能够在编译期而不是在运行期检测出类型问题的 DSL. 详情请参见 [类型安全的构建器](type-safe-builders.html). ## 实际练习 ### 习题 1 你有一个 `fetchData()` 函数, 参数是一个带接受者的 Lambda 表达式. 请更新 Lambda 表达式, 使用 `append()` 函数, 让你的代码的输出成为: `Data received - Processed`. ```KOTLIN fun fetchData(callback: StringBuilder.() -> Unit) { val builder = StringBuilder("Data received") builder.callback() } fun main() { fetchData { // 请在这里编写你的代码 // 输出结果为: Data received - Processed } } ``` ```KOTLIN fun fetchData(callback: StringBuilder.() -> Unit) { val builder = StringBuilder("Data received") builder.callback() } fun main() { fetchData { append(" - Processed") println(this.toString()) // 输出结果为: Data received - Processed } } ``` ### 习题 2 你有一个 `Button` 类, 以及 `ButtonEvent` 和 `Position` 数据. 请编写代码, 触发 `Button` 类的 `onEvent()` 成员函数, 触发一个 double-click 事件. 你的代码应该打印输出 `"Double click!"`. ```KOTLIN class Button { fun onEvent(action: ButtonEvent.() -> Unit) { // 模拟 double-click 事件 (不是 right-click) val event = ButtonEvent(isRightClick = false, amount = 2, position = Position(100, 200)) event.action() // 触发事件回调 } } data class ButtonEvent( val isRightClick: Boolean, val amount: Int, val position: Position ) data class Position( val x: Int, val y: Int ) fun main() { val button = Button() button.onEvent { // 请在这里编写你的代码 // 输出结果为: Double click! } } ``` ```KOTLIN class Button { fun onEvent(action: ButtonEvent.() -> Unit) { // 模拟 double-click 事件 (不是 right-click) val event = ButtonEvent(isRightClick = false, amount = 2, position = Position(100, 200)) event.action() // 触发事件回调 } } data class ButtonEvent( val isRightClick: Boolean, val amount: Int, val position: Position ) data class Position( val x: Int, val y: Int ) fun main() { val button = Button() button.onEvent { if (!isRightClick && amount == 2) { println("Double click!") // 输出结果为: Double click! } } } ``` ### 习题 3 编写一个函数, 创建一个整数 List 的副本, 其中每个元素增大 1. 请使用已经提供的函数框架, 它使用一个 `incremented` 函数扩展了 `List`. ```KOTLIN fun List.incremented(): List { val originalList = this return buildList { // 请在这里编写你的代码 } } fun main() { val originalList = listOf(1, 2, 3) val newList = originalList.incremented() println(newList) // 输出结果为: [2, 3, 4] } ``` ```KOTLIN fun List.incremented(): List { val originalList = this return buildList { for (n in originalList) add(n + 1) } } fun main() { val originalList = listOf(1, 2, 3) val newList = originalList.incremented() println(newList) // 输出结果为: [2, 3, 4] } ``` ## 下一步 [中级教程: 类与接口](kotlin-tour-intermediate-classes-interfaces.html) # 中级教程: 类与接口 ![First step](images/icon-1-done.svg) [扩展函数](kotlin-tour-intermediate-extension-functions.html) ![Second step](images/icon-2-done.svg) [作用域函数](kotlin-tour-intermediate-scope-functions.html) ![Third step](images/icon-3-done.svg) [带接受者的 Lambda 表达式](kotlin-tour-intermediate-lambdas-receiver.html) ![Fourth step](images/icon-4.svg) 类与接口 ![Fifth step](images/icon-5-todo.svg) [对象](kotlin-tour-intermediate-objects.html) ![Sixth step](images/icon-6-todo.svg) [开放类与特殊类](kotlin-tour-intermediate-open-special-classes.html) ![Seventh step](images/icon-7-todo.svg) [属性](kotlin-tour-intermediate-properties.html) ![Eighth step](images/icon-8-todo.svg) [Null 值安全性](kotlin-tour-intermediate-null-safety.html) ![Ninth step](images/icon-9-todo.svg) [库与 API](kotlin-tour-intermediate-libraries-and-apis.html) 在初学者教程中, 你已经学习了如何使用类和数据类来保存数据, 以及维护一组能够在代码中共用的特性. 最终, 你会想要创建一个层级结构, 在你的项目中高效的共用代码. 本章介绍 Kotlin 为共用代码提供了哪些选择, 以及这些方案如何让你的代码更加安全, 更加易于维护. ## 类继承 在前一章, 我们介绍了如何使用扩展函数来扩展类, 而不必修改原来的源代码. 但如果你在处理一些复杂的任务, 需要在类 之间 共用代码, 那么应该怎么办? 对于这样的情况, 你可以使用类继承. Kotlin 中的类默认不能继承. Kotlin 这样设计是为了防止意外的继承, 让你的类更易于维护. Kotlin 类只支持 单继承, 意思是说 一次只能从一个类 继承. 被继承的这个类称为 父类. 一个类的父类又继承另一个类 (祖类), 构成一个层级结构. Kotlin 的类层级结构的最顶端是共通的父类: `Any`. 所有的类都最终继承 `Any` 类: ![包含 Any 类型的类层级结构示例](images/any-type-class.png) `Any` 类自动提供了 `toString()` 函数, 作为它的成员函数. 因此, 你可以在任何类中使用这个继承得到的函数. 例如: ```KOTLIN class Car(val make: String, val model: String, val numberOfDoors: Int) fun main() { //sampleStart val car1 = Car("Toyota", "Corolla", 4) // 通过字符串模板使用 .toString() 函数, 打印输出类的属性 println("Car1: make=${car1.make}, model=${car1.model}, numberOfDoors=${car1.numberOfDoors}") // 输出结果为: Car1: make=Toyota, model=Corolla, numberOfDoors=4 //sampleEnd } ``` 如果你想要使用继承在类之间共用某些代码, 首先请考虑使用抽象类. ### 抽象类 抽象类默认可以继承. 抽象类的目的是提供成员, 供其它类继承或实现. 因此, 它们有构造器, 但不能创建抽象类的实例. 在子类中, 使用 `override` 关键字来定义父类的属性和函数的行为. 通过这种方式, 可以说子类 "覆盖(override)" 了父类的成员. Tip: 当你定义继承的函数或属性的行为时, 我们称之为一个 实现. 抽象类可以包含 带有 实现的函数和属性, 也可以包含 没有 实现的函数和属性, 称为抽象函数和属性. 要创建一个抽象类, 请使用 `abstract` 关键字: ```KOTLIN abstract class Animal ``` 要声明一个 没有 实现的函数或属性, 也使用 `abstract` 关键字: ```KOTLIN abstract fun makeSound() abstract val sound: String ``` 例如, 假设你想要创建一个抽象类 `Product`, 并从它创建子类来定义不同的产品类别: ```KOTLIN abstract class Product(val name: String, var price: Double) { // 抽象属性, 表示产品类别 abstract val category: String // 一个函数, 可以由所有产品共用 fun productInfo(): String { return "Product: $name, Category: $category, Price: $price" } } ``` 在这个抽象类中: * 构造器有 2 个参数, 表示产品的 `name` 和 `price`. * 有一个抽象属性, 包含产品类别, 类型是字符串. * 有一个函数, 打印输出关于产品的信息. 我们来为电子产品创建一个子类. 在子类中为 `category` 属性定义实现之前, 你必须使用 `override` 关键字: ```KOTLIN class Electronic(name: String, price: Double, val warranty: Int) : Product(name, price) { override val category = "Electronic" } ``` `Electronic` 类: * 继承 `Product` 抽象类. * 构造器有一个额外的参数: `warranty`, 这是电子产品独有的. * 覆盖了 `category` 属性, 包含字符串 `"Electronic"`. 现在, 你可以这样使用这些类: ```KOTLIN abstract class Product(val name: String, var price: Double) { // 抽象属性, 表示产品类别 abstract val category: String // 一个函数, 可以由所有产品共用 fun productInfo(): String { return "Product: $name, Category: $category, Price: $price" } } class Electronic(name: String, price: Double, val warranty: Int) : Product(name, price) { override val category = "Electronic" } //sampleStart fun main() { // 创建 Electronic 类的一个实例 val laptop = Electronic(name = "Laptop", price = 1000.0, warranty = 2) println(laptop.productInfo()) // 输出结果为: Product: Laptop, Category: Electronic, Price: 1000.0 } //sampleEnd ``` 尽管抽象类非常适合以这种方式共用代码, 但它们仍然存在限制, 因为 Kotlin 中的类只支持单继承. 如果你需要从多个来源继承, 请考虑使用接口. ## 接口 接口与类类似, 但有一些不同: * 你不能创建接口的实例. 接口没有构造器或头部. * 接口的函数和属性默认隐式的可以继承. 在 Kotlin 中, 我们称之为 "open". * 如果你不为接口的函数提供实现, 不需要将函数标记为 `abstract`. 与抽象类类似, 可以使用接口来定义一组函数和属性, 之后供类继承和实现. 这种方式帮助你专注于由接口描述的抽象功能, 而不是具体的实现细节. 使用接口能够让你的代码: * 更加模块化, 因为它隔离了不同的部分, 允许它们独自演化. * 更易于理解, 因为它将相关的函数组合到一个内聚的功能集合中. * 更易于测试, 因为你可以在测试中快速的使用 mock 替换真实的实现. 要声明一个接口, 请使用 `interface` 关键字: ```KOTLIN interface PaymentMethod ``` ### 接口实现 接口支持多继承, 因此类可以一次实现多个接口. 首先, 我们来看看类实现 单个 接口的场景. 要创建实现单个接口的类, 请在你的类头部之后添加冒号, 之后是想要实现的接口名称. 不要在接口名称之后使用括号 `()`, 因为接口没有构造器: ```KOTLIN class CreditCardPayment : PaymentMethod ``` 例如: ```KOTLIN interface PaymentMethod { // 函数默认可以继承 fun initiatePayment(amount: Double): String } class CreditCardPayment(val cardNumber: String, val cardHolderName: String, val expiryDate: String) : PaymentMethod { override fun initiatePayment(amount: Double): String { // 模拟使用信用卡处理支付 return "Payment of $$amount initiated using Credit Card ending in ${cardNumber.takeLast(4)}." } } fun main() { val paymentMethod = CreditCardPayment("1234 5678 9012 3456", "John Doe", "12/25") println(paymentMethod.initiatePayment(100.0)) // 输出结果为: Payment of $100.0 initiated using Credit Card ending in 3456. } ``` 在这个示例中: * `PaymentMethod` 是一个接口, 有一个 `initiatePayment()` 函数, 没有实现. * `CreditCardPayment` 是一个类, 实现 `PaymentMethod` 接口. * `CreditCardPayment` 类覆盖继承的 `initiatePayment()` 函数. * `paymentMethod` 是 `CreditCardPayment` 类的一个实例. * 在 `paymentMethod` 实例上, 调用覆盖的 `initiatePayment()` 函数, 使用参数 `100.0`. 要创建一个实现 多个 接口的类, 请在你的类头部之后添加冒号, 之后是想要实现的接口名称, 以逗号分隔: ```KOTLIN class CreditCardPayment : PaymentMethod, PaymentType ``` 例如: ```KOTLIN interface PaymentMethod { fun initiatePayment(amount: Double): String } interface PaymentType { val paymentType: String } class CreditCardPayment(val cardNumber: String, val cardHolderName: String, val expiryDate: String) : PaymentMethod, PaymentType { override fun initiatePayment(amount: Double): String { // 模拟使用信用卡处理支付 return "Payment of $$amount initiated using Credit Card ending in ${cardNumber.takeLast(4)}." } override val paymentType: String = "Credit Card" } fun main() { val paymentMethod = CreditCardPayment("1234 5678 9012 3456", "John Doe", "12/25") println(paymentMethod.initiatePayment(100.0)) // 输出结果为: Payment of $100.0 initiated using Credit Card ending in 3456. println("Payment is by ${paymentMethod.paymentType}") // 输出结果为: Payment is by Credit Card } ``` 在这个示例中: * `PaymentMethod` 是一个接口, 有一个 `initiatePayment()` 函数, 没有实现. * `PaymentType` 是一个接口, 有 `paymentType` 属性, 没有初始化. * `CreditCardPayment` 是一个类, 实现 `PaymentMethod` 和 `PaymentType` 接口. * `CreditCardPayment` 类覆盖继承的 `initiatePayment()` 函数和 `paymentType` 属性. * `paymentMethod` 是 `CreditCardPayment` 类的一个实例. * 在 `paymentMethod` 实例上, 调用覆盖的 `initiatePayment()` 函数, 使用参数 `100.0`. * 在 `paymentMethod` 实例上, 访问覆盖的 `paymentType` 属性. 关于接口和接口继承, 详情请参见 [接口](interfaces.html). ## 委托 接口是很有用的, 但如果你的接口包含很多函数, 它的子类可能会出现大量样板代码. 如果你只想覆盖一个类的一小部分行为时, 你就需要大量的重复代码. Tip: 样板代码是指一块代码在软件项目的多个部分中重复使用, 只有很少的修改, 或根本没有修改. 例如, 假设你有一个接口 `DrawingTool`, 包含很多函数和一个属性 `color`: ```KOTLIN interface DrawingTool { val color: String fun draw(shape: String) fun erase(area: String) fun getToolInfo(): String } ``` 你创建了一个类 `PenTool`, 实现 `DrawingTool` 接口, 为它的所有成员提供实现: ```KOTLIN class PenTool : DrawingTool { override val color: String = "black" override fun draw(shape: String) { println("Drawing $shape using a pen in $color") } override fun erase(area: String) { println("Erasing $area with pen tool") } override fun getToolInfo(): String { return "PenTool(color=$color)" } } ``` 你想要创建与 `PenTool` 类似的类, 保持相同的行为, 只是 `color` 值不同. 一种方案是创建一个新的类, 接受一个实现了 `DrawingTool` 接口的对象作为参数, 例如一个 `PenTool` 类实例. 然后, 在这个类之内, 你可以覆盖 `color` 属性. 但在这种情况下, 你需要为 `DrawingTool` 接口的每个成员添加实现: ```KOTLIN interface DrawingTool { val color: String fun draw(shape: String) fun erase(area: String) fun getToolInfo(): String } class PenTool : DrawingTool { override val color: String = "black" override fun draw(shape: String) { println("Drawing $shape using a pen in $color") } override fun erase(area: String) { println("Erasing $area with pen tool") } override fun getToolInfo(): String { return "PenTool(color=$color)" } } //sampleStart class CanvasSession(val tool: DrawingTool) : DrawingTool { override val color: String = "blue" override fun draw(shape: String) { tool.draw(shape) } override fun erase(area: String) { tool.erase(area) } override fun getToolInfo(): String { return tool.getToolInfo() } } //sampleEnd fun main() { val pen = PenTool() val session = CanvasSession(pen) println("Pen color: ${pen.color}") // 输出结果为: Pen color: black println("Session color: ${session.color}") // 输出结果为: Session color: blue session.draw("circle") // 输出结果为: Drawing circle with pen in black session.erase("top-left corner") // 输出结果为: Erasing top-left corner with pen tool println(session.getToolInfo()) // 输出结果为: PenTool(color=black) } ``` 你会看出, 如果在 `DrawingTool` 接口中有大量的成员函数, `CanvasSession` 类中的样板代码数量会变得很大. 但是, 还有另一种选择. 在 Kotlin 中, 可以使用 `by` 关键字, 将接口实现委托给一个类的实例. 例如: ```KOTLIN class CanvasSession(val tool: DrawingTool) : DrawingTool by tool ``` 其中, `tool` 是 `PenTool` 类的实例名称, 成员函数的实现委托给它. 现在你不必为 `CanvasSession` 类中的成员函数添加实现了. 编译器会自动通过 `PenTool` 类为你完成这些. 这样可以为你避免编写大量样板代码的麻烦. 你只需要对子类中想要修改的行为添加代码即可. 例如, 如果你想要修改 `color` 属性的值: ```KOTLIN interface DrawingTool { val color: String fun draw(shape: String) fun erase(area: String) fun getToolInfo(): String } class PenTool : DrawingTool { override val color: String = "black" override fun draw(shape: String) { println("Drawing $shape using a pen in $color") } override fun erase(area: String) { println("Erasing $area with pen tool") } override fun getToolInfo(): String { return "PenTool(color=$color)" } } //sampleStart class CanvasSession(val tool: DrawingTool) : DrawingTool by tool { // 没有样板代码! override val color: String = "blue" } //sampleEnd fun main() { val pen = PenTool() val session = CanvasSession(pen) println("Pen color: ${pen.color}") // 输出结果为: Pen color: black println("Session color: ${session.color}") // 输出结果为: Session color: blue session.draw("circle") // 输出结果为: Drawing circle with pen in black session.erase("top-left corner") // 输出结果为: Erasing top-left corner with pen tool println(session.getToolInfo()) // 输出结果为: PenTool(color=black) } ``` 如果你想要, 你也可以在 `CanvasSession` 类中覆盖继承的成员函数的行为, 但现在你不必为每个继承的成员函数添加新代码. 详情请参见 [委托](delegation.html). ## 实际练习 ### 习题 1 想象你正在开发一个智能家居系统. 智能家居通常包含不同类型的设备, 它们都具备一些基本功能, 但也有一些独特的行为. 请在下面的示例代码中, 完成 `abstract` 类 `SmartDevice`, 让子类 `SmartLight` 能够成功编译. 然后, 创建另一个子类 `SmartThermostat`, 继承 `SmartDevice` 类, 并实现 `turnOn()` 和 `turnOff()` 函数, 这两个函数包含打印语句, 描述哪个加热器正在加热, 或已经关闭. 最后, 添加另一个函数, 名为 `adjustTemperature()`, 接受一个温度值参数, 并打印输出: `$name thermostat set to $temperature°C.` 提示 : 在 : `SmartDevice` : 类中, 添加 : `turnOn()` : 和 : `turnOff()` : 函数, 然后你可以在 : `SmartThermostat` : 类中覆盖这两个函数的行为. ```KOTLIN abstract class // 请在这里编写你的代码 class SmartLight(name: String) : SmartDevice(name) { override fun turnOn() { println("$name is now ON.") } override fun turnOff() { println("$name is now OFF.") } fun adjustBrightness(level: Int) { println("Adjusting $name brightness to $level%.") } } class SmartThermostat // 请在这里编写你的代码 fun main() { val livingRoomLight = SmartLight("Living Room Light") val bedroomThermostat = SmartThermostat("Bedroom Thermostat") livingRoomLight.turnOn() // 输出结果为: Living Room Light is now ON. livingRoomLight.adjustBrightness(10) // 输出结果为: Adjusting Living Room Light brightness to 10%. livingRoomLight.turnOff() // 输出结果为: Living Room Light is now OFF. bedroomThermostat.turnOn() // 输出结果为: Bedroom Thermostat thermostat is now heating. bedroomThermostat.adjustTemperature(5) // 输出结果为: Bedroom Thermostat thermostat set to 5°C. bedroomThermostat.turnOff() // 输出结果为: Bedroom Thermostat thermostat is now off. } ``` ```KOTLIN abstract class SmartDevice(val name: String) { abstract fun turnOn() abstract fun turnOff() } class SmartLight(name: String) : SmartDevice(name) { override fun turnOn() { println("$name is now ON.") } override fun turnOff() { println("$name is now OFF.") } fun adjustBrightness(level: Int) { println("Adjusting $name brightness to $level%.") } } class SmartThermostat(name: String) : SmartDevice(name) { override fun turnOn() { println("$name thermostat is now heating.") } override fun turnOff() { println("$name thermostat is now off.") } fun adjustTemperature(temperature: Int) { println("$name thermostat set to $temperature°C.") } } fun main() { val livingRoomLight = SmartLight("Living Room Light") val bedroomThermostat = SmartThermostat("Bedroom Thermostat") livingRoomLight.turnOn() // 输出结果为: Living Room Light is now ON. livingRoomLight.adjustBrightness(10) // 输出结果为: Adjusting Living Room Light brightness to 10%. livingRoomLight.turnOff() // 输出结果为: Living Room Light is now OFF. bedroomThermostat.turnOn() // 输出结果为: Bedroom Thermostat thermostat is now heating. bedroomThermostat.adjustTemperature(5) // 输出结果为: Bedroom Thermostat thermostat set to 5°C. bedroomThermostat.turnOff() // 输出结果为: Bedroom Thermostat thermostat is now off. } ``` ### 习题 2 创建一个接口 `Media`, 用来实现特定的媒体类, 例如 `Audio`, `Video`, 或 `Podcast`. 你的接口必须包含: * 一个属性 `title`, 表示媒体的标题. * 一个函数 `play()`, 播放媒体. 然后, 创建一个类 `Audio`, 实现 `Media` 接口. `Audio` 类必须在构造器中使用 `title` 属性, 而且必须有一个额外的属性 `composer`, 类型 为`String`. 在这个类中, 实现 `play()` 函数, 打印输出: `"Playing audio: $title, composed by $composer"`. 提示 : 你可以在类的头部使用 : `override` : 关键字, 在构造器中实现来自接口的属性. ```KOTLIN interface // 请在这里编写你的代码 class // 请在这里编写你的代码 fun main() { val audio = Audio("Symphony No. 5", "Beethoven") audio.play() // 输出结果为: Playing audio: Symphony No. 5, composed by Beethoven } ``` ```KOTLIN interface Media { val title: String fun play() } class Audio(override val title: String, val composer: String) : Media { override fun play() { println("Playing audio: $title, composed by $composer") } } fun main() { val audio = Audio("Symphony No. 5", "Beethoven") audio.play() // 输出结果为: Playing audio: Symphony No. 5, composed by Beethoven } ``` ### 习题 3 你正在为一个电子商务应用程序构建支付处理系统. 每一种支付方法需要能够对支付进行授权, 并处理一笔交易. 有些支付还需要能够处理退款. 1. 在 `Refundable` 接口中, 添加一个 `refund()` 函数, 处理退款. 2. 在 `PaymentMethod` 抽象类中: * 添加一个 `authorize()` 函数, 接受金额参数, 并打印输出一条包含金额的消息. * 添加一个 `processPayment()` 抽象函数, 也接受金额参数. 3. 创建一个 `CreditCard` 类, 实现 `Refundable` 接口和 `PaymentMethod` 抽象类. 在这个类中, 添加 `refund()` 和 `processPayment()` 函数的实现, 让它们打印以下语句: * `"Refunding $amount to the credit card."` * `"Processing credit card payment of $amount."` ```KOTLIN interface Refundable { // 请在这里编写你的代码 } abstract class PaymentMethod(val name: String) { // 请在这里编写你的代码 } class CreditCard // 请在这里编写你的代码 fun main() { val visa = CreditCard("Visa") visa.authorize(100.0) // 输出结果为: Authorizing payment of $100.0. visa.processPayment(100.0) // 输出结果为: Processing credit card payment of $100.0. visa.refund(50.0) // 输出结果为: Refunding $50.0 to the credit card. } ``` ```KOTLIN interface Refundable { fun refund(amount: Double) } abstract class PaymentMethod(val name: String) { fun authorize(amount: Double) { println("Authorizing payment of $$amount.") } abstract fun processPayment(amount: Double) } class CreditCard(name: String) : PaymentMethod(name), Refundable { override fun processPayment(amount: Double) { println("Processing credit card payment of $$amount.") } override fun refund(amount: Double) { println("Refunding $$amount to the credit card.") } } fun main() { val visa = CreditCard("Visa") visa.authorize(100.0) // 输出结果为: Authorizing payment of $100.0. visa.processPayment(100.0) // 输出结果为: Processing credit card payment of $100.0. visa.refund(50.0) // 输出结果为: Refunding $50.0 to the credit card. } ``` ### 习题 4 你有一个简单的消息应用程序, 包含一些基本功能, 但你想要添加一些功能来处理 智能 消息, 但不想大量重复代码. 在下面的代码中, 定义一个 `SmartMessenger` 类, 继承 `Messenger` 接口, 但将实现委托给一个 `BasicMessenger` 类的实例. 在 `SmartMessenger` 类中, 覆盖 `sendMessage()` 函数, 发送智能消息. 这个函数必须接受一个 `message` 参数, 并包含一个打印输出语句: `"Sending a smart message: $message"`. 此外还要调用来自 `BasicMessenger` 类的 `sendMessage()` 函数, 并给消息加上 `[smart]` 前缀. Note: 你不需要重写 `SmartMessenger` 类中的 `receiveMessage()` 函数. ```KOTLIN interface Messenger { fun sendMessage(message: String) fun receiveMessage(): String } class BasicMessenger : Messenger { override fun sendMessage(message: String) { println("Sending message: $message") } override fun receiveMessage(): String { return "You've got a new message!" } } class SmartMessenger // 请在这里编写你的代码 fun main() { val basicMessenger = BasicMessenger() val smartMessenger = SmartMessenger(basicMessenger) basicMessenger.sendMessage("Hello!") // 输出结果为: Sending message: Hello! println(smartMessenger.receiveMessage()) // 输出结果为: You've got a new message! smartMessenger.sendMessage("Hello from SmartMessenger!") // 输出结果为: Sending a smart message: Hello from SmartMessenger! // 输出结果为: Sending message: [smart] Hello from SmartMessenger! } ``` ```KOTLIN interface Messenger { fun sendMessage(message: String) fun receiveMessage(): String } class BasicMessenger : Messenger { override fun sendMessage(message: String) { println("Sending message: $message") } override fun receiveMessage(): String { return "You've got a new message!" } } class SmartMessenger(val basicMessenger: BasicMessenger) : Messenger by basicMessenger { override fun sendMessage(message: String) { println("Sending a smart message: $message") basicMessenger.sendMessage("[smart] $message") } } fun main() { val basicMessenger = BasicMessenger() val smartMessenger = SmartMessenger(basicMessenger) basicMessenger.sendMessage("Hello!") // 输出结果为: Sending message: Hello! println(smartMessenger.receiveMessage()) // 输出结果为: You've got a new message! smartMessenger.sendMessage("Hello from SmartMessenger!") // 输出结果为: Sending a smart message: Hello from SmartMessenger! // 输出结果为: Sending message: [smart] Hello from SmartMessenger! } ``` ## 下一步 [中级教程: 对象](kotlin-tour-intermediate-objects.html) # 中级教程: 对象 ![First step](images/icon-1-done.svg) [扩展函数](kotlin-tour-intermediate-extension-functions.html) ![Second step](images/icon-2-done.svg) [作用域函数](kotlin-tour-intermediate-scope-functions.html) ![Third step](images/icon-3-done.svg) [带接受者的 Lambda 表达式](kotlin-tour-intermediate-lambdas-receiver.html) ![Fourth step](images/icon-4-done.svg) [类与接口](kotlin-tour-intermediate-classes-interfaces.html) ![Fourth step](images/icon-5.svg) 对象 ![Sixth step](images/icon-6-todo.svg) [开放类与特殊类](kotlin-tour-intermediate-open-special-classes.html) ![Seventh step](images/icon-7-todo.svg) [属性](kotlin-tour-intermediate-properties.html) ![Eighth step](images/icon-8-todo.svg) [Null 值安全性](kotlin-tour-intermediate-null-safety.html) ![Ninth step](images/icon-9-todo.svg) [库与 API](kotlin-tour-intermediate-libraries-and-apis.html) 在这一章中, 你将探索对象声明, 扩展对类的理解. 这些知识将帮助你高效的管理整个项目的行为. ## 对象声明 在 Kotlin 中, 你可以使用 对象 声明 来声明一个只有唯一实例的类. 从某种意义上说, 你在声明类的 同时 也就创建了唯一的实例. 当你想要创建一个类, 并以你的程序中的唯一引用点的方式使用它, 或者想要协调它在整个系统中的行为, 对象声明会非常有用. Tip: 只有唯一一个易于访问的实例的类, 称为 单例(singleton). Kotlin 中的对象是 延迟加载(lazy) 的, 意思就是说, 它们只在被访问的时候才创建. Kotlin 还会确保所有的对象以线程安全的方式创建, 因此你不必手动检查. 要创建一个对象声明, 请使用 `object` 关键字: ```KOTLIN object DoAuth {} ``` 之后是你的 `object` 的名称, 并在大括号 `{}` 表示的对象 body 部中添加属性或成员函数. Note: 对象不能拥有构造器, 因此它们没有像类那样的头部. 例如, 假设你想要创建一个对象, 名为 `DoAuth`, 负责身份验证: ```KOTLIN object DoAuth { fun takeParams(username: String, password: String) { println("input Auth parameters = $username:$password") } } fun main(){ // 当 takeParams() 函数被调用时, 对象被创建 DoAuth.takeParams("coding_ninja", "N1njaC0ding!") // 输出结果为: input Auth parameters = coding_ninja:N1njaC0ding! } ``` 这个对象有一个成员函数, 名为 `takeParams`, 参数是 `username` 和 `password` 变量, 并打印一个字符串到控制台. 只有在函数初次被调用时, `DoAuth` 对象才会被创建. Note: 对象可以从类和接口继承. 例如: ```KOTLIN interface Auth { fun takeParams(username: String, password: String) } object DoAuth : Auth { override fun takeParams(username: String, password: String) { println("input Auth parameters = $username:$password") } } ``` ### 数据对象(Data Object) 为了更容易的打印输出对象声明的内容, Kotlin 提供了 数据(Data) 对象. 与你在初学者教程中学过的数据类类似, 数据对象自动带有额外的成员函数: `toString()` 和 `equals()`. Tip: 与数据类不同, 数据对象没有自动带有 `copy()` 成员函数, 因为它们只有唯一的实例, 不能复制. 要创建一个数据对象, 请使用与对象声明相同的语法, 但前面加上 `data` 关键字: ```KOTLIN data object AppConfig {} ``` 例如: ```KOTLIN data object AppConfig { var appName: String = "My Application" var version: String = "1.0.0" } fun main() { println(AppConfig) // 输出结果为: AppConfig println(AppConfig.appName) // 输出结果为: My Application } ``` 关于数据对象, 详情请参见 [数据对象](object-declarations.html#data-objects). ### 同伴对象(Companion Object) 在 Kotlin 中, 一个类可以带有一个对象: 一个 同伴(Companion) 对象. 对每个类, 你只能有 一个 同伴对象. 只有在类初次被引用时, 同伴对象才会被创建. 在同伴对象之内声明的任何属性或函数, 都在类的所有实例之间共享. 要在一个类之内创建一个同伴对象, 请使用与对象声明相同的语法, 但前面加上 `companion` 关键字: ```KOTLIN companion object Bonger {} ``` Note: 同伴对象不一定需要名称. 如果你没有定义名称, 则默认名称为 `Companion`. 要访问同伴对象的任何属性或函数, 请通过类名称来引用它. 例如: ```KOTLIN class BigBen { companion object Bonger { fun getBongs(nTimes: Int) { repeat(nTimes) { print("BONG ") } } } } fun main() { // 当类初次被引用时, 同伴对象被创建. BigBen.getBongs(12) // 输出结果为: BONG BONG BONG BONG BONG BONG BONG BONG BONG BONG BONG BONG } ``` 这个示例创建了一个类, 名为 `BigBen`, 它包含一个同伴对象, 名为 `Bonger`. 同伴对象有一个成员函数, 名为 `getBongs()`, 接受一个整数参数, 并打印 `"BONG"` 到控制台, 打印次数与整数参数相同. 在 `main()` 函数 中, 通过类的名称调用了 `getBongs()` 函数. 同伴对象会在这个时候被创建. 调用 `getBongs()` 函数的参数是 `12`. 详情请参见 [同伴对象(Companion Object)](object-declarations.html#companion-objects). ## 实际练习 ### 习题 1 你运营着一个咖啡店, 并有一个系统来追踪客户订单. 请参考下面的代码, 并完成第 2 个数据对象的声明, 让 `main()` 函数中的以下代码成功运行: ```KOTLIN interface Order { val orderId: String val customerName: String val orderTotal: Double } data object OrderOne: Order { override val orderId = "001" override val customerName = "Alice" override val orderTotal = 15.50 } data object // 请在这里编写你的代码 fun main() { // 打印输出每个数据对象的名称 println("Order name: $OrderOne") // 输出结果为: Order name: OrderOne println("Order name: $OrderTwo") // 输出结果为: Order name: OrderTwo // 检查订单是否相同 println("Are the two orders identical? ${OrderOne == OrderTwo}") // 输出结果为: Are the two orders identical? false if (OrderOne == OrderTwo) { println("The orders are identical.") } else { println("The orders are unique.") // 输出结果为: The orders are unique. } println("Do the orders have the same customer name? ${OrderOne.customerName == OrderTwo.customerName}") // 输出结果为: Do the orders have the same customer name? false } ``` ```KOTLIN interface Order { val orderId: String val customerName: String val orderTotal: Double } data object OrderOne: Order { override val orderId = "001" override val customerName = "Alice" override val orderTotal = 15.50 } data object OrderTwo: Order { override val orderId = "002" override val customerName = "Bob" override val orderTotal = 12.75 } fun main() { // 打印输出每个数据对象的名称 println("Order name: $OrderOne") // 输出结果为: Order name: OrderOne println("Order name: $OrderTwo") // 输出结果为: Order name: OrderTwo // 检查订单是否相同 println("Are the two orders identical? ${OrderOne == OrderTwo}") // 输出结果为: Are the two orders identical? false if (OrderOne == OrderTwo) { println("The orders are identical.") } else { println("The orders are unique.") // 输出结果为: The orders are unique. } println("Do the orders have the same customer name? ${OrderOne.customerName == OrderTwo.customerName}") // 输出结果为: Do the orders have the same customer name? false } ``` ### 习题 2 创建一个对象声明, 继承自 `Vehicle` 接口, 以创建一个唯一的车辆类型: `FlyingSkateboard`. 实现你的对象中的 `name` 属性和 `move()` 函数, 让 `main()` 函数中的以下代码成功运行: ```KOTLIN interface Vehicle { val name: String fun move(): String } object // 请在这里编写你的代码 fun main() { println("${FlyingSkateboard.name}: ${FlyingSkateboard.move()}") // 输出结果为: Flying Skateboard: Glides through the air with a hover engine println("${FlyingSkateboard.name}: ${FlyingSkateboard.fly()}") // 输出结果为: Flying Skateboard: Woooooooo } ``` ```KOTLIN interface Vehicle { val name: String fun move(): String } object FlyingSkateboard : Vehicle { override val name = "Flying Skateboard" override fun move() = "Glides through the air with a hover engine" fun fly(): String = "Woooooooo" } fun main() { println("${FlyingSkateboard.name}: ${FlyingSkateboard.move()}") // 输出结果为: Flying Skateboard: Glides through the air with a hover engine println("${FlyingSkateboard.name}: ${FlyingSkateboard.fly()}") // 输出结果为: Flying Skateboard: Woooooooo } ``` ### 习题 3 你有一个 App, 你想要用它记录温度. 类本身使用摄氏单位保存信息, 但你想要提供一个简单方法, 创建华氏单位的实例. 请完成数据类, 让 `main()` 函数中的以下代码成功运行: 提示 : 使用同伴对象. ```KOTLIN data class Temperature(val celsius: Double) { val fahrenheit: Double = celsius * 9 / 5 + 32 // 请在这里编写你的代码 } fun main() { val fahrenheit = 90.0 val temp = Temperature.fromFahrenheit(fahrenheit) println("${temp.celsius}°C is $fahrenheit °F") // 输出结果为: 32.22222222222222°C is 90.0 °F } ``` ```KOTLIN data class Temperature(val celsius: Double) { val fahrenheit: Double = celsius * 9 / 5 + 32 companion object { fun fromFahrenheit(fahrenheit: Double): Temperature = Temperature((fahrenheit - 32) * 5 / 9) } } fun main() { val fahrenheit = 90.0 val temp = Temperature.fromFahrenheit(fahrenheit) println("${temp.celsius}°C is $fahrenheit °F") // 输出结果为: 32.22222222222222°C is 90.0 °F } ``` ## 下一步 [中级教程: 开放类与特殊类](kotlin-tour-intermediate-open-special-classes.html) # 中级教程: 开放类与特殊类 ![First step](images/icon-1-done.svg) [扩展函数](kotlin-tour-intermediate-extension-functions.html) ![Second step](images/icon-2-done.svg) [作用域函数](kotlin-tour-intermediate-scope-functions.html) ![Third step](images/icon-3-done.svg) [带接受者的 Lambda 表达式](kotlin-tour-intermediate-lambdas-receiver.html) ![Fourth step](images/icon-4-done.svg) [类与接口](kotlin-tour-intermediate-classes-interfaces.html) ![Fifth step](images/icon-5-done.svg) [对象](kotlin-tour-intermediate-objects.html) ![Fourth step](images/icon-6.svg) 开放类与特殊类 ![Seventh step](images/icon-7-todo.svg) [属性](kotlin-tour-intermediate-properties.html) ![Eighth step](images/icon-8-todo.svg) [Null 值安全性](kotlin-tour-intermediate-null-safety.html) ![Ninth step](images/icon-9-todo.svg) [库与 API](kotlin-tour-intermediate-libraries-and-apis.html) 在这一章中, 你将学习开放类, 它们如何与接口一起工作, 以及 Kotlin 中其他特殊类型的类. ## 开放类 如果你不能使用接口或抽象类, 你可以将一个类声明为 open, 明确的让它能够被继承. 方法是, 在你的类声明之前使用 `open` 关键字: ```KOTLIN open class Vehicle(val make: String, val model: String) ``` 要创建一个从另一个类继承的类, 请在你的类头部之后添加一个冒号, 然后调用你想要继承的父类的构造器. 这个示例中, `Car` 类继承 `Vehicle` 类: ```KOTLIN open class Vehicle(val make: String, val model: String) class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model) fun main() { // 创建 Car 类的一个实例 val car = Car("Toyota", "Corolla", 4) // 打印输出汽车的详细信息 println("Car Info: Make - ${car.make}, Model - ${car.model}, Number of doors - ${car.numberOfDoors}") // 输出结果为: Car Info: Make - Toyota, Model - Corolla, Number of doors - 4 } ``` 和创建普通类的实例一样, 如果你的类继承一个父类, 那么它必须初始化父类头部中定义的所有参数. 因此在这个示例中, `Car` 类的 `car` 实例初始化父类参数: `make` 和 `model`. ### 覆盖继承的行为 如果你想要从一个类继承, 但改变某些行为, 你可以覆盖继承的行为. 默认情况下, 不能覆盖父类的成员函数或属性. 与抽象类一样, 你需要添加特殊的关键字. #### 成员函数 要让父类中的函数能够被覆盖, 请在父类中它的声明之前使用 `open` 关键字: ```KOTLIN open fun displayInfo() {} ``` 要覆盖一个继承的成员函数, 请在子类中函数声明之前使用 `override` 关键字: ```KOTLIN override fun displayInfo() {} ``` 例如: ```KOTLIN open class Vehicle(val make: String, val model: String) { open fun displayInfo() { println("Vehicle Info: Make - $make, Model - $model") } } class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model) { override fun displayInfo() { println("Car Info: Make - $make, Model - $model, Number of Doors - $numberOfDoors") } } fun main() { val car1 = Car("Toyota", "Corolla", 4) val car2 = Car("Honda", "Civic", 2) // 使用覆盖的 displayInfo() 函数 car1.displayInfo() // 输出结果为: Car Info: Make - Toyota, Model - Corolla, Number of Doors - 4 car2.displayInfo() // 输出结果为: Car Info: Make - Honda, Model - Civic, Number of Doors - 2 } ``` 在这个示例中: * `Car` 类继承自 `Vehicle` 类, 创建 `Car` 类的 2 个实例: `car1` 和 `car2`. * 在 `Car` 类中, 覆盖 `displayInfo()` 函数, 也打印输出车门数量. * 对 `car1` 和 `car2` 实例调用覆盖的 `displayInfo()` 函数. #### 属性 在 Kotlin 中, 使用 `open` 关键字让一个属性能够继承, 并在之后覆盖它, 这样的方法不是常见的做法. 大多数情况下, 使用抽象类或接口, 其中的属性默认能够继承. 开放类中的属性能够被子类访问. 一般来说, 最好直接访问属性, 而不要用新的属性覆盖它们. 例如, 假设你有一个属性 `transmissionType`, 你想要之后覆盖它. 覆盖属性的语法与覆盖成员函数完全一样. 你可以这样做: ```KOTLIN open class Vehicle(val make: String, val model: String) { open val transmissionType: String = "Manual" } class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model) { override val transmissionType: String = "Automatic" } ``` 但是, 这不是好的做法. 相反, 你可以将属性添加到可继承的类的构造中, 并在创建 `Car` 子类时声明它的值: ```KOTLIN open class Vehicle(val make: String, val model: String, val transmissionType: String = "Manual") class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model, "Automatic") ``` 直接访问属性, 而不是覆盖, 可以让代码更加简单, 更加易读. 只在父类中声明属性一次, 然后通过构造器传递属性值, 就不再需要在子类中进行覆盖. 关于类的继承, 以及覆盖类的行为, 详情请参见 [继承](inheritance.html). ### 开放类与接口 你可以创建一个类, 它继承一个类 并且 实现多个接口. 这种情况下, 你必须在冒号之后先声明父类, 然后列出接口: ```KOTLIN // 定义接口 interface EcoFriendly { val emissionLevel: String } interface ElectricVehicle { val batteryCapacity: Double } // 父类 open class Vehicle(val make: String, val model: String) // 子类 open class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model) // 新的类, 继承 Car, 并实现 2 个接口 class ElectricCar( make: String, model: String, numberOfDoors: Int, val capacity: Double, val emission: String ) : Car(make, model, numberOfDoors), EcoFriendly, ElectricVehicle { override val batteryCapacity: Double = capacity override val emissionLevel: String = emission } ``` ## 特殊类 除抽象类, 开放类, 数据类之外, Kotlin 还有一些特殊类型的类, 是为各种目的设计的, 例如限制特定的行为, 或减少创建小对象时的性能损失. ### 封闭类(Sealed Class) 有些时候你可能会想要限制继承. 你可以使用封闭类(Sealed Class)来实现. 封闭类是一个特殊类型的 [抽象类](kotlin-tour-intermediate-classes-interfaces.html#abstract-classes). 一旦将一个类声明为封闭, 就只能在同一个包之内创建它的子类. 在这个范围之外, 不能继承封闭类. Tip: 包是一组相关的类和函数的代码的集合, 通常放在一个目录中. 关于 Kotlin 中的包, 详情请参见 [包与导入](packages.html). 要创建一个封闭类, 请使用 `sealed` 关键字: ```KOTLIN sealed class Mammal ``` 封闭类在与 `when` 表达式一起使用时特别有用. 使用 `when` 表达式, 你可以对所有可能的子类定义行为. 例如: ```KOTLIN sealed class Mammal(val name: String) class Cat(val catName: String) : Mammal(catName) class Human(val humanName: String, val job: String) : Mammal(humanName) fun greetMammal(mammal: Mammal): String { when (mammal) { is Human -> return "Hello ${mammal.name}; You're working as a ${mammal.job}" is Cat -> return "Hello ${mammal.name}" } } fun main() { println(greetMammal(Cat("Snowy"))) // 输出结果为: Hello Snowy } ``` 在这个示例中: * 有一个封闭类 `Mammal`, 构造器参数为 `name`. * `Cat` 类继承 `Mammal` 封闭类, 并使用它自己的构造器中的 `catName` 参数, 作为 `Mammal` 类的 `name` 参数. * `Human` 类继承 `Mammal` 封闭类, 并使用它自己的构造器中的 `humanName` 参数, 作为 `Mammal` 类的 `name` 参数. 它的构造器中还有 `job` 参数. * `greetMammal()` 函数接受 `Mammal` 类型的参数, 并返回一个字符串. * 在 `greetMammal()` 的函数 body 部, 有一个 `when` 表达式, 使用 [is 操作符](typecasts.html#is-and-is-operators) 检查 `mammal` 的类型, 决定执行哪个动作. * `main()` 函数调用 `greetMammal()` 函数, 使用 `Cat` 类的一个实例, `name` 参数为 `Snowy`. Tip: 这个教程系列关于 `is` 操作符的详细讨论, 请参见 [Null 值安全性](kotlin-tour-intermediate-null-safety.html). 关于封闭类, 以及推荐的使用场景, 详情请参见 [封闭类与封闭接口](sealed-classes.html). ### 枚举类(Enum Class) 当你想用一个类表达一组有限的, 不同的值, 适合使用枚举类(Enum Class). 一个枚举类包含一些枚举常数, 枚举常数自身又是枚举类的实例. 要创建枚举类, 请使用 `enum` 关键字: ```KOTLIN enum class State ``` 假设你想要创建一个枚举类, 包含一个进程的不同状态. 各个枚举常数必须使用逗号 `,` 分隔: ```KOTLIN enum class State { IDLE, RUNNING, FINISHED } ``` `State` 枚举类包含枚举常数: `IDLE`, `RUNNING`, 和 `FINISHED`. 要访问一个枚举常数, 请使用类名称, 加上 `.`, 再加上枚举常数的名称: ```KOTLIN val state = State.RUNNING ``` 你可以在 `when` 表达式中使用这个枚举类, 根据枚举常数的值定义要执行的动作: ```KOTLIN enum class State { IDLE, RUNNING, FINISHED } fun main() { val state = State.RUNNING val message = when (state) { State.IDLE -> "It's idle" State.RUNNING -> "It's running" State.FINISHED -> "It's finished" } println(message) // 输出结果为: It's running } ``` 和通常的类一样, 枚举类可以拥有属性和成员函数. 例如, 假设你在使用 HTML, 想要创建一个枚举类, 包含一些颜色. 你想要每个颜色拥有一个属性, 假设叫做 `rgb`, 其中包含它们的 16 进制 RGB 值. 在创建枚举常数时, 你必须使用这个属性来初始化它: ```KOTLIN enum class Color(val rgb: Int) { RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF), YELLOW(0xFFFF00) } ``` Note: Kotlin 将 16 进制数保存为整数, 因此 `rgb` 属性使用 `Int` 类型, 而不是 `String` 类型. 要添加对这个类一个成员函数, 将函数与枚举常数用分号 `;` 分隔: ```KOTLIN enum class Color(val rgb: Int) { RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF), YELLOW(0xFFFF00); fun containsRed() = (this.rgb and 0xFF0000 != 0) } fun main() { val red = Color.RED // 对枚举常数调用 containsRed() 函数 println(red.containsRed()) // 输出结果为: true // 使用类名称, 对枚举常数调用 containsRed() 函数 println(Color.BLUE.containsRed()) // 输出结果为: false println(Color.YELLOW.containsRed()) // 输出结果为: true } ``` 在这个示例中, `containsRed()` 成员函数使用 `this` 关键字访问枚举常数的 `rgb` 属性的值, 并检查 16 进制值的最先头的位是否包含 `FF`, 并返回一个 boolean 值. 详情请参见 [枚举类](enum-classes.html). ### 内联的值类(Inline Value Class) 有时候在你的代码中, 你可能想要创建小的类对象, 而且只是短暂的使用它们. 这种方法可能造成性能损失. 内联的值类(Inline Value Class) 是一种特殊类型的类, 可以避免这样的性能损失. 但是, 它们只能包含值. 要创建一个内联的值类, 请使用 `value` 关键字, 以及 `@JvmInline` 注解: ```KOTLIN @JvmInline value class Email ``` Tip: `@JvmInline` 注解 指示 Kotlin 在编译代码时进行优化. 详情请参见 [注解](annotations.html). 内联的值类 必须 拥有单个属性, 在类的 header 部初始化. 假设你想要创建一个类, 收集 EMail 地址: ```KOTLIN // address 属性在类的 header 部初始化. @JvmInline value class Email(val address: String) fun sendEmail(email: Email) { println("Sending email to ${email.address}") } fun main() { val myEmail = Email("example@example.com") sendEmail(myEmail) // 输出结果为: Sending email to example@example.com } ``` 在这个示例中: * `Email` 是一个内联的值类, 在类的 header 部有一个属性: `address`. * `sendEmail()` 函数接受 `Email` 类型的对象作为参数, 并向标准输出打印一个字符串. * `main()` 函数: * 创建 `Email` 类的一个实例 `myEmail`. * 对 `myEmail` 对象调用 `sendEmail()` 函数. 通过使用内联的值类, 你让你的类成为内联的, 可以在代码中直接使用它, 而不必创建对象. 这样可以显著的减少内存使用量, 并改善你的代码的运行时性能. 关于内联的值类, 详情请参见 [内联的值类](inline-classes.html). ## 实际练习 ### 习题 1 你管理着一家快递公司, 需要一种方法来追踪包裹的状态. 请创建一个封闭类 `DeliveryStatus`, 包含数据类, 表示以下状态: `Pending`, `InTransit`, `Delivered`, `Canceled`. 请完成 `DeliveryStatus` 类的声明, 让 `main()` 函数中的代码运行成功: ```KOTLIN sealed class // 请在这里编写你的代码 fun printDeliveryStatus(status: DeliveryStatus) { when (status) { is DeliveryStatus.Pending -> { println("The package is pending pickup from ${status.sender}.") } is DeliveryStatus.InTransit -> { println("The package is in transit and expected to arrive by ${status.estimatedDeliveryDate}.") } is DeliveryStatus.Delivered -> { println("The package was delivered to ${status.recipient} on ${status.deliveryDate}.") } is DeliveryStatus.Canceled -> { println("The delivery was canceled due to: ${status.reason}.") } } } fun main() { val status1: DeliveryStatus = DeliveryStatus.Pending("Alice") val status2: DeliveryStatus = DeliveryStatus.InTransit("2024-11-20") val status3: DeliveryStatus = DeliveryStatus.Delivered("2024-11-18", "Bob") val status4: DeliveryStatus = DeliveryStatus.Canceled("Address not found") printDeliveryStatus(status1) // 输出结果为: The package is pending pickup from Alice. printDeliveryStatus(status2) // 输出结果为: The package is in transit and expected to arrive by 2024-11-20. printDeliveryStatus(status3) // 输出结果为: The package was delivered to Bob on 2024-11-18. printDeliveryStatus(status4) // 输出结果为: The delivery was canceled due to: Address not found. } ``` ```KOTLIN sealed class DeliveryStatus { data class Pending(val sender: String) : DeliveryStatus() data class InTransit(val estimatedDeliveryDate: String) : DeliveryStatus() data class Delivered(val deliveryDate: String, val recipient: String) : DeliveryStatus() data class Canceled(val reason: String) : DeliveryStatus() } fun printDeliveryStatus(status: DeliveryStatus) { when (status) { is DeliveryStatus.Pending -> { println("The package is pending pickup from ${status.sender}.") } is DeliveryStatus.InTransit -> { println("The package is in transit and expected to arrive by ${status.estimatedDeliveryDate}.") } is DeliveryStatus.Delivered -> { println("The package was delivered to ${status.recipient} on ${status.deliveryDate}.") } is DeliveryStatus.Canceled -> { println("The delivery was canceled due to: ${status.reason}.") } } } fun main() { val status1: DeliveryStatus = DeliveryStatus.Pending("Alice") val status2: DeliveryStatus = DeliveryStatus.InTransit("2024-11-20") val status3: DeliveryStatus = DeliveryStatus.Delivered("2024-11-18", "Bob") val status4: DeliveryStatus = DeliveryStatus.Canceled("Address not found") printDeliveryStatus(status1) // 输出结果为: The package is pending pickup from Alice. printDeliveryStatus(status2) // 输出结果为: The package is in transit and expected to arrive by 2024-11-20. printDeliveryStatus(status3) // 输出结果为: The package was delivered to Bob on 2024-11-18. printDeliveryStatus(status4) // 输出结果为: The delivery was canceled due to: Address not found. } ``` ### 习题 2 在你的程序中, 需要处理不同状态和类型的错误. 你有一个封闭类, 捕捉数据类或对象中声明的各种状态. 请完成下面的代码, 创建枚举类 `Problem`, 表示不同的问题类型: `NETWORK`, `TIMEOUT`, and `UNKNOWN`. ```KOTLIN sealed class Status { data object Loading : Status() data class Error(val problem: Problem) : Status() { // 请在这里编写你的代码 } data class OK(val data: List) : Status() } fun handleStatus(status: Status) { when (status) { is Status.Loading -> println("Loading...") is Status.OK -> println("Data received: ${status.data}") is Status.Error -> when (status.problem) { Status.Error.Problem.NETWORK -> println("Network issue") Status.Error.Problem.TIMEOUT -> println("Request timed out") Status.Error.Problem.UNKNOWN -> println("Unknown error occurred") } } } fun main() { val status1: Status = Status.Error(Status.Error.Problem.NETWORK) val status2: Status = Status.OK(listOf("Data1", "Data2")) handleStatus(status1) // 输出结果为: Network issue handleStatus(status2) // 输出结果为: Data received: [Data1, Data2] } ``` ```KOTLIN sealed class Status { data object Loading : Status() data class Error(val problem: Problem) : Status() { enum class Problem { NETWORK, TIMEOUT, UNKNOWN } } data class OK(val data: List) : Status() } fun handleStatus(status: Status) { when (status) { is Status.Loading -> println("Loading...") is Status.OK -> println("Data received: ${status.data}") is Status.Error -> when (status.problem) { Status.Error.Problem.NETWORK -> println("Network issue") Status.Error.Problem.TIMEOUT -> println("Request timed out") Status.Error.Problem.UNKNOWN -> println("Unknown error occurred") } } } fun main() { val status1: Status = Status.Error(Status.Error.Problem.NETWORK) val status2: Status = Status.OK(listOf("Data1", "Data2")) handleStatus(status1) // 输出结果为: Network issue handleStatus(status2) // 输出结果为: Data received: [Data1, Data2] } ``` ## 下一步 [中级教程: 属性](kotlin-tour-intermediate-properties.html) # 中级教程: 属性 ![First step](images/icon-1-done.svg) [扩展函数](kotlin-tour-intermediate-extension-functions.html) ![Second step](images/icon-2-done.svg) [作用域函数](kotlin-tour-intermediate-scope-functions.html) ![Third step](images/icon-3-done.svg) [带接受者的 Lambda 表达式](kotlin-tour-intermediate-lambdas-receiver.html) ![Fourth step](images/icon-4-done.svg) [类与接口](kotlin-tour-intermediate-classes-interfaces.html) ![Fifth step](images/icon-5-done.svg) [对象](kotlin-tour-intermediate-objects.html) ![Sixth step](images/icon-6-done.svg) [开放类与特殊类](kotlin-tour-intermediate-open-special-classes.html) ![Seventh step](images/icon-7.svg) 属性 ![Eighth step](images/icon-8-todo.svg) [Null 值安全性](kotlin-tour-intermediate-null-safety.html) ![Ninth step](images/icon-9-todo.svg) [库与 API](kotlin-tour-intermediate-libraries-and-apis.html) 在初学者教程中, 你已经学习了如何使用属性来声明类实例的特征, 以及如何访问属性. 在这一章中, 我们进一步深入介绍 Kotlin 中的属性如何工作, 并探索在代码中使用属性的其它方式. ## 后端域变量(Backing Field) 在 Kotlin 中, 属性拥有默认的 `get()` 和 `set()` 函数, 称为属性访问器, 负责获取和修改属性值. 这些默认函数在代码中并不明确的可见, 编译器自动生成这些函数, 在后台管理属性的访问. 这些访问器使用一个 后端域变量(Backing Field) 来存储实际的属性值. 如果以下条件中的任何一个成立, 后端域变量就会存在: * 你对属性使用默认的 `get()` 或 `set()` 函数. * 你在代码中使用 `field` 关键字访问属性值. Tip: `get()` 和 `set()` 函数也叫做取值函数(getter)和设值函数(setter). 例如, 这段代码有一个 `category` 属性, 它没有自定义的 `get()` 或 `set()` 函数, 因此使用默认的实现: ```KOTLIN class Contact(val id: Int, var email: String) { var category: String = "" } ``` 在底层实现中, 这段代码等价于下面的伪代码: ```KOTLIN class Contact(val id: Int, var email: String) { var category: String = "" get() = field set(value) { field = value } } ``` 在这个示例中: * `get()` 函数从域变量获取属性值: `""`. * `set()` 函数接受参数 `value`, 并将它赋值给域变量, 其中 `value` 为 `""`. 当你想要在你的 `get()` 或 `set()` 函数中添加额外的逻辑, 又不引起无限的循环, 访问后端域变量会很有用. 例如, 你有一个 `Person` 类, 它有一个 `name` 属性: ```KOTLIN class Person { var name: String = "" } ``` 你想要确保 `name` 属性的首字母为大写, 因此创建了一个自定义 `set()` 函数, 它使用 [.replaceFirstChar()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/replace-first-char.html) 和 [.uppercase()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/uppercase-char.html) extension 函数. 但是, 如果在你的 `set()` 函数中直接引用属性, 就会导致无限循环, 并在运行期发生 `StackOverflowError` 错误: ```KOTLIN class Person { var name: String = "" set(value) { // 这里会导致运行期错误 name = value.replaceFirstChar { firstChar -> firstChar.uppercase() } } } fun main() { val person = Person() person.name = "kodee" println(person.name) // 这里会发生错误: Exception in thread "main" java.lang.StackOverflowError } ``` 要解决这个问题, 可以在你的 `set()` 函数中改为通过 `field` 关键字引用后端域变量: ```KOTLIN class Person { var name: String = "" set(value) { field = value.replaceFirstChar { firstChar -> firstChar.uppercase() } } } fun main() { val person = Person() person.name = "kodee" println(person.name) // 输出结果为: Kodee } ``` 当你想要添加日志, 在属性值变更时发送通知, 或者使用附加逻辑比较属性的旧值和新值时, 后端域变量也很有用. 详情请参见 [后端域变量](properties.html#backing-fields). ## 扩展属性 和扩展函数一样, 也有扩展属性. 扩展属性让你能够向既有的类添加新的属性, 而不必修改它们的源代码. 但是, Kotlin 中的扩展属性 没有 后端域变量. 这就意味着你需要自己编写 `get()` 和 `set()` 函数. 此外, 没有后端域变量也意味着扩展属性不能保存任何状态. 要声明一个扩展属性, 请在你想要扩展的类的名称之后加上 `.`, 再加上属性的名称. 和通常的类属性一样, 你需要为你的属性声明类型. 例如: ```KOTLIN val String.lastChar: Char ``` 当你想要属性包含计算得到的值, 而不使用继承时, 扩展属性是很有用的. 你可以将扩展属性想象为一个函数, 只有一个参数: 接受者. 例如, 假设你有一个数据类 `Person`, 它有 2 个属性: `firstName` 和 `lastName`. ```KOTLIN data class Person(val firstName: String, val lastName: String) ``` 你想要得到人的全名, 但不要修改 `Person` data 类, 也不要继承它. 你可以创建一个带有自定义 `get()` 函数的扩展属性来实现这一点: ```KOTLIN data class Person(val firstName: String, val lastName: String) // 扩展属性, 用于得到全名 val Person.fullName: String get() = "$firstName $lastName" fun main() { val person = Person(firstName = "John", lastName = "Doe") // 使用扩展属性 println(person.fullName) // 输出结果为: John Doe } ``` Note: 扩展属性不能覆盖既有的类属性. 与扩展函数一样, Kotlin 标准库大量使用了扩展属性. 例如, 请参见 `CharSequence` 的 [lastIndex 属性](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/last-index.html). ## 委托属性(Delegated Property) 在 [类与接口](kotlin-tour-intermediate-classes-interfaces.html#delegation) 章节中, 你已经学习了委托. 你也可以对属性使用委托, 将它们的属性访问器委托给另一个对象. 当你的需求比存储属性更加复杂, 简单的后端域变量无法处理时, 委托属性会很有用, 例如需要将值存储到数据表中, 浏览器会话中, 或 Map 中. 使用委托属性(Delegated Property) 也可以减少样板代码, 因为取得和设置你的属性的逻辑只存在于你委托的对象中. 委托属性的语法与类的委托类似, 但操作层级不同. 请声明你的属性, 后面加上 `by` 关键字, 再加上你想要委托的对象. 例如: ```KOTLIN val displayName: String by Delegate ``` 这里, 委托属性 `displayName` 使用 `Delegate` 对象作为它的属性访问器. 你委托的每个对象 必须 有一个 `getValue()` 操作符函数, Kotlin 使用它来获取委托属性的值. 如果属性是可变的, 还必须有一个 `setValue()` 操作符函数, Kotlin 使用它来设置委托属性的值. 默认情况下, `getValue()` 和 `setValue()` 函数的结构如下: ```KOTLIN operator fun getValue(thisRef: Any?, property: KProperty<*>): String {} operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {} ``` 在这些函数中: * `operator` 关键字将这些函数标记为操作符函数, 允许它们覆盖 `get()` 和 `set()` 函数. * `thisRef` 参数表示 包含 委托属性的对象. 默认情况下, 类型设置为 `Any?`, 但你可能需要声明更具体的类型. * `property` 参数表示值正在被访问或被修改的那个属性. 你可以使用这个参数来获取属性信息, 例如属性的名称或类型. 默认情况下, 类型设置为 `KProperty<*>`, 但你也可以使用 `Any?`. 在你的代码中, 不必进行修改. `getValue()` 函数的返回类型默认为 `String`, 但如果你需要, 可以调整这个类型. `setValue()` 函数有一个额外的参数 `value`, 用来保存正在赋值给属性的新值. 那么, 在实际运用中是什么样的呢? 假设你想要一个计算得到的属性, 例如用户的显示名称, 它只计算一次, 因为这个操作性能开销较大, 而你的应用程序对性能比较敏感. 你可以使用一个委托属性来缓存显示名称, 让它只计算一次, 但可以随时读取, 而不带来性能损失. 首先, 你需要创建负责委托的对象. 在这个示例中, 对象将是 `CachedStringDelegate` 类的一个实例: ```KOTLIN class CachedStringDelegate { var cachedValue: String? = null } ``` `cachedValue` 属性包含缓存的值. 在 `CachedStringDelegate` 类中, 将你在委托属性的 `get()` 函数中想要的行为, 添加到 `getValue()` 操作符函数的 body 部: ```KOTLIN class CachedStringDelegate { var cachedValue: String? = null operator fun getValue(thisRef: Any?, property: KProperty<*>): String { if (cachedValue == null) { cachedValue = "Default Value" println("Computed and cached: $cachedValue") } else { println("Accessed from cache: $cachedValue") } return cachedValue ?: "Unknown" } } ``` `getValue()` 函数检查 `cachedValue` 属性是否为 `null`. 如果是, 函数将它赋值为 `"Default value"`, 并打印输出一个字符串, 作为日志. 如果 `cachedValue` 属性已经有了计算的值, 那么属性不为 `null`. 这种情况下, 并打印输出另一个字符串, 作为日志. 最后, 函数使用 Elvis 操作符, 返回缓存的值, 或者如果值为 `null`, 则返回 `"Unknown"`. 现在你可以将想要缓存的属性(`val displayName`)委托给 `CachedStringDelegate` 类的实例: ```KOTLIN import kotlin.reflect.KProperty class CachedStringDelegate { var cachedValue: String? = null operator fun getValue(thisRef: User, property: KProperty<*>): String { if (cachedValue == null) { cachedValue = "${thisRef.firstName} ${thisRef.lastName}" println("Computed and cached: $cachedValue") } else { println("Accessed from cache: $cachedValue") } return cachedValue ?: "Unknown" } } class User(val firstName: String, val lastName: String) { val displayName: String by CachedStringDelegate() } fun main() { val user = User("John", "Doe") // 第 1 次访问属性时, 计算值, 并缓存 println(user.displayName) // 输出结果为: Computed and cached: John Doe // 输出结果为: John Doe // 后续访问属性时, 会从缓存获取值 println(user.displayName) // 输出结果为: Accessed from cache: John Doe // 输出结果为: John Doe } ``` 在这个示例中: * 创建一个 `User` 类, 它的 header 部有 2 个属性, `firstName`, 和 `lastName`, body 部有 1 个 属性, `displayName`. * 将 `displayName` 属性委托给 `CachedStringDelegate` 类的实例. * 创建 `User` 类的一个实例 `user`. * 打印输出对 `user` 实例访问 `displayName` 属性的结果. 请注意, 在 `getValue()` 函数中, `thisRef` 参数的类型从 `Any?` 类型缩小到了对象类型: `User`. 这是为了让编译器能够访问 `User` 类的 `firstName` 和 `lastName` 属性. ### 标准委托 Kotlin 标准库提供了一些有用的委托, 让你不必总是从头创建. 如果你使用这些委托, 你不需要定义 `getValue()` 和 `setValue()` 函数, 因为标准库会自动提供. #### 延迟加载(Lazy)属性 为了只在初次访问时才初始化一个属性, 请使用延迟加载(Lazy)属性. 标准库为委托提供了 `Lazy` 接口. 要创建 `Lazy` 接口的实例, 请使用 `lazy()` 函数, 给它提供一个 Lambda 表达式, `get()` 函数第一次被调用时会执行这个 Lambda 表达式. 之后对 `get()` 函数的任何调用都会返回与第一次调用时提供的相同结果. 延迟加载属性使用 [尾缀 Lambda 表达式(Trailing Lambda)](kotlin-tour-functions.html#trailing-lambdas) 语法来传递 Lambda 表达式. 例如: ```KOTLIN class Database { fun connect() { println("Connecting to the database...") } fun query(sql: String): List { return listOf("Data1", "Data2", "Data3") } } val databaseConnection: Database by lazy { val db = Database() db.connect() db } fun fetchData() { val data = databaseConnection.query("SELECT * FROM data") println("Data: $data") } fun main() { // 第 1 次访问 databaseConnection fetchData() // 输出结果为: Connecting to the database... // 输出结果为: Data: [Data1, Data2, Data3] // 后续访问, 会使用已有的连接 fetchData() // 输出结果为: Data: [Data1, Data2, Data3] } ``` 在这个示例中: * 有一个 `Database` 类,它有 `connect()` 和 `query()` 成员函数. * `connect()` 函数向控制台打印输出一个字符串, `query()` 函数接受一个 SQL 查询, 返回一个 List. * 有一个 `databaseConnection` 属性, 它是延迟加载属性. * 向 `lazy()` 函数提供的 Lambda 表达式: * 创建一个 `Database` 类实例. * 对这个实例(`db`)调用 `connect()` 成员函数. * 返回这个实例. * 有一个 `fetchData()` 函数: * 对 `databaseConnection` 属性调用 `query()` 函数, 创建一个 SQL 查询. * 将 SQL 查询赋值给 `data` 变量. * 将 `data` 变量打印输出到控制台. * `main()` 函数调用 the `fetchData()` 函数. 第 1 次被调用时, 延迟加载属性会被初始化. 第 2 次被调用时, 会返回与第 1 次调用相同的结果. 延迟加载属性不仅对资源密集型的初始化有用, 而且对于你的代码中可能不会用到的属性也很有用. 此外, 延迟加载属性默认是线程安全的, 这一点对于并发环境尤其有用. 详情请参见 [延迟加载属性](delegated-properties.html#lazy-properties). #### 可观察(Observable)属性 要监测属性值的变更, 请使用可观察(Observable)属性. 可观察属性 is useful when 如果你想要监测属性值的变更, 并利用这个信息来触发某种反应, 可观察属性会很有用. 标准库提供了 `Delegates` 对象可以用作委托. 要创建一个可观察属性, 你首先要导入 `kotlin.properties.Delegates.observable`. 然后, 使用 `observable()` 函数, 并为这个函数提供一个 Lambda 表达式, 当属性发生变更时会执行这个 Lambda 表达式. 与延迟加载属性一样, 可观察属性使用 [尾缀 Lambda 表达式(Trailing Lambda)](kotlin-tour-functions.html#trailing-lambdas) 语法来传递 Lambda 表达式. 例如: ```KOTLIN import kotlin.properties.Delegates.observable class Thermostat { var temperature: Double by observable(20.0) { _, old, new -> if (new > 25) { println("Warning: Temperature is too high! ($old°C -> $new°C)") } else { println("Temperature updated: $old°C -> $new°C") } } } fun main() { val thermostat = Thermostat() thermostat.temperature = 22.5 // 输出结果为: Temperature updated: 20.0°C -> 22.5°C thermostat.temperature = 27.0 // 输出结果为: Warning: Temperature is too high! (22.5°C -> 27.0°C) } ``` 在这个示例中: * 有一个 `Thermostat` 类, 包含一个可观察属性: `temperature`. * `observable()` 函数接受参数 `20.0`, 并将使用它来初始化属性. * 提供给 `observable()` 函数的 Lambda 表达式: * 有 3 个参数: * `_`, 表示属性本身. * `old`, 表示属性的旧值. * `new`, 表示属性的新值. * 检查 `new` 参数是否大于 `25`, 根据检查结果, 向控制台打印输出一个字符串. * `main()` 函数: * 创建 `Thermostat` 类的一个实例 `thermostat`. * 将实例的 `temperature` 属性值更新到 `22.5`, 这时会触发温度更新信息的打印输出语句. * 将实例的 `temperature` 属性值更新到 `27.0`, 这时会触发警告信息的打印输出语句. 可观察属性不仅可用于日志输出和调试目的. 还可以用于其它使用场景, 例如UI 更新, 或执行额外检查, 例如验证数据有效性. 详情请参见 [可观察属性](delegated-properties.html#observable-properties). ## 实际练习 ### 习题 1 你管理着一家书店的库存系统. 库存信息保存在一个 List 中, 其中的每个元素表示某种书的数量. 例如, `listOf(3, 0, 7, 12)` 表示书店中第 1 种书有 3 份, 第 2 种书有 0 份, 第 3 种书有 7 份, 第 4 种书有 12 份. 请编写一个函数 `findOutOfStockBooks()`, 返回一个 List, 其中包含所有缺货书籍的索引. 提示 1 : 使用标准库中的 : [indices](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/indices.html) : 扩展属性. 提示 2 : 你可以使用 : [buildList()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/build-list.html) : 函数来创建和管理 List, 而不是手动的创建并返回一个可变的 List. : `buildList()` : 函数使用一个带接受者的 Lambda 表达式, 你在前面的章节中已经学过. ```KOTLIN fun findOutOfStockBooks(inventory: List): List { // 请在这里编写你的代码 } fun main() { val inventory = listOf(3, 0, 7, 0, 5) println(findOutOfStockBooks(inventory)) // 输出结果为: [1, 3] } ``` ```KOTLIN fun findOutOfStockBooks(inventory: List): List { val outOfStockIndices = mutableListOf() for (index in inventory.indices) { if (inventory[index] == 0) { outOfStockIndices.add(index) } } return outOfStockIndices } fun main() { val inventory = listOf(3, 0, 7, 0, 5) println(findOutOfStockBooks(inventory)) // 输出结果为: [1, 3] } ``` ```KOTLIN fun findOutOfStockBooks(inventory: List): List = buildList { for (index in inventory.indices) { if (inventory[index] == 0) { add(index) } } } fun main() { val inventory = listOf(3, 0, 7, 0, 5) println(findOutOfStockBooks(inventory)) // 输出结果为: [1, 3] } ``` ### 习题 2 你有一个旅行 App, 需要以公里和英里为单位显示距离. 请为 `Double` 类型创建一个扩展属性 `asMiles`, 将距离从公里转换为英里: Note: 从公里转换为英里的公式是 `miles = kilometers * 0.621371`. 提示 : 请记住, 扩展属性需要自定义的 : `get()` : 函数. ```KOTLIN val // 请在这里编写你的代码 fun main() { val distanceKm = 5.0 println("$distanceKm km is ${distanceKm.asMiles} miles") // 输出结果为: 5.0 km is 3.106855 miles val marathonDistance = 42.195 println("$marathonDistance km is ${marathonDistance.asMiles} miles") // 输出结果为: 42.195 km is 26.218757 miles } ``` ```KOTLIN val Double.asMiles: Double get() = this * 0.621371 fun main() { val distanceKm = 5.0 println("$distanceKm km is ${distanceKm.asMiles} miles") // 输出结果为: 5.0 km is 3.106855 miles val marathonDistance = 42.195 println("$marathonDistance km is ${marathonDistance.asMiles} miles") // 输出结果为: 42.195 km is 26.218757 miles } ``` ### 习题 3 你有一个系统健康状况检查器, 能够检查云系统的状态. 它有 2 个函数用来执行健康状况检查, 但是这 2 个函数会消耗大量性能. 请使用延迟加载属性来初始化这些检查, 让这些性能消耗巨大的函数只在需要是运行: ```KOTLIN fun checkAppServer(): Boolean { println("Performing application server health check...") return true } fun checkDatabase(): Boolean { println("Performing database health check...") return false } fun main() { // 请在这里编写你的代码 when { isAppServerHealthy -> println("Application server is online and healthy") isDatabaseHealthy -> println("Database is healthy") else -> println("System is offline") } // 输出结果为: Performing application server health check... // 输出结果为: Application server is online and healthy } ``` ```KOTLIN fun checkAppServer(): Boolean { println("Performing application server health check...") return true } fun checkDatabase(): Boolean { println("Performing database health check...") return false } fun main() { val isAppServerHealthy by lazy { checkAppServer() } val isDatabaseHealthy by lazy { checkDatabase() } when { isAppServerHealthy -> println("Application server is online and healthy") isDatabaseHealthy -> println("Database is healthy") else -> println("System is offline") } // 输出结果为: Performing application server health check... // 输出结果为: Application server is online and healthy } ``` ### 习题 4 你正在构建一个简单的预算追踪 App. App 需要监测用户预算余额的变化, 并在余额低于某个阈值时通知用户. 你有一个 `Budget` 类, 使用 `totalBudget` 属性初始化, 这个属性包含预算初始金额. 请在这个类中创建一个可观察属性 `remainingBudget`, 它需要: * 当余额低于预算初始金额的 20% 时, 打印输出一个警告信息. * 当预算高于前一个值时, 打印输出一个鼓励信息. ```KOTLIN import kotlin.properties.Delegates.observable class Budget(val totalBudget: Int) { var remainingBudget: Int // 请在这里编写你的代码 } fun main() { val myBudget = Budget(totalBudget = 1000) myBudget.remainingBudget = 800 myBudget.remainingBudget = 150 // 输出结果为: Warning: Your remaining budget (150) is below 20% of your total budget. myBudget.remainingBudget = 50 // 输出结果为: Warning: Your remaining budget (50) is below 20% of your total budget. myBudget.remainingBudget = 300 // 输出结果为: Good news: Your remaining budget increased to 300. } ``` ```KOTLIN import kotlin.properties.Delegates.observable class Budget(val totalBudget: Int) { var remainingBudget: Int by observable(totalBudget) { _, oldValue, newValue -> if (newValue < totalBudget * 0.2) { println("Warning: Your remaining budget ($newValue) is below 20% of your total budget.") } else if (newValue > oldValue) { println("Good news: Your remaining budget increased to $newValue.") } } } fun main() { val myBudget = Budget(totalBudget = 1000) myBudget.remainingBudget = 800 myBudget.remainingBudget = 150 // 输出结果为: Warning: Your remaining budget (150) is below 20% of your total budget. myBudget.remainingBudget = 50 // 输出结果为: Warning: Your remaining budget (50) is below 20% of your total budget. myBudget.remainingBudget = 300 // 输出结果为: Good news: Your remaining budget increased to 300. } ``` ## 下一步 [中级教程: Null 值安全性](kotlin-tour-intermediate-null-safety.html) # 中级教程: Null 值安全性 ![First step](images/icon-1-done.svg) [扩展函数](kotlin-tour-intermediate-extension-functions.html) ![Second step](images/icon-2-done.svg) [作用域函数](kotlin-tour-intermediate-scope-functions.html) ![Third step](images/icon-3-done.svg) [带接受者的 Lambda 表达式](kotlin-tour-intermediate-lambdas-receiver.html) ![Fourth step](images/icon-4-done.svg) [类与接口](kotlin-tour-intermediate-classes-interfaces.html) ![Fifth step](images/icon-5-done.svg) [对象](kotlin-tour-intermediate-objects.html) ![Sixth step](images/icon-6-done.svg) [开放类与特殊类](kotlin-tour-intermediate-open-special-classes.html) ![Seventh step](images/icon-7-done.svg) [属性](kotlin-tour-intermediate-properties.html) ![Eighth step](images/icon-8.svg) Null 值安全性 ![Ninth step](images/icon-9-todo.svg) [库与 API](kotlin-tour-intermediate-libraries-and-apis.html) 在初学者教程中, 你已经学习了如何在代码中处理 `null` 值. 这一章介绍 Null 值安全性功能的常见使用场景, 以及如何充分利用这些功能. ## 智能类型转换(Smart Cast) 与安全类型转换(Safe Cast) Kotlin 有些时候能够推断类型, 不需要明确的声明. 如果你告诉 Kotlin 将一个变量或对象当作某个特定的类型处理, 这个过程叫做 类型转换. 如果类型能够自动转换时, 例如能够推断得到, 称为 智能类型转换(Smart Cast). ### is 和 !is 操作符 在探索类型转换的工作原理之前, 我们来看看如何检查一个对象是不是某个类型. 为了实现这样的检查, 你可以使用 `is` 和 `!is` 操作符, 与 `when` 或 `if` 条件表达式: * `is` 检查对象是否属于这个类型, 并返回 boolean 值. * `!is` 检查对象是否 不属于 这个类型, 并返回 boolean 值. 例如: ```KOTLIN fun printObjectType(obj: Any) { when (obj) { is Int -> println("It's an Integer with value $obj") !is Double -> println("It's NOT a Double") else -> println("Unknown type") } } fun main() { val myInt = 42 val myDouble = 3.14 val myList = listOf(1, 2, 3) // 类型为 Int printObjectType(myInt) // 输出结果为: It's an Integer with value 42 // 类型为 List, 因此它不是 Double. printObjectType(myList) // 输出结果为: It's NOT a Double // 类型为 Double, 因此会执行 else 分支. printObjectType(myDouble) // 输出结果为: Unknown type } ``` Tip: 在 [开放类与特殊类](kotlin-tour-intermediate-open-special-classes.html#sealed-classes) 章节中, 你已经看到了如何使用 `when` 调节表达式以及 `is` 和 `!is` 操作符的示例. ### as 和 as? 操作符 要明确的将一个对象 转换 为另一个类型, 请使用 `as` 操作符. 包括从可为 Null 的类型转换为不可为 Null 类型的情况. 如果无法转换, 程序会在 运行期 崩溃. 所以 `as` 被称为 不安全的 类型转换操作符. ```KOTLIN fun main() { //sampleStart val a: String? = null val b = a as String // 这里会发生运行期错误 print(b) //sampleEnd } ``` 要明确的将一个对象转换为不可为 Null 的类型, 但在转换失败时返回 `null` 而不是抛出异常, 请使用 `as?` 操作符. 由于 `as?` 操作符在失败时不会发生错误, 因此称为 安全的 类型转换操作符. ```KOTLIN fun main() { //sampleStart val a: String? = null val b = a as? String // 返回 null 值 print(b) // null //sampleEnd } ``` 你可以将 `as?` 操作符与 Elvis 操作符 `?:` 结合起来, 将多行代码精简为一行. 例如, 下面的 `calculateTotalStringLength()` 函数计算一个混合 List 中提供的所有字符串的总长度: ```KOTLIN fun calculateTotalStringLength(items: List): Int { var totalLength = 0 for (item in items) { totalLength += if (item is String) { item.length } else { 0 // 对于不是字符串的元素, 加 0 } } return totalLength } ``` 在这个示例中: * 使用 `totalLength` 变量作为计数器. * 使用 `for` 循环, 遍历 List 中的每个元素. * 使用 `if` 和 `is` 操作符, 检查当前元素是不是字符串: * 如果是, 将字符串长度加到计数器. * 如果不是, 计数器不会增加. * 返回 `totalLength` 变量最终的值. 这段代码可以精简为: ```KOTLIN fun calculateTotalStringLength(items: List): Int { return items.sumOf { (it as? String)?.length ?: 0 } } ``` 这个示例使用 [.sumOf()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/sum-of.html) 扩展函数, 并对这个函数提供一个 Lambda 表达式: * 对 List 中的每个元素, 使用 `as?`, 执行安全的类型转换, 转换为 `String`. * 使用安全调用 `?.`, 如果前面的调用没有返回 `null` 值, 则访问 `length` 属性. * 使用 Elvis 操作符 `?:`, 如果安全调用返回 `null` 值, 返回 `0`. ## Null 值与集合 在 Kotlin 中, 使用集合经常需要处理 `null` 值, 并过滤掉不需要的元素. Kotlin 有很多有用的函数, 在处理 List, Set, Map, 和其他类型的集合时, 你可以用它们编写出简洁, 高效, 而且 Null 值安全的代码. 从一个 List 过滤掉 `null` 值, 请使用 [filterNotNull()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/filter-not-null.html) 函数: ```KOTLIN fun main() { //sampleStart val emails: List = listOf("alice@example.com", null, "bob@example.com", null, "carol@example.com") val validEmails = emails.filterNotNull() println(validEmails) // 输出结果为: [alice@example.com, bob@example.com, carol@example.com] //sampleEnd } ``` 如果你想要在创建 List 时直接过滤 `null` 值, 请使用 [listOfNotNull()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/list-of-not-null.html) 函数: ```KOTLIN fun main() { //sampleStart val serverConfig = mapOf( "appConfig.json" to "App Configuration", "dbConfig.json" to "Database Configuration" ) val requestedFile = "appConfig.json" val configFiles = listOfNotNull(serverConfig[requestedFile]) println(configFiles) // 输出结果为: [App Configuration] //sampleEnd } ``` 在这两个示例中, 如果所有元素都是 `null` 值, 会返回空的 List. Kotlin 还提供了一些函数, 可以在集合中查找值. 如果值没有找到, 这些函数会返回 `null` 值, 而不是发生错误: * [maxOrNull()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/max-or-null.html) 查找最大值. 如果不存在, 返回 `null` 值. * [minOrNull()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/min-or-null.html) 查找最小值. 如果不存在, 返回 `null` 值. 例如: ```KOTLIN fun main() { //sampleStart // 一周的温度记录 val temperatures = listOf(15, 18, 21, 21, 19, 17, 16) // 查找一周中的最高温度 val maxTemperature = temperatures.maxOrNull() println("Highest temperature recorded: ${maxTemperature ?: "No data"}") // 输出结果为: Highest temperature recorded: 21 // 查找一周中的最低温度 val minTemperature = temperatures.minOrNull() println("Lowest temperature recorded: ${minTemperature ?: "No data"}") // 输出结果为: Lowest temperature recorded: 15 //sampleEnd } ``` 这个示例中, 如果函数返回 `null` 值, 则使用 Elvis 操作符 `?:` 返回打印输出语句. Note: `maxOrNull()`, 和 `minOrNull()` 函数只能用于 不 包含 `null` 值的集合. 否则, 你就无法区分: 函数找不到需要的值? 还是它找到了 `null` 值? 你可以使用 [singleOrNull()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/single-or-null.html) 函数, 以 Lambda 表达式作为参数, 来查找匹配条件的单个元素. 如果值不存在, 或者相同的值存在多个元素, 这个函数返回 `null` 值. ```KOTLIN fun main() { //sampleStart // 一周的温度记录 val temperatures = listOf(15, 18, 21, 21, 19, 17, 16) // 检查是否恰好有 1 天的温度为 30 度 val singleHotDay = temperatures.singleOrNull{ it == 30 } println("Single hot day with 30 degrees: ${singleHotDay ?: "None"}") // 输出结果为: Single hot day with 30 degrees: None //sampleEnd } ``` Note: `singleOrNull()` 函数只能用于 不 包含 `null` 值的集合. 有些函数使用 Lambda 表达式来转换集合, 如果无法实现目的, 则返回 `null` 值. 要使用 Lambda 表达式转换集合, 并返回第一个值非 `null` 的值, 请使用 [firstNotNullOfOrNull()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/first-not-null-of-or-null.html) 函数. 如果不存在这样的值, 函数返回 `null` 值: ```KOTLIN fun main() { //sampleStart data class User(val name: String?, val age: Int?) val users = listOf( User(null, 25), User("Alice", null), User("Bob", 30) ) val firstNonNullName = users.firstNotNullOfOrNull { it.name } println(firstNonNullName) // 输出结果为: Alice //sampleEnd } ``` 要使用 Lambda 表达式顺序的处理每个集合元素, 并创建一个累计的值 (或者如果集合为空, 返回 `null` 值), 请使用 [reduceOrNull()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/reduce-or-null.html) 函数: ```KOTLIN fun main() { //sampleStart // 购物车中商品的价格 val itemPrices = listOf(20, 35, 15, 40, 10) // 使用 reduceOrNull() 函数计算总价格 val totalPrice = itemPrices.reduceOrNull { runningTotal, price -> runningTotal + price } println("Total price of items in the cart: ${totalPrice ?: "No items"}") // 输出结果为: Total price of items in the cart: 120 val emptyCart = listOf() val emptyTotalPrice = emptyCart.reduceOrNull { runningTotal, price -> runningTotal + price } println("Total price of items in the empty cart: ${emptyTotalPrice ?: "No items"}") // 输出结果为: Total price of items in the empty cart: No items //sampleEnd } ``` 这个示例中, 如果函数返回 `null` 值, 也使用 Elvis 操作符 `?:` 返回打印输出语句. Note: `reduceOrNull()` 函数只能用于 不 包含 `null` 值的集合. 请参见 Kotlin 的 [标准库](https://kotlinlang.org/api/core/kotlin-stdlib/), 其中还有很多函数, 你可以使用它们来提高代码安全性. ## 提前返回(Early Return) 与 Elvis 操作符 在初学者教程中, 你已经学习了如何使用 [提前返回(Early Return)](kotlin-tour-functions.html#early-returns-in-functions), 让你的函数在某个阶段之后停止进一步的处理. 你可以使用 Elvis 操作符 `?:` 和提前返回, 在函数中检查先决条件. 使用这种方式, 能够很好的保持代码简洁, 因为你不需要使用嵌套的检查. 代码复杂度降低也让它更加易于维护. 例如: ```KOTLIN data class User( val id: Int, val name: String, // 朋友 user 的 ID List val friends: List ) // 得到一个用户的朋友数量的函数 fun getNumberOfFriends(users: Map, userId: Int): Int { // 获取用户, 如果没有找到, 返回 -1 val user = users[userId] ?: return -1 // 返回朋友数量 return user.friends.size } fun main() { // 创建一些示例用户 val user1 = User(1, "Alice", listOf(2, 3)) val user2 = User(2, "Bob", listOf(1)) val user3 = User(3, "Charlie", listOf(1)) // 创建用户 Map val users = mapOf(1 to user1, 2 to user2, 3 to user3) println(getNumberOfFriends(users, 1)) // 输出结果为: 2 println(getNumberOfFriends(users, 2)) // 输出结果为: 1 println(getNumberOfFriends(users, 4)) // 输出结果为: -1 } ``` 在这个示例中: * 有一个 `User` 数据类, 属性表示用户的 `id`, `name`, 以及朋友列表. * `getNumberOfFriends()` 函数: * 它的参数是一个 `User` 实例的 Map, 以及一个整数类型的用户 ID. * 使用给定的 用户 ID, 找到 `User` 实例 Map 的值. * 如果 Map 值是 `null` 值, 使用 Elvis 操作符提前返回, 返回值为 `-1`. * 将从 Map 找到的值, 赋值给 `user` 变量. * 使用 `size` 属性, 返回用户的朋友列表中的朋友数量. * `main()` 函数: * 创建 3 个 `User` 实例. * 创建这些 `User` 实例的 Map, 并赋值给 `users` 变量. * 对 `users` 变量, 使用值 `1` 和 `2` 调用 `getNumberOfFriends()` 函数, 对 `"Alice"` 返回 2 个朋友, 对 `"Bob"` 返回 1 个朋友. * 对 `users` 变量, 使用值 `4` 调用 `getNumberOfFriends()` 函数, 会发生提前返回, 返回值为 `-1`. 你可能会注意到, 如果没有提前返回, 代码可以更加简洁. 但是, 这种方法需要很多次安全调用, 因为 `users[userId]` 可能返回 `null` 值, 造成代码变得有点难以阅读: ```KOTLIN fun getNumberOfFriends(users: Map, userId: Int): Int { // 获取用户, 如果没有找到, 返回 -1 return users[userId]?.friends?.size ?: -1 } ``` 这个示例中, 尽管只使用 Elvis 操作符检查了一个条件, 但你可以添加多个检查来覆盖任何重要的错误路径. 使用 Elvis 操作符提前返回能够防止你的程序执行不必要的工作, 并在检测到 `null` 值或不正确的情况时立即停止执行, 让代码更加安全. 关于如何在代码中使用 `return`, 详情请参见 [返回与跳转](returns.html). ## 实际练习 ### 习题 1 你在为一个 App 开发通知系统, 使用者能够启用或禁用不同类型的通知. 请完成 `getNotificationPreferences()` 函数, 目标是: 1. `validUser` 变量使用 `as?` 操作符, 检查 `user` 是不是 `User` 类的实例. 如果不是, 返回空的 List. 2. `userName` 变量使用 Elvis `?:` 操作符, 当使用者为 `null` 时, 让名字的默认值为 `"Guest"`. 3. 最后的返回语句使用 `.takeIf()` 函数, 只对 EMail 和 SMS 启用的情况, 包含它们的通知选项. 4. `main()` 函数成功返回, 并打印输出期望的输出结果. Tip: [takeIf() 函数](scope-functions.html#takeif-and-takeunless) 只有在给定的条件为 true 时返回原来的值, 否则返回 `null`. 例如: ```KOTLIN fun main() { // 使用者已经登录 val userIsLoggedIn = true // 使用者有一个活跃的会话 val hasSession = true // 如果使用者已经登录, 并且有活跃的会话, 则允许访问 Dashboard val canAccessDashboard = userIsLoggedIn.takeIf { hasSession } println(canAccessDashboard ?: "Access denied") // 输出结果为: true } ``` ```KOTLIN data class User(val name: String?) fun getNotificationPreferences(user: Any, emailEnabled: Boolean, smsEnabled: Boolean): List { val validUser = // 请在这里编写你的代码 val userName = // 请在这里编写你的代码 return listOfNotNull( /* 请在这里编写你的代码 */) } fun main() { val user1 = User("Alice") val user2 = User(null) val invalidUser = "NotAUser" println(getNotificationPreferences(user1, emailEnabled = true, smsEnabled = false)) // 输出结果为: [Email Notifications enabled for Alice] println(getNotificationPreferences(user2, emailEnabled = false, smsEnabled = true)) // 输出结果为: [SMS Notifications enabled for Guest] println(getNotificationPreferences(invalidUser, emailEnabled = true, smsEnabled = true)) // 输出结果为: [] } ``` ```KOTLIN data class User(val name: String?) fun getNotificationPreferences(user: Any, emailEnabled: Boolean, smsEnabled: Boolean): List { val validUser = user as? User ?: return emptyList() val userName = validUser.name ?: "Guest" return listOfNotNull( "Email Notifications enabled for $userName".takeIf { emailEnabled }, "SMS Notifications enabled for $userName".takeIf { smsEnabled } ) } fun main() { val user1 = User("Alice") val user2 = User(null) val invalidUser = "NotAUser" println(getNotificationPreferences(user1, emailEnabled = true, smsEnabled = false)) // 输出结果为: [Email Notifications enabled for Alice] println(getNotificationPreferences(user2, emailEnabled = false, smsEnabled = true)) // 输出结果为: [SMS Notifications enabled for Guest] println(getNotificationPreferences(invalidUser, emailEnabled = true, smsEnabled = true)) // 输出结果为: [] } ``` ### 习题 2 你在开发一个基于订阅的流媒体服务, 使用者可以拥有多个订阅, 但 一次只能有一个处于活跃状态. 请完成 `getActiveSubscription()` 函数, 使用 `singleOrNull()` 函数, 指定的判断条件是, 如果存在多个活跃的订阅则返回 `null` 值: ```KOTLIN data class Subscription(val name: String, val isActive: Boolean) fun getActiveSubscription(subscriptions: List): Subscription? // 请在这里编写你的代码 fun main() { val userWithPremiumPlan = listOf( Subscription("Basic Plan", false), Subscription("Premium Plan", true) ) val userWithConflictingPlans = listOf( Subscription("Basic Plan", true), Subscription("Premium Plan", true) ) println(getActiveSubscription(userWithPremiumPlan)) // 输出结果为: Subscription(name=Premium Plan, isActive=true) println(getActiveSubscription(userWithConflictingPlans)) // 输出结果为: null } ``` ```KOTLIN data class Subscription(val name: String, val isActive: Boolean) fun getActiveSubscription(subscriptions: List): Subscription? { return subscriptions.singleOrNull { subscription -> subscription.isActive } } fun main() { val userWithPremiumPlan = listOf( Subscription("Basic Plan", false), Subscription("Premium Plan", true) ) val userWithConflictingPlans = listOf( Subscription("Basic Plan", true), Subscription("Premium Plan", true) ) println(getActiveSubscription(userWithPremiumPlan)) // 输出结果为: Subscription(name=Premium Plan, isActive=true) println(getActiveSubscription(userWithConflictingPlans)) // 输出结果为: null } ``` ```KOTLIN data class Subscription(val name: String, val isActive: Boolean) fun getActiveSubscription(subscriptions: List): Subscription? = subscriptions.singleOrNull { it.isActive } fun main() { val userWithPremiumPlan = listOf( Subscription("Basic Plan", false), Subscription("Premium Plan", true) ) val userWithConflictingPlans = listOf( Subscription("Basic Plan", true), Subscription("Premium Plan", true) ) println(getActiveSubscription(userWithPremiumPlan)) // 输出结果为: Subscription(name=Premium Plan, isActive=true) println(getActiveSubscription(userWithConflictingPlans)) // 输出结果为: null } ``` ### 习题 3 你在开发一个社交媒体平台, 使用者有用户名称和帐号状态. 你想要看到目前活跃的用户名称列表. 请完成 `getActiveUsernames()` 函数, 让 [mapNotNull() 函数](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/map-not-null.html) 的判断条件在用户状态为活跃时返回用户名称, 否则返回 `null` 值: ```KOTLIN data class User(val username: String, val isActive: Boolean) fun getActiveUsernames(users: List): List { return users.mapNotNull { /* 请在这里编写你的代码 */ } } fun main() { val allUsers = listOf( User("alice123", true), User("bob_the_builder", false), User("charlie99", true) ) println(getActiveUsernames(allUsers)) // 输出结果为: [alice123, charlie99] } ``` Tip: 和习题 1 一样, 检查用户是否活跃时, 可以使用 [takeIf() 函数](scope-functions.html#takeif-and-takeunless). ```KOTLIN data class User(val username: String, val isActive: Boolean) fun getActiveUsernames(users: List): List { return users.mapNotNull { user -> if (user.isActive) user.username else null } } fun main() { val allUsers = listOf( User("alice123", true), User("bob_the_builder", false), User("charlie99", true) ) println(getActiveUsernames(allUsers)) // 输出结果为: [alice123, charlie99] } ``` ```KOTLIN data class User(val username: String, val isActive: Boolean) fun getActiveUsernames(users: List): List = users.mapNotNull { user -> user.username.takeIf { user.isActive } } fun main() { val allUsers = listOf( User("alice123", true), User("bob_the_builder", false), User("charlie99", true) ) println(getActiveUsernames(allUsers)) // 输出结果为: [alice123, charlie99] } ``` ### 习题 4 你正在为一个电子商务平台开发库存管理系统. 在处理一次销售之前, 你需要根据可用的库存数量检查要求的产品数量是否正确. 请完成 `validateStock()` 函数, 使用提前返回和 Elvis 操作符 (如果可能的话) 进行检查: * `requested` 变量是否为 `null`. * `available` 变量是否为 `null`. * `requested` 变量是否为负值. * `requested` 变量的值是否高于 `available` 变量. 对以上所有情况, 函数必须提前返回 `-1`. ```KOTLIN fun validateStock(requested: Int?, available: Int?): Int { // 请在这里编写你的代码 } fun main() { println(validateStock(5,10)) // 输出结果为: 5 println(validateStock(null,10)) // 输出结果为: -1 println(validateStock(-2,10)) // 输出结果为: -1 } ``` ```KOTLIN fun validateStock(requested: Int?, available: Int?): Int { val validRequested = requested ?: return -1 val validAvailable = available ?: return -1 if (validRequested < 0) return -1 if (validRequested > validAvailable) return -1 return validRequested } fun main() { println(validateStock(5,10)) // 输出结果为: 5 println(validateStock(null,10)) // 输出结果为: -1 println(validateStock(-2,10)) // 输出结果为: -1 } ``` ## 下一步 [中级教程: 库与 API](kotlin-tour-intermediate-libraries-and-apis.html) # 中级教程: 库与 API ![First step](images/icon-1-done.svg) [扩展函数](kotlin-tour-intermediate-extension-functions.html) ![Second step](images/icon-2-done.svg) [作用域函数](kotlin-tour-intermediate-scope-functions.html) ![Third step](images/icon-3-done.svg) [带接受者的 Lambda 表达式](kotlin-tour-intermediate-lambdas-receiver.html) ![Fourth step](images/icon-4-done.svg) [类与接口](kotlin-tour-intermediate-classes-interfaces.html) ![Fifth step](images/icon-5-done.svg) [对象](kotlin-tour-intermediate-objects.html) ![Sixth step](images/icon-6-done.svg) [开放类与特殊类](kotlin-tour-intermediate-open-special-classes.html) ![Seventh step](images/icon-7-done.svg) [属性](kotlin-tour-intermediate-properties.html) ![Eighth step](images/icon-8-done.svg) [Null 值安全性](kotlin-tour-intermediate-null-safety.html) ![Ninth step](images/icon-9.svg) 库与 API 为了更加充分的利用 Kotlin, 请使用既有的库和 API, 这样你就可以将更多的时间用来编码, 花更少的时间来重新发明轮子. 库分发了可重用的代码, 简化常见任务. 库中包含了包和对象, 将相关的类, 函数, 和实用工具组织在一起. 库公开了 API (Application Programming Interface), 包含一组函数, 类, 或属性, 开发者可以在他们的代码中使用. ![Kotlin 库和 API](images/kotlin-library-diagram.svg) 我们来探索一下 Kotlin 能够做到什么. ## 标准库 Kotlin 有一个标准库, 提供了必要的类型, 函数, 集合, 以及实用工具, 让你的代码更加简洁, 而且富有表现力. 在任何 Kotlin 文件中可以使用标准库 (everything in the [kotlin 包](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/)) 的大部分内容, 不需要明确的导入: ```KOTLIN fun main() { val text = "emosewa si niltoK" // 使用标准库的 reversed() 函数 val reversedText = text.reversed() // 使用标准库的 print() 函数 print(reversedText) // 输出结果为: Kotlin is awesome } ``` 但是, 标准库的有些部分, 在你的代码中使用之前需要导入. 例如, 如果你想要使用标准库的时间测量功能, 你需要导入 [kotlin.time 包](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.time/). 请你的文件的最上方, 添加 `import` 关键字, 之后是你需要的包: ```KOTLIN import kotlin.time.* ``` 星号 `*` 是通配符导入, 告诉 Kotlin 导入包中的所有内容. 你不能对同伴对象使用 `*`. 相反, 对于你想要使用的同伴对象成员, 需要明确声明. 例如: ```KOTLIN import kotlin.time.Duration import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes fun main() { val thirtyMinutes: Duration = 30.minutes val halfHour: Duration = 0.5.hours println(thirtyMinutes == halfHour) // 输出结果为: true } ``` 在这个示例中: * 导入 `Duration` 类, 以及它的同伴对象中的 `hours` 和 `minutes` 扩展属性. * 使用 `minutes` 属性, 将 `30` 转换为一个表示 30 分钟时间长度的 `Duration`. * 使用 `hours` 属性, 将 `0.5` 转换为一个表示 30 分钟时间长度的 `Duration`. * 检查这两个时间长度是否相等, 并打印输出结果. ### 在构建库之前请先检索 在你决定编写你自己的代码之前, 请先检查标准库, 看看你在寻找的东西是否已经存在了. 标准库已经为你提供了很多类, 函数, 和属性, 下面是一个列表: * [集合](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/) * [序列](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.sequences/) * [字符串操作](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.text/) * [时间管理](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.time/) 关于标准库中的其它内容, 请查看它的 [API 参考文档](https://kotlinlang.org/api/core/kotlin-stdlib/). ## Kotlin 库 标准库涵盖了很多常见的使用场景, 但还有一些它没有解决的情况. 幸运的是, Kotlin 开发组和社区的其它成员开发了大量的库, 来完善和补充标准库. 例如, [kotlinx-datetime](https://kotlinlang.org/api/kotlinx-datetime/) 能够帮助你在不同的平台上管理时间. 你可以在我们的 [检索平台](https://klibs.io/) 找到有用的库. 要使用这些库, 你需要一些额外的步骤, 例如添加依赖项或 plugin. 每个库都有一个 GitHub 代码仓库, 包含如何将它包含到你的 Kotlin 项目的说明. 添加库之后, 你就可以导入其中的任何包. 下面是一个示例, 演示如何导入 `kotlinx-datetime` 包, 查找纽约的当前时间: ```KOTLIN import kotlinx.datetime.* fun main() { val now = Clock.System.now() // 得到当前时刻 println("Current instant: $now") val zone = TimeZone.of("America/New_York") val localDateTime = now.toLocalDateTime(zone) println("Local date-time in NY: $localDateTime") } ``` 在这个示例中: * 导入 `kotlinx.datetime` 包. * 使用 `Clock.System.now()` 函数, 创建 `Instant` 类的一个实例, 其中包含当前时间, 并将结果赋值给 `now` 变量. * 打印输出当前时间. * 使用 `TimeZone.of()` 函数, 找到纽约的时区, 并将结果赋值给 `zone` 变量. * 在包含当前时间的实例上调用 `.toLocalDateTime()` 函数, 使用纽约的时区作为参数. * 将结果赋值给 `localDateTime` 变量. * 打印输出针对纽约的时区调整后的时间. Tip: 要了解这个示例中使用的函数和类, 详情请参见 [API 参考文档](https://kotlinlang.org/api/kotlinx-datetime/kotlinx-datetime/kotlinx.datetime/). ## 对 API 选择使用者同意(Opt-in) 库的作者可能会对某些 API 标记为, 在你的代码中使用之前, 需要使用者同意(Opt-in). 当 API 还处于开发阶段, 未来可能发生变化时, 通常会这样做. 如果你不进行用者同意(Opt-in), 你会看到类似这样的警告或错误信息: ```TEXT This declaration needs opt-in. Its usage should be marked with '@...' or '@OptIn(...)' ``` 要选择使用者同意(Opt-in), 请标注 `@OptIn`, 之后是括号, 括号之内是对 API 进行分组的类名称, 之后是 2 个冒号 `::` 和 `class`. 例如, 标准库的 `uintArrayOf()` 函数属于 `@ExperimentalUnsignedTypes`, 如 [API 参考文档](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/to-u-int-array.html) 所示: ```KOTLIN @ExperimentalUnsignedTypes inline fun uintArrayOf(vararg elements: UInt): UIntArray ``` 在你的代码中, 使用者同意(Opt-in)大致如下: ```KOTLIN @OptIn(ExperimentalUnsignedTypes::class) ``` 下面是一个示例, 对使用 `uintArrayOf()` 函数选择使用者同意(Opt-in), 创建一个无符号整数的数组, 并修改其中一个元素: ```KOTLIN @OptIn(ExperimentalUnsignedTypes::class) fun main() { // 创建一个无符号整数的数组 val unsignedArray: UIntArray = uintArrayOf(1u, 2u, 3u, 4u, 5u) // 修改一个元素 unsignedArray[2] = 42u println("Updated array: ${unsignedArray.joinToString()}") // 输出结果为: Updated array: 1, 2, 42, 4, 5 } ``` 这是选择使用者同意(Opt-in)的最简单的方法, 但也有其它方法. 详情请参见 [明确要求使用者同意的功能](opt-in-requirements.html). ## 实际练习 ### 习题 1 你正在开发一个金融应用程序, 帮助使用者计算他们的投资的未来价值. 计算复利的公式是: $A = P \times (1 + \displaystyle\frac{r}{n})^{nt}$ 其中: * `A` 是计算利息后的累计金额 (本金 + 利息). * `P` 是本金 (初始投资额). * `r` 是年利率 (小数). * `n` 是每年计算复利的次数. * `t` 是投资时间 (年). 请更新代码: 1. 从 [kotlin.math 包](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.math/) 导入必要的函数. 2. 向 `calculateCompoundInterest()` 函数添加函数体, 计算应用复利后的最终金额. ```KOTLIN // 请在这里编写你的代码 fun calculateCompoundInterest(P: Double, r: Double, n: Int, t: Int): Double { // 请在这里编写你的代码 } fun main() { val principal = 1000.0 val rate = 0.05 val timesCompounded = 4 val years = 5 val amount = calculateCompoundInterest(principal, rate, timesCompounded, years) println("The accumulated amount is: $amount") // 输出结果为: The accumulated amount is: 1282.0372317085844 } ``` ```KOTLIN import kotlin.math.* fun calculateCompoundInterest(P: Double, r: Double, n: Int, t: Int): Double { return P * (1 + r / n).pow(n * t) } fun main() { val principal = 1000.0 val rate = 0.05 val timesCompounded = 4 val years = 5 val amount = calculateCompoundInterest(principal, rate, timesCompounded, years) println("The accumulated amount is: $amount") // 输出结果为: The accumulated amount is: 1282.0372317085844 } ``` ### 习题 2 你想要测量在你的程序中执行多个数据处理任务消耗的时间. 请更新代码, 加入正确的导入语句, 以及 [kotlin.time](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.time/) 包中的正确的函数: ```KOTLIN // 请在这里编写你的代码 fun main() { val timeTaken = /* 请在这里编写你的代码 */ { // 模拟某些数据处理 val data = List(1000) { it * 2 } val filteredData = data.filter { it % 3 == 0 } // 模拟处理过滤后的数据 val processedData = filteredData.map { it / 2 } println("Processed data") } println("Time taken: $timeTaken") // 例如: 16 ms } ``` ```KOTLIN import kotlin.time.measureTime fun main() { val timeTaken = measureTime { // 模拟某些数据处理 val data = List(1000) { it * 2 } val filteredData = data.filter { it % 3 == 0 } // 模拟处理过滤后的数据 val processedData = filteredData.map { it / 2 } println("Processed data") } println("Time taken: $timeTaken") // 例如: 16 ms } ``` ### 习题 3 在最新的 Kotlin 发布版中, 标准库中有一个新的功能. 你想要试用这个功能, 但它要求使用者同意. 这个功能属于 [@ExperimentalStdlibApi](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-experimental-stdlib-api/). 那么你的代码中, 选择使用者同意的代码应该是什么样的? ```KOTLIN @OptIn(ExperimentalStdlibApi::class) ``` ## 下一步做什么? 恭喜你! 你已经完成了中级教程! 欢迎 [分享你的阅读体验](https://surveys.hotjar.com/bf4ce865-99ce-4fc1-b107-e9b16bc31592). 下一步, 请查看我们针对流行的 Kotlin 应用程序的教程: * [使用 Spring Boot 和 Kotlin 创建一个后端应用程序](jvm-create-project-with-spring-boot.html) * 从头创建一个针对 Android 和 iOS 的跨平台应用程序, 并且: * [共用业务逻辑, 但使用原生 UI](https://kotlinlang.org/docs/multiplatform/multiplatform-create-first-app.html) * [共用业务逻辑与 UI](https://kotlinlang.org/docs/multiplatform/compose-multiplatform-create-first-app.html) # Kotlin 2.4.0-RC2 版中的新功能 本章不翻译, 请阅读 [原文](https://kotlinlang.org/docs/whatsnew-eap.html) # What's new in Kotlin 2.4.0 The Kotlin 2.4.0 release is out! Here are the main highlights: * Language: [Stable context parameters, explicit backing fields, and multiple features for annotation use-site targets](#stable-features) * Standard library: [Stabilized support for the UUID API](#stable-uuid-api-in-the-common-kotlin-standard-library) and [support for checking sorted order](#support-for-checking-sorted-order) * Kotlin/JVM: [Support for Java 26](#support-for-java-26) and [annotations in metadata enabled by default](#annotations-in-metadata-enabled-by-default) * Kotlin/Native: [Support for Swift packages as dependencies, updates on Swift export, and the CMS GC enabled by default](#kotlin-native) * Kotlin/Wasm: [Incremental compilation enabled by default and support for WebAssembly Component Model](#kotlin-wasm) * Kotlin/JS: [Support for value class export and ES2015 features in JS code inlining](#kotlin-js) * Gradle: [Compatibility with Gradle 9.5.0](#gradle) * Maven: [Automatic alignment between Java and JVM target versions](#maven) * Kotlin compiler: [More consistent inline function behavior during .klib compilation](#consistent-intra-module-function-inlining-during-klib-compilation) Tip: For information about the Kotlin release cycle, see the [Kotlin release process](releases.html). ## Update to Kotlin 2.4.0 The latest version of Kotlin is included in the latest versions of [IntelliJ IDEA](https://www.jetbrains.com/idea/download/) and [Android Studio](https://developer.android.com/studio). To update to the new Kotlin version, make sure your IDE is updated to the latest version and [change the Kotlin version](releases.html#update-to-a-new-kotlin-version) to 2.4.0 in your build scripts. ## New features In previous Kotlin releases, several new features were introduced as Experimental. The following features have now graduated to [Stable](components-stability.html#stability-levels-explained) in Kotlin 2.4.0, so you no longer need to opt in to use them: * [Context parameters](context-parameters.html), except for [context arguments](#explicit-context-arguments-for-context-parameters) and [callable references](https://github.com/Kotlin/KEEP/blob/context-parameters/proposals/context-parameters.md#callable-references) * [@all meta-target for properties](annotations.html#all-meta-target) * [New defaulting rules for use-site annotation targets](annotations.html#defaults-when-no-use-site-targets-are-specified) * [Explicit backing fields](properties.html#explicit-backing-fields) * [Stable UUID API in the common Kotlin standard library](#stable-uuid-api-in-the-common-kotlin-standard-library) * [New API for converting unsigned integers to BigInteger on the JVM](#new-api-for-converting-unsigned-integers-to-biginteger-on-the-jvm) * [Support for checking sorted order](#support-for-checking-sorted-order) * [Support for value class export to JavaScript/TypeScript](#support-for-value-class-export-to-javascript-typescript) * [Support for ES2015 features when inlining JS code](#support-for-es2015-features-when-inlining-js-code) * [Maven: Automatic alignment between Java and JVM target versions](#automatic-alignment-between-java-and-jvm-target-versions) * [Support for Maven Toolchains](#support-for-maven-toolchains) Note: Support for using explicit backing fields in IntelliJ IDEA without the `-Xexplicit-backing-fields` compiler option will be available in 2026.1.4. ## New features * [Explicit context arguments for context parameters](#explicit-context-arguments-for-context-parameters) * [Support for collection literals](#support-for-collection-literals) * [Improved compile-time constants](#improved-compile-time-constants) * [Improved unused result checks for higher-order functions](#improved-unused-result-checks-for-higher-order-functions) * [New @IntroducedAt annotation to generate version-based overloads for optional parameters](#new-introducedat-annotation-to-generate-version-based-overloads-for-optional-parameters) * [New map fallback functions to distinguish null values and missing keys](#new-map-fallback-functions-to-distinguish-null-values-and-missing-keys) * [Swift package import](#swift-package-import) * [Swift export goes Alpha with improved concurrency support](#swift-export-goes-alpha-with-improved-concurrency-support) * [Support for the WebAssembly Component Model](#support-for-the-webassembly-component-model) ## Language Kotlin 2.4.0 promotes context parameters, explicit backing fields, and annotation use-site targets features to [Stable](components-stability.html#stability-levels-explained). This release also introduces [explicit context arguments for context parameters](#explicit-context-arguments-for-context-parameters). ### Stable features Kotlin 2.2.0 and 2.3.0 introduced a few language features as [Experimental](components-stability.html#stability-levels-explained). We're happy to announce that the following language features are now [Stable](components-stability.html#stability-levels-explained) in this release: * [Context parameters](whatsnew22.html#preview-of-context-parameters), except for [context arguments](#explicit-context-arguments-for-context-parameters) and [callable references](https://github.com/Kotlin/KEEP/blob/context-parameters/proposals/context-parameters.md#callable-references) * [@all meta-target for properties](annotations.html#all-meta-target) * [New defaulting rules for use-site annotation targets](annotations.html#defaults-when-no-use-site-targets-are-specified) * [Explicit backing fields](properties.html#explicit-backing-fields) [See the full list of Kotlin language design features and proposals](kotlin-language-features-and-proposals.html). ### No more deprecation warnings on the last segments of imports In previous Kotlin versions, when a deprecated class was imported, the deprecation error was reported at the call site as well as at the import directive itself. As there's no way to suppress deprecation errors on imports, you may have worked around this by suppressing deprecation reports for the entire file or by using star imports. Since reporting the deprecation on the import of a called symbol isn't useful in most cases, Kotlin 2.4.0 doesn't issue a warning when the deprecated symbol is referenced in the last segment of the import directive. For more information, see [KT-30155](https://youtrack.jetbrains.com/issue/KT-30155). ### Explicit context arguments for context parameters Kotlin 2.4.0 introduces explicit context arguments for [context parameters](context-parameters.html). Kotlin 2.3.20 [changed the overload resolution for context parameters](whatsnew2320.html#changes-to-overload-resolution-for-context-parameters). As a result, calls to overloads that differ only by context parameters can become ambiguous. You can now resolve this ambiguity by passing an explicit context argument at the call site. Here's an example: ```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() { // Selects the overload with the EmailSender context parameter sendNotification(emailSender = defaultEmailSender) // Selects the overload with the SmsSender context parameter sendNotification(smsSender = defaultSmsSender) } ``` You can also use explicit context arguments instead of the `context()` function to reduce nesting and make some calls easier to read. If you need to use the same context arguments in multiple calls, use the `context()` function instead. This feature is [Experimental](components-stability.html#stability-levels-explained). To opt in, add the following compiler option to your build file: Gradle: ```KOTLIN kotlin { compilerOptions { freeCompilerArgs.add("-Xexplicit-context-arguments") } } ``` Maven: ```XML org.jetbrains.kotlin kotlin-maven-plugin -Xexplicit-context-arguments ``` For more information, see the feature's [KEEP](https://github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0448-explicit-context-arguments.md). ### Support for collection literals Kotlin 2.4.0 introduces experimental support for collection literals. You can now create collections in a simpler and more concise way using brackets `[]`. For example: ```KOTLIN fun main() { // Mutable list with explicit type declaration // val shapes: MutableList = mutableListOf("triangle", "square", "circle") // Mutable list with brackets syntax val shapes: MutableList = ["triangle", "square", "circle"] println(shapes) // [triangle, square, circle] } ``` Note: Currently, collection literals can't be used to construct collections defined in Java. For more information, see [KT-80494](https://youtrack.jetbrains.com/issue/KT-80494). If the compiler doesn't have enough information to infer the collection type, it defaults to the `List` type: ```KOTLIN fun main() { val fruit = ["apple", "banana", "cherry"] println(fruit) // [apple, banana, cherry] } ``` You can also declare custom `operator fun of` functions to use bracket syntax with your own types. For example, if you have the following `DoubleMatrix` class: ```KOTLIN class DoubleMatrix(vararg val rows: Row) { companion object { operator fun of(vararg rows: Row) = DoubleMatrix(*rows) } class Row(vararg val elements: Double) { companion object { operator fun of(vararg elements: Double) = Row(*elements) } } } ``` You can create an `identityMatrix` class instance like this: ```KOTLIN fun main() { val identityMatrix: DoubleMatrix = [ [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], ] } ``` In this example, the compiler translates the nested collection literals into calls to the corresponding `operator fun of` functions. The compiler resolves these calls recursively and uses the expected types to choose the correct overloads. This feature is [Experimental](components-stability.html#stability-levels-explained). To opt in, add the following compiler option to your build file: Gradle: ```KOTLIN kotlin { compilerOptions { freeCompilerArgs.add("-Xcollection-literals") } } ``` Maven: ```XML org.jetbrains.kotlin kotlin-maven-plugin -Xcollection-literals ``` For more information, see the feature's [KEEP](https://github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0416-collection-literals.md). ### Improved compile-time constants Kotlin 2.4.0 brings experimental improvements to [compile-time constants](properties.html#compile-time-constants), making support for numeric and string types more consistent and easier to use. These improvements include support for: * Unsigned type operations. * Standard library functions for strings, like `.lowercase()`, `.uppercase()`, and `.trim()` functions. * Evaluation of the `.name` property of [enum constants](enum-classes.html) and the [KCallable interface](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.reflect/-k-callable/). To make it clear which functions are evaluated at compile time, Kotlin 2.4.0 introduces the `IntrinsicConstEvaluation` annotation. Some functions are evaluated at compile-time but don't have the annotation yet. Later releases will add the annotation to the remaining functions. For a list of supported functions, see the KEEP [appendix](https://github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0444-improve-compile-time-constants.md#appendix). This feature is [Experimental](components-stability.html#stability-levels-explained). To opt in, add the following compiler option to your build file: Gradle: ```KOTLIN kotlin { compilerOptions { freeCompilerArgs.add("-Xintrinsic-const-evaluation") } } ``` Maven: ```XML org.jetbrains.kotlin kotlin-maven-plugin -Xintrinsic-const-evaluation ``` For more information, see the feature's [KEEP](https://github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0444-improve-compile-time-constants.md). ### Improved unused result checks for higher-order functions Kotlin 2.4.0 introduces a new Experimental `returnsResultOf()` contract to improve the [unused return value checker](unused-return-value-checker.html). This contract enables the checker to distinguish between unused results that can be ignored and meaningful unused results from higher-order functions that return the result of a lambda, such as the `let` scope function. Warning: Kotlin contracts are [Experimental](components-stability.html#stability-levels-explained). To opt in, add the `@OptIn(ExperimentalContracts::class)` annotation when declaring a function with a contract. To use this feature, add `returnsResultOf()` to the function's contract: ```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) } ``` Here's an example that uses a custom `.customLet()` function with a nullable value: ```KOTLIN fun handleNullablePackageName(packageName: String?, builder: StringBuilder) { // The checker doesn't report a warning // because the return value of the append() function can be ignored packageName?.customLet { builder.append(it) } // The checker reports a warning because the returned string is unused packageName?.customLet { "kotlin.$it" } } ``` The unused return value checker is [Experimental](components-stability.html#stability-levels-explained) and must be enabled to report unused return values. For more information about enabling and configuring the checker, see [Unused return value checker](unused-return-value-checker.html#configure-the-unused-return-value-checker). #### How to enable The `returnsResultOf()` contract is [Experimental](components-stability.html#stability-levels-explained). Be aware that using it produces pre-release binaries that earlier Kotlin compiler versions can't read. To opt in, add the following compiler option to your build file: 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 ``` ### New `@IntroducedAt` annotation to generate version-based overloads for optional parameters Kotlin 2.4.0 introduces the `@IntroducedAt` annotation for preserving binary compatibility when adding new optional parameters to published APIs. Previously, adding optional parameters to a function often required using `@JvmOverloads`, which can generate more overloads than needed. Alternatively, preserving binary compatibility required you to keep older signatures as hidden deprecated overloads. With the `@IntroducedAt` annotation, you can annotate newly added optional parameters with the version in which they were introduced. The compiler uses this information to automatically generate the corresponding hidden overloads. This annotation is [Experimental](components-stability.html#stability-levels-explained). To opt in, use the `@OptIn(ExperimentalVersionOverloading::class)` annotation. Here's an example: ```KOTLIN @OptIn(ExperimentalVersionOverloading::class) fun Button( label: String = "", color: Color = DefaultColor, @IntroducedAt("1.1") borderColor: Color = DefaultBorderColor, @IntroducedAt("1.2") borderStyle: Style = DefaultBorderStyle, @IntroducedAt("1.2") borderWidth: Int = 1, onClick: () -> Unit ) { // Function body } ``` In this example, the compiler generates hidden overloads for the older versions of the `Button()` function. Since both `@IntroducedAt` and `@JvmOverloads` generate overloads, using them together can cause conflicting overloads. If you use both annotations, the compiler reports a warning. If you suppress the warning, the compiler prioritizes overloads generated from the `@IntroducedAt` annotation. ## Standard library Kotlin 2.4.0 stabilizes support for UUIDs in the common Kotlin standard library. It also adds new extension functions for converting unsigned integers to `BigInteger` on the JVM and support for checking sorted order. ### Stable UUID API in the common Kotlin standard library Kotlin 2.0.20 introduced a [class for generating UUIDs](whatsnew2020.html#support-for-uuids-in-the-common-kotlin-standard-library) (universally unique identifiers) and added support for converting between Kotlin and Java UUIDs. Later releases gradually improved this experimental feature by adding support for: * [Comparing UUIDs with < and > operators](whatsnew2120.html#changes-in-uuid-parsing-formatting-and-comparability) * [Parsing UUIDs from hex-and-dash and plain text formats](uuids.html#parse-uuids) * [Returning null when parsing invalid UUIDs](whatsnew23.html#support-for-returning-null-when-parsing-invalid-uuids). In Kotlin 2.4.0, [the kotlin.uuid.Uuid API](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.uuid/-uuid/) becomes [Stable](components-stability.html#stability-levels-explained). The only exceptions are [the functions for generating V4 and V7 UUIDs](whatsnew23.html#support-for-generating-v7-uuids-for-specific-timestamps), which remain [Experimental](components-stability.html#stability-levels-explained) and still require opt-in. For more information about how to work with UUIDs, see [UUIDs](uuids.html). ### Support for checking sorted order Kotlin 2.4.0 adds new extension functions for checking sorted order in iterables, arrays, and sequences. This includes the following extension functions: * `.isSorted()` * `.isSortedDescending()` * `.isSortedWith(comparator)` * `.isSortedBy(selector)` * `.isSortedByDescending(selector)` You can use these extension functions to check whether elements are already sorted, without sorting them again or creating your own helper functions. They return `true` if the elements are in the specified order, or if there are fewer than two elements, and `false` otherwise. These functions stop as soon as they encounter an out-of-order pair, which makes them efficient for large inputs. Here's an example of checking sorted order with `.isSorted()` and `.isSortedBy()` functions: ```KOTLIN data class User(val name: String, val age: Int) fun main() { val numbers = listOf(1, 2, 3, 4) println(numbers.isSorted()) // true val users = listOf( User("Alice", 24), User("Bob", 31), User("Charlie", 29), ) println(users.isSortedBy(User::age)) // false } ``` ### New API for converting unsigned integers to `BigInteger` on the JVM Kotlin 2.4.0 introduces the `UInt.toBigInteger()` and `ULong.toBigInteger()` extension functions on the JVM. Previously, converting `UInt` and `ULong` values to `BigInteger` required string-based workarounds or custom conversion logic. Starting with Kotlin 2.4.0, you can now use `.toBigInteger()` to convert unsigned integer values directly to `BigInteger`. Here's an example: ```KOTLIN fun main() { //sampleStart val unsignedLong = Long.MAX_VALUE.toULong() + 1uL val unsignedInt = UInt.MAX_VALUE println(unsignedLong.toBigInteger()) // 9223372036854775808 println(unsignedInt.toBigInteger()) // 4294967295 //sampleEnd } ``` ### New map fallback functions to distinguish `null` values and missing keys Kotlin 2.4.0 adds new variants of the existing [.getOrElse()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/get-or-else.html) and [.getOrPut()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/get-or-put.html) [map extension functions](map-operations.html) for maps with nullable values. These functions retrieve a value for a key or use a default value as a fallback. For maps with nullable values, the new variants let you choose whether a stored `null` value behaves like a missing key or an existing value, and they make that choice clear in their function names. The new extension functions include the following: * `.getOrElseIfNull(key, defaultValue)` and `.getOrPutIfNull(key, defaultValue)`, which return the default value if the key is missing or has a `null` value, similar to the existing `.getOrElse()` and `.getOrPut()` functions. * `.getOrElseIfMissing(key, defaultValue)` and `.getOrPutIfMissing(key, defaultValue)`, which return the default value only when the map doesn't contain the specified key. These APIs are [Experimental](components-stability.html#stability-levels-explained) and require opt-in with the `@OptIn(ExperimentalStdlibApi::class)` annotation. Here's an example that demonstrates the difference between `.getOrPutIfNull()` and `.getOrPutIfMissing()` when the key exists with a `null` value: ```KOTLIN @OptIn(ExperimentalStdlibApi::class) fun main() { val mapForNull = mutableMapOf("user" to null) val mapForMissing = mutableMapOf("user" to null) // Replaces the value if "user" has a null value mapForNull.getOrPutIfNull("user") { "default_user" } println(mapForNull) // {user=default_user} // Keeps the null value because "user" exists in the map mapForMissing.getOrPutIfMissing("user") { "default_user" } println(mapForMissing) // {user=null} } ``` You can also use the `.getOrElseIfMissing()` and `.getOrPutIfMissing()` functions for caches that store nullable values. If `defaultValue` returns `null`, the map stores it and doesn't call `defaultValue` again for the same key. Here's an example: ```KOTLIN data class Response(val body: String) class Service { var queryCount = 0 fun query(key: String): Response? { queryCount += 1 return null } } //sampleStart @OptIn(ExperimentalStdlibApi::class) fun main() { val service = Service() val cache = mutableMapOf() fun getCachedResponseOrQuery(key: String): Response? = cache.getOrPutIfMissing(key) { service.query(key) } // Stores null because the cache doesn't contain "user" getCachedResponseOrQuery("user") println(cache) // {user=null} // Uses the cached null and doesn't query the service again getCachedResponseOrQuery("user") println(service.queryCount) // 1 } //sampleEnd ``` We would appreciate your feedback in [YouTrack](https://youtrack.jetbrains.com/issue/KT-67337). ## Kotlin/JVM Kotlin 2.4.0 supports a new Java version and enables annotations in metadata by default. ### Support for Java 26 Starting with Kotlin 2.4.0, the compiler can generate classes containing Java 26 bytecode. ### Annotations in metadata enabled by default The Kotlin Metadata JVM library in Kotlin 2.2.0 [introduced support for reading annotations stored in Kotlin metadata](whatsnew22.html#support-for-reading-and-writing-annotations-in-kotlin-metadata). With this support, the Kotlin compiler writes annotations into metadata alongside the JVM bytecode, making them accessible to the Kotlin Metadata JVM library. As a result, annotation processors and other tools can understand and manipulate these annotations at the metadata level without using reflection or modifying source code. In Kotlin 2.4.0, this support is enabled by default. ## Kotlin/Native Starting with Kotlin 2.4.0, [Swift export is promoted to Alpha](#swift-export-goes-alpha-with-improved-concurrency-support). This release also brings support for [Swift package import](#swift-package-import), Xcode 26.4, improvements for memory consumption, and garbage collection. ### Default concurrent marking in garbage collector In Kotlin 2.0.20, the Kotlin team [introduced experimental support](whatsnew2020.html#concurrent-marking-in-garbage-collector) for the concurrent mark and sweep garbage collector (CMS GC). After processing user feedback and fixing regressions, we are now ready to enable CMS by default, starting with Kotlin 2.4.0. The previous default parallel mark concurrent sweep (PMCS) setup in the garbage collector had to pause application threads while the GC marked objects in the heap. In contrast, CMS allows the marking phase to run concurrently with application threads. This significantly improves GC pause duration and app responsiveness, which is important for the performance of latency-critical applications. CMS has already demonstrated its effectiveness in benchmarks for UI applications built with [Compose Multiplatform](https://blog.jetbrains.com/kotlin/2024/10/compose-multiplatform-1-7-0-released/#performance-improvements-on-ios). If you face problems, you can switch back to PMCS. To do that, set the following [binary option](native-binary-options.html) in your `gradle.properties` file: ``` kotlin.native.binary.gc=pmcs ``` For more information on the Kotlin/Native garbage collector, see our [documentation](native-memory-manager.html#garbage-collector). ### Reduced memory consumption during devirtualization analysis Previously, devirtualization analysis was one of the most memory-consuming phases in the Kotlin/Native compiler. Namely, the link release task consumed too much memory, especially in large projects. Kotlin 2.4.0 introduces improvements that help reduce peak memory consumption during link release tasks. According to benchmarks from one of our EAP users, the improved devirtualization analysis reduced memory consumption by link release tasks by half, saving at least 13 GB. ### Support for Xcode 26.4 Starting with Kotlin 2.4.0, the Kotlin/Native compiler supports Xcode 26.4 – one of the latest stable versions of Xcode. You can now update your Xcode and get access to the latest APIs to continue working on your Kotlin projects for Apple operating systems. ### LLVM update to version 21 In Kotlin 2.4.0, we updated LLVM from version 19 to 21. The new version includes performance improvements and helps keep the Kotlin/Native compiler up to date. This update shouldn't affect your code, but if you encounter any issues, please report them to our [issue tracker](http://kotl.in/issue). ### Changes to Apple target support Kotlin 2.4.0 raises the default minimum supported versions of Apple targets: * For iOS and tvOS, from 14.0 to 15.0. * For macOS, from 11.0 to 12.0. * For watchOS, from 7.0 to 8.0. If you need to support a lower version in your project than the default one, use the `freeCompilerArgs` option in your build file: ```KOTLIN kotlin { targets.withType().configureEach { binaries.configureEach { freeCompilerArgs += "-Xoverride-konan-properties=minVersion.ios=14.0" freeCompilerArgs += "-Xoverride-konan-properties=minVersion.macos=11.0" freeCompilerArgs += "-Xoverride-konan-properties=minVersion.tvos=14.0" freeCompilerArgs += "-Xoverride-konan-properties=minVersion.watchos=7.0" } } } ``` ### Swift export goes Alpha with improved concurrency support Starting with Kotlin 2.4.0, Kotlin's interoperability with Swift through Swift export is officially in Alpha! This release brings major improvements to concurrency support, adding native and direct structured concurrency to Swift export and the ability to export `kotlinx.coroutines` flows to Swift. #### Support for structured concurrency You can now seamlessly call suspending Kotlin code from Swift. Kotlin [suspend functions](composing-suspending-functions.html) and suspend functional types are exported as Swift's idiomatic `async` counterparts: ```KOTLIN // Kotlin suspend fun hello(): String { delay(1000) return "Hello Swift! This is Kotlin." } ``` ```SWIFT // Swift let msg = try await hello() ``` #### Export of flow types to Swift This update also adds support for exporting `kotlinx.coroutines` flows to Swift. Flows in `kotlinx.coroutines` represent an asynchronous stream of data that can be emitted and consumed concurrently. They are commonly used for reactive programming patterns, such as listening for database updates, network requests, or UI events. Previously, the only way to expose the `Flow` interface from [kotlinx.coroutines.flow](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/) to Swift was through third-party solutions. Now you can export flows out of the box into Swift's idiomatic counterpart: [AsyncSequence](https://developer.apple.com/documentation/Swift/AsyncSequence). The feature is enabled by default. You can export any public API with the `Flow` type to Swift while preserving type information. For example: ```KOTLIN // Kotlin // Type String is preserved when exporting Flow fun flowOfStrings(): Flow = flowOf("hello", "any", "world") ``` ```SWIFT // Swift var actual: [String] = [] // Type String is correctly inferred from Kotlin for try await element in flowOfStrings().asAsyncSequence() { actual.append(element) } ``` For more information about Swift export, see our [documentation](native-swift-export.html). ### Swift package import Kotlin Multiplatform projects now can declare [Swift packages](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/) as dependencies for an iOS app in their Gradle configuration: ```KOTLIN // build.gradle.kts kotlin { swiftPMDependencies { swiftPackage( url = url("https://github.com/firebase/firebase-ios-sdk.git"), version = from("12.11.0"), products = listOf( product("FirebaseAI"), product("FirebaseAnalytics"), ... } ``` For working samples and more detailed information, see [SwiftPM import](https://kotlinlang.org/docs/multiplatform/multiplatform-spm-import.html). If your project relies on CocoaPods dependencies, you can migrate the current setup to use Swift packages. The KMP tooling accounts for this use case and helps you reconfigure the project automatically. For details, see our [CocoaPods migration guide](https://kotlinlang.org/docs/multiplatform/multiplatform-cocoapods-spm-migration.html). ## Kotlin/Wasm Kotlin 2.4.0 enables incremental compilation for Kotlin/Wasm by default and introduces support for the WebAssembly Component Model. ### Incremental compilation enabled by default Kotlin/Wasm introduced incremental compilation in Kotlin 2.1.0. Starting with Kotlin 2.4.0, it is [Stable](components-stability.html#stability-levels-explained) and enabled by default. With this feature, the compiler rebuilds only the files affected by recent changes, which significantly reduces build time. To disable incremental compilation, add the following line to your project's `local.properties` or `gradle.properties` file: ``` # gradle.properties kotlin.incremental.wasm=false ``` If you run into any issues, report them in [YouTrack](https://kotl.in/issue) ### Improved display of internal variables in Chrome DevTools Kotlin 2.4.0 improves the debugging experience for Kotlin/Wasm in Chrome DevTools by making temporary, synthetic, and internal variables easier to distinguish from user-defined variables. The Kotlin compiler and compiler plugins, such as Compose, can generate these variables. They now use the `~` prefix by default, so they are grouped together and moved to the end of the variable list, which Chrome DevTools sorts by name. ### Support for the WebAssembly Component Model Kotlin/Wasm goes a step further in Kotlin 2.4.0 by introducing experimental support for the [WebAssembly Component Model](https://component-model.bytecodealliance.org/). The proposal defines a way to build components from Wasm modules through standardized interfaces and types. This approach helps Wasm evolve from a low-level binary instruction format into a system for composing reusable, language-agnostic components. It enables Kotlin/Wasm to go beyond the browser. For example, Kotlin and WebAssembly are well suited for Function-as-a-Service, also known as FaaS or serverless, applications. To try this feature, check out [a simple server built with wasi:http](https://github.com/Kotlin/sample-wasi-http-kotlin/). ![Kotlin/Wasm with WebAssembly Component Model](images/kotlin-wasm-wasi-http.gif) Share your feedback in [YouTrack](https://youtrack.jetbrains.com/issue/KT-64569/Kotlin-Wasm-Support-Component-Model). ## Kotlin/JS Kotlin 2.4.0 further improves export to JavaScript/TypeScript, including support for exporting value classes, interfaces, and type variance, as well as ES2015 features when inlining JS code. ### Support for value class export to JavaScript/TypeScript Previously, only regular Kotlin classes could be exported to JavaScript/TypeScript. Kotlin 2.4.0 lifts that limitation. You can now export Kotlin's [inline value classes](inline-classes.html) as regular TypeScript classes. To export a value class, mark it with the `@JsExport` annotation on the Kotlin side: ```KOTLIN // Kotlin @JsExport @JvmInline value class Email(val address: String) { init { require(address.contains("@")) { "Invalid email" } } } @JsExport class AuthService { suspend fun login(email: Email): String = ... } ``` From the TypeScript side, it looks like a regular class: ```TYPESCRIPT // TypeScript import { AuthService, Email } from "..." const auth = new AuthService(); console.log(await auth.login(new Email("jane@example.com"))); // "Welcome, jane@example.com!" console.log(await auth.login(new Email("not-an-email"))); // "Invalid email" ``` For more information, see [@JsExport annotation](js-to-kotlin-interop.html#jsexport-annotation). ### Support for ES2015 features when inlining JS code Starting with Kotlin 2.4.0, JavaScript code inlining has full support for [ES2015 features](js-project-setup.html#support-for-es2015-features). It's useful for interoperability with third-party libraries, as well as for direct control over automatic application code generation. Now you can use modern JS features inside [js()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.js/js.html) calls, including: * `const` and `let` variable declarations * ES classes * Generators * Lambdas ([arrow functions](whatsnew21.html#support-for-generating-es2015-arrow-functions)) * Spread and rest operators * Template strings Remember that the parameter of the `js()` function should be a string constant because it's parsed at compile time and translated to JavaScript code "as-is". For example, to inline the spread operator, use: ```KOTLIN fun spreadExample(): dynamic = js(""" const add = (a, b, c) => a + b + c; const nums = [1, 2, 3]; const sum = add(...nums); const a = [1, 2, 3]; const b = [...a, 4, 5, 6]; return { sum, b: b }; """) ``` For more information on inlining JavaScript code, see [our documentation](js-interop.html#inline-javascript). ### Preserve type variance when exporting to TypeScript Previously, Kotlin [variance](generics.html#variance) information in generic positions was lost when exporting types to TypeScript. With Kotlin 2.4.0, variance annotation is now saved during export and mapped to TypeScript's [variance annotations](https://www.typescriptlang.org/docs/handbook/2/generics.html#variance-annotations). In your Kotlin code, define the variance of your generic type parameters: ```KOTLIN // Kotlin // 'out' signals covariance (the interface only produces T) interface Producer { fun produce(): T } // 'in' signals contravariance (the interface only consumes T) interface Consumer { fun consume(item: T) } ``` With Kotlin 2.4.0, the `in` and `out` keywords are preserved in the generated TypeScript output: ```TYPESCRIPT // Generated .d.ts export interface Producer { produce(): T; } export interface Consumer { consume(item: T): void; } ``` ### Improved interface export to JavaScript/TypeScript Kotlin 2.4.0 makes it more convenient to export Kotlin interfaces to JavaScript/TypeScript. The new `@JsNoRuntime` annotation removes the previously required metadata for implementing Kotlin interfaces, allowing the direct mapping to regular TypeScript interfaces, similar to how external interfaces already behave by default. To export a Kotlin interface, for example in your Kotlin Multiplatform project, annotate it with `@JsNoRuntime` in the common code: ```KOTLIN // commonMain import kotlin.js.JsNoRuntime @JsNoRuntime expect interface DataProcessor { fun process(data: String): Int } ``` Then provide the actual implementation in your JS-specific source code: ```KOTLIN // jsMain @JsNoRuntime actual interface DataProcessor { actual fun process(data: String) } ``` Because the required metadata for implementing Kotlin interfaces is removed, the interface is mapped to a regular TypeScript interface: ```TYPESCRIPT // Generated .d.ts export interface DataProcessor { process(data: string): void; } ``` The `@JsNoRuntime` annotation is only allowed on standard interfaces, so that TypeScript can treat Kotlin interfaces as regular TypeScript interfaces. Therefore, the following operations are prohibited: * `is` and `as` type checks. * Class references with the [::class syntax](js-reflection.html). * Passing an interface as a reified type argument. Tip: Avoid annotating external interfaces with `@JsNoRuntime`, as this results in a compiler warning. ### Lifting restrictions on exporting interfaces Kotlin 2.4.0 makes another step toward the stabilization of `@JsExport`, improving how Kotlin interfaces are exported. Now you can export Kotlin interfaces with nested classes and named companion objects: ```KOTLIN @JsExport interface Identity { class Metadata(val tag: String) companion object Registry { val defaultTag = "GUEST" } } ``` For more information, see [@JsExport annotation](js-to-kotlin-interop.html#jsexport-annotation). ## Gradle Kotlin 2.4.0 is fully compatible with Gradle 7.6.3 through 9.5.0. You can also use Gradle versions up to the latest Gradle release. However, be aware that doing so may result in deprecation warnings, and some new Gradle features might not work. Kotlin 2.4.0 also brings improvements like consistent default module names across platforms and compiler messages written to the Problems API for the Kotlin/JVM. ### Minimum supported AGP version bumped to 8.5.2 Starting with Kotlin 2.4.0, the minimum supported Android Gradle plugin version is 8.5.2. ### Consistent module names across platforms Prior to Kotlin 2.4.0, default module names differed across platforms. This inconsistency could cause naming conflicts and resolution issues. Kotlin 2.4.0 standardizes the default names to `{group}:{project_name}` across all platforms. If you need to revert the JVM module name to its previous version, add the following to your `build.gradle.kts` file for a Kotlin/JVM project: ```KOTLIN kotlin { compilerOptions.moduleName(project.name) } ``` For a multiplatform project: ```KOTLIN kotlin { jvm { compilerOptions.moduleName(project.name) } } ``` ### Compiler messages written to Problems API for Kotlin/JVM In Kotlin 2.2.0, the Kotlin Gradle plugin (KGP) started reporting diagnostics to [Gradle's Problems API](https://docs.gradle.org/current/userguide/reporting_problems.html) to provide a consistent experience both in Gradle's CLI and in IntelliJ IDEA. In Kotlin 2.4.0, the plugin also writes compiler messages to the Problems API for Kotlin/JVM, bringing the API closer to becoming a single source for all logs and messages. ## Maven Kotlin 2.4.0 makes project configuration even easier with support for Maven Toolchains and automatic alignment between Java and JVM target versions. ### Automatic alignment between Java and JVM target versions To simplify project configuration and prevent compatibility issues, the Kotlin Maven plugin now automatically aligns the JVM target version with the Java compiler version configured in the project. This ensures that the Kotlin and Maven compilers target the same bytecode version, avoiding issues where Kotlin-generated bytecode is incompatible with the rest of the project or the intended deployment environment. With the `` option enabled, you don't need to set the `kotlin.compiler.jvmTarget` or `kotlin.compiler.jdkRelease` options. If neither of them is defined, the Kotlin Maven plugin automatically resolves the JVM target version in the following order: 1. As the `maven.compiler.release` version defined either as a project property or within the `maven-compiler-plugin` configuration. In this case, both `jvmTarget` and `jdkRelease` compiler options are set for the Kotlin compiler, limiting the API to a specific JDK version. 2. As the `maven.compiler.target` version in case the Maven release version is not set. The compiler target can be defined either as a project property or within the `maven-compiler-plugin` configuration. In this case, only Kotlin's `jvmTarget` is set, and the API is not limited to a specific JDK version. This greatly simplifies your Kotlin project configuration, so your `pom.xml` file can look like this: ```XML 17 2.4.0 org.jetbrains.kotlin kotlin-maven-plugin ${kotlin.version} true ``` During the build, the plugin outputs a similar message: ``` [INFO] Using jvmTarget=17 (derived from maven.compiler.release=17) ``` Note: The `` option only checks project-level properties and the global `maven-compiler-plugin` configuration. It doesn't check the configurations defined in the plugin's `` section. For more information about automatic project configuration, see [our documentation](maven-configure-project.html#jvm-target-version). ### Support for Maven Toolchains Kotlin 2.4.0 introduces support for [Maven Toolchains](https://maven.apache.org/guides/mini/guide-using-toolchains.html) to the Kotlin Maven plugin. The feature helps manage the JDK version in your build. With Maven Toolchains, you can specify the JDK version used for Kotlin compilation, independent of the JVM version running Maven (set in `JAVA_HOME`). When the `maven-toolchains-plugin` is configured in the build, the Kotlin Maven plugin automatically picks up the selected JDK toolchain, in the same way the Maven compiler plugin and other Maven plugins do. This allows you to configure a single toolchain to control the JDK used across all plugins in the build, including Kotlin compilation: ```XML org.apache.maven.plugins maven-toolchains-plugin 3.2.0 toolchain 21 ``` Keep in mind the priority of different ways to set up the JDK version: 1. `jdkHome` in the `kotlin-maven-plugin` configuration. An explicitly set `jdkHome` option always takes precedence over the toolchain version. 2. JDK version in `maven-toolchains-plugin`. The JDK version set through Maven Toolchains overrides the JDK version set in the `JAVA_HOME` path. 3. The `JAVA_HOME` path. You can also use a plugin-specific `` option to directly set the JDK version in the toolchain of `kotlin-maven-plugin`. Compared to using `maven-toolchains-plugin`, this parameter only affects Kotlin compilation and has no impact on other plugins in the build. Note: Currently, setting `maven-toolchains-plugin` to use a specific JDK version does not affect the `kapt` and `test-kapt` goals of `kotlin-maven-plugin`. To work around this, set the necessary version in the `JAVA_HOME` path. For more details, see [KT-79897](https://youtrack.jetbrains.com/issue/KT-79897). For more information on configuring Kotlin Maven projects, see our [documentation](maven-configure-project.html). ## Build tools API Kotlin 2.4.0 brings a number of improvements to the build tools API (BTA). The BTA: * Introduces new type-safe abstractions for most JVM and common compiler options. The BTA now handles their format instead of the client, reducing the risk of errors and providing an additional layer of assistance. This change is backwards-compatible at runtime, but it may break source compatibility. * Can now track non-source changes in incremental compilation, such as configuring a different Kotlin version or changing compiler options. Build systems can control this behavior through the `BaseIncrementalCompilationConfiguration.TRACK_CONFIGURATION_INPUTS` option. * Supports [binary compatibility validation](gradle-binary-compatibility-validation.html) through the `AbiValidationToolchain`, making it easier for other build systems to add this functionality. * Introduces a new feature so that build systems can customize how compiler messages are displayed through the [CompilerMessageRenderer](https://github.com/JetBrains/kotlin/blob/2.4.0/compiler/build-tools/kotlin-build-tools-api/src/main/kotlin/org/jetbrains/kotlin/buildtools/api/CompilerMessageRenderer.kt) interface and the [JvmCompilationOperation builder](https://github.com/JetBrains/kotlin/blob/2.4.0/compiler/build-tools/kotlin-build-tools-api/src/main/kotlin/org/jetbrains/kotlin/buildtools/api/jvm/operations/JvmCompilationOperation.kt#L59). * Introduces new options for configuring [Kotlin daemon](kotlin-daemon.html) logging: * `LOGS_PATH` — the directory for daemon log files. * `LOGS_FILE_SIZE_LIMIT` — the maximum log file size in bytes. * `LOGS_FILE_COUNT_LIMIT` — the maximum number of retained log files. By default, limits are set to a value specific to the Kotlin compiler version. To have no limit, build tools must set the option to `null`. Build systems can set the option when configuring the [execution policy](https://github.com/JetBrains/kotlin/blob/2.4.0/compiler/build-tools/kotlin-build-tools-api/src/main/kotlin/org/jetbrains/kotlin/buildtools/api/ExecutionPolicy.kt): ```KOTLIN val executionPolicy = kotlinToolchains.daemonExecutionPolicy { set(ExecutionPolicy.WithDaemon.LOGS_PATH, Paths("/var/log/kotlin-daemon")) set(ExecutionPolicy.WithDaemon.LOGS_FILE_SIZE_LIMIT, 10_485_760L) set(ExecutionPolicy.WithDaemon.LOGS_FILE_COUNT_LIMIT, 10) } ``` ## Kotlin compiler Kotlin 2.4.0 includes more consistent behavior for inline functions declared in the same module during `.klib` compilation. ### Consistent intra-module function inlining during klib compilation Previously, [function inlining](inline-functions.html) behaved inconsistently on different Kotlin platforms. The JetBrains team is working to unify it across all supported platforms to ensure the same compatibility guarantees. On the Kotlin/JVM, function inlining happens at compile time. So, when Kotlin sources are compiled with the Kotlin/JVM compiler, the resulting class files have no inline function calls in the bytecode because the bodies of inline functions are inlined into their call sites, so their behavior is fixed during compilation. On the contrary, on Kotlin/Native, Kotlin/JS, and Kotlin/Wasm, function inlining did not happen during source-to-klib compilation, only during binary generation. As a result, the behavior of inline functions wasn't fixed during `.klib` compilation, and `.klib` libraries didn't provide the same compatibility guarantees for inline functions as Kotlin/JVM does. Kotlin 2.4.0 takes the first step in unifying the behavior of inline functions by enabling intra-module inlining when generating `.klib` artifacts: ```KOTLIN // Existing logging.klib library inline fun logDebug(message: String) { println("[DEBUG] $message") } ``` ```KOTLIN // Currently compiled App module inline fun greetUser(name: String) { println("Hello, $name!") } fun main() { logDebug("App started") // Not inlined: declared in another module greetUser("Alice") // Inlined: declared in the same module } ``` When compiled to a `.klib`, the code looks something like: ```KOTLIN // Pseudocode fun main() { logDebug("App started") // Not inlined, declared in another module val tmp0 = "Alice" println("Hello, $tmp0!") // Inlined from greetUser() } ``` This means only inline functions declared in the same module are inlined during `.klib` compilation. Other functions, in this case, are inlined during the generation of platform-specific binaries. #### How to enable Starting with 2.4.0, the intra-module inlining is enabled by default for Kotlin/Native, Kotlin/JS, and Kotlin/Wasm. If you face unexpected problems with this feature, you can disable it using the following compiler option in the command line: ```BASH -Xklib-ir-inliner=disabled ``` The next step is to enable cross-module inlining to ensure all inline functions in the project are consistently inlined. This change is planned for future Kotlin releases, but you can already try it out using the following compiler option in the command line: ```BASH -Xklib-ir-inliner=full ``` Please share your feedback and report any problems in [YouTrack](https://kotl.in/issue). ### Consistent partial library linkage across Kotlin compilers In Kotlin 1.9.0, partial library linkage was enabled by default for both the Kotlin/Native and Kotlin/JS compilers, with Kotlin/Wasm following in Kotlin 2.0.0. This feature effectively makes compilers treat linkage issues in Kotlin libraries consistently with Kotlin/JVM. Since then, we haven't received negative feedback and haven't noticed users disabling the partial linkage in their projects. That's why starting with Kotlin 2.4.0, the partial linkage is always enabled, and the `-Xpartial-linkage` compiler option is now deprecated. The default log level for all Kotlin compilers is `SILENT`. Linkage issues are not reported during compilation. To change this behavior in your projects, set the `-Xpartial-linkage-loglevel` compiler option in your build file: ```KOTLIN // build.gradle.kts kotlin { macosX64("native") { binaries.executable() compilations.configureEach { compilerOptions.configure { // To report linkage issues with the “info” log level: freeCompilerArgs.add("-Xpartial-linkage-loglevel=INFO") // To report issues as errors: freeCompilerArgs.add("-Xpartial-linkage-loglevel=ERROR") } } } } ``` * `INFO` reports linkage issues with the "info" log level. * `WARNING` reports warnings at compile time and records them in compilation logs. * `ERROR` allows compilation to fail in case of linkage issues and reports errors in compilation logs. Use this option to examine the linkage issues more closely. If you encounter issues with this feature, please report them in [our issue tracker](https://kotl.in/issue). ## Kotlin compiler plugins In Kotlin 2.4.0, Kotlin's compiler plugins received notable updates, too. The kapt plugin can now exclude unnecessary annotation processors from the compile classpath, and the Power-assert plugin offers simplified configuration through the new runtime library. ### kapt: Exclude annotation processors from compile classpath Kotlin 2.4.0 adds support for the `includeCompileClasspath` configuration option for annotation processor discovery, similar to the Kotlin Gradle plugin. The new option allows you to exclude unnecessary annotation processors from the compile classpath. To configure this in your build file, set the `includeCompileClasspath` option to `false` in the `` section of the kapt plugin: ```XML kapt kapt false ... ... ``` Alternatively, you can do the same with the `kapt.include.compile.classpath` in the `` section: ```XML false ``` With the option set to `false`, annotation processors not included in the `` section of the kapt configuration are excluded from the kapt processing. If `includeCompileClasspath` is not set and kapt detects an annotation processor on the compile classpath that is not explicitly defined in the `` section, you'll see the following deprecation warning: ```TEXT [WARNING] Annotation processors discovery from compile classpath is deprecated. Set 'kapt.include.compile.classpath=false' to disable discovery. ``` For more information on kapt configuration, see our [documentation](kapt.html). ### Power-assert: New runtime library Kotlin 2.4.0 makes Power-assert capable functions more discoverable and easier to configure with the new runtime library. Previously, adopting Power-assert required complex build configurations and function parameter conventions. Starting with this release, Power-assert capable functions can use the new runtime library to integrate directly with the compiler plugin transformations. This brings major improvements for both plugin users and library authors: * The new `CallExplanation` data structure provides detailed information about the call site. This enables more dynamic diagram rendering for assertion failures and better integration with external tools. * The new `@PowerAssert` annotation makes assertion functions instantly discoverable by the compiler plugin. That way, you can now add out-of-the-box support for Power-assert into your libraries. Tip: Use our [example collection](https://github.com/bnorm/power-assert-examples#power-assert-examples) as a playground for experimenting with the new features. For more information, see our [documentation](power-assert.html#use-the-power-assert-plugin). ## Compose compiler With Kotlin 2.4.0, the Compose compiler offers more consistent incremental compilation and advances the deprecation cycle of several feature flags. ### Consistent incremental compilation for internal declarations Starting from Kotlin 2.4.0, the Compose compiler offers more consistent incremental compilation. Stability of internal types across different files is now inferred during runtime. This allows Compose to update inferred stability values even when class usages are not recompiled. As a side effect, the size of your artifacts may increase whenever a `@Composable` function uses an `internal` class from a different file as a parameter. This is caused by the compiler encoding the execution paths for both stable and unstable cases, since stability has to be decided during runtime. This overhead of runtime stability is removed by minifiers that perform full-app optimizations (such as R8) as they are able to infer the unnecessary execution path and eliminate it. This update does not change the final stability value, so the behavior of `@Composable` functions remains unchanged. ### Feature flag deprecations Kotlin 2.4.0 advances the deprecation cycle of experimental feature flags that graduated to stable and are now enabled by default: * `StrongSkipping`, `IntrinsicRemember`, and associated DSL properties are advanced to `DeprecationLevel.ERROR`. They will be removed in Kotlin 2.5.0. * `OptimizeNonSkippingGroups` and `PausableComposition` are now deprecated. They are scheduled to be removed in Kotlin 2.6.0. ## Breaking changes and deprecations This section highlights important breaking changes and deprecations. For a complete overview, see our [Compatibility guide](compatibility-guide-24.html). * Starting with Kotlin 2.4.0, the compiler no longer supports `-language-version=1.9`. As a result, the K1 compiler is no longer supported. * Kotlin 2.4.0 streamlines the DSL for binary compatibility validation in the Kotlin Gradle plugin and deprecates some parts. For the latest DSL, see [Binary compatibility validation in the Kotlin Gradle plugin](gradle-binary-compatibility-validation.html). * [Support for Kotlin script execution through the KotlinScriptMojo Maven plugin has been removed](compatibility-guide-22.html). ## Documentation updates We made the following documentation changes in the Kotlin ecosystem: * [Liquid Glass in a Compose Multiplatform app](https://kotlinlang.org/docs/multiplatform/ios-liquid-glass.html) – Migrate an iOS app from fully Compose-driven navigation to native SwiftUI navigation with iOS 26 Liquid Glass styling. * [Adding Swift packages as dependencies to KMP modules](https://kotlinlang.org/docs/multiplatform/multiplatform-spm-import.html) – Learn how to set up a SwiftPM dependency in your KMP project. * [Switch Kotlin Multiplatform project from CocoaPods to SwiftPM dependencies](https://kotlinlang.org/docs/multiplatform/multiplatform-cocoapods-spm-migration.html) manually or [with Junie](https://kotlinlang.org/docs/multiplatform/multiplatform-cocoapods-spm-migration-ai.html) – Learn how you can use Junie and Kotlin AI skills to make migration easier. * [Configure TeamCity for a KMP app](https://kotlinlang.org/docs/multiplatform/configure-teamcity-for-kmp.html) – Use TeamCity to build, test, and deploy your KMP applications. * [Recommended serialization approaches for Navigation 3](https://kotlinlang.org/docs/multiplatform/compose-navigation-3.html#recommended-serialization-approaches) – Find the best way to use serialization with Navigation 3 in your CMP application. * [Multiplatform ViewModel](https://kotlinlang.org/docs/multiplatform/compose-viewmodel.html) – Learn how to set up and work with ViewModels in a multiplatform project. * [Backend development with Kotlin](server-overview.html) – Explore the different frameworks you can use for backend development. * [Create a task manager app with Spring Boot and Claude](spring-boot-claude.html) – Learn how Claude can help you create an app with Spring Boot from scratch. * [Configure a Maven project](maven-configure-project.html) – Set up Kotlin compilation in your existing Java Maven project or in a new Kotlin Maven project. * [Test Kotlin projects with Maven](jvm-test-maven.html) – Learn how to create tests with JUnit and use Maven plugins to run unit and integration tests. * [Use annotation processors in Kotlin projects](jvm-annotation-processors.html) – Choose between kapt and KSP to process annotations in your backend project. * [Kotlin AI skills](kotlin-ai-skills.html) – Use agent skills to help you perform Kotlin-specific tasks. * [Kotlin Language Server](kotlin-lsp.html) – Read about JetBrains' official implementation of the Language Server Protocol (LSP) for Kotlin. * [Numbers](numbers.html) – Explore Kotlin's number types and how to work with them. * [Getting started with KSP](ksp-quickstart.html) – Learn how to add a KSP-based processor to your project or create your own. * [Migrate from kapt to KSP](ksp-kapt-migration.html) – Migrate your annotation processors to get the best out of Kotlin's features. * [Lincheck overview](lincheck-guide.html) – Understand how Lincheck works behind the scenes to test concurrent code on the JVM. * [Getting started with Lincheck](lincheck-getting-started.html) – Create a project and run tests with Lincheck. * [Testing arbitrary code with Lincheck](null) – Learn how to test concurrent code with Lincheck. * [How to test data structures with Lincheck](null) – Dive into Lincheck's data structure testing process. * [Testing strategies with Lincheck](null) – Learn about Lincheck's testing strategies: model checking and stress testing. * [Configuring a testing strategy with Lincheck](null) – Explore the different options for Lincheck's testing strategies. * [Deploy a Ktor application with Dokku](https://ktor.io/docs/dokku.html) – Learn about the deployment workflow with Dokku. # Kotlin 2.4.x 兼容性指南 本章不翻译, 请阅读 [原文](https://kotlinlang.org/docs/compatibility-guide-24.html) # 基本语法概述 本章会通过示例程序向你介绍 Kotlin 的一系列基本语法元素. 在各节的末尾, 你可以找到各个专题详细信息的页面链接. 你也可以通过 JetBrains Academy 的免费 [Kotlin 核心课程](https://hyperskill.org/tracks?category=4&utm_source=jbkotlin_hs&utm_medium=referral&utm_campaign=kotlinlang-docs&utm_content=button_1&utm_term=22.03.23) 学习 Kotlin 的全部基本知识. ## 包的定义与导入 包的定义应该在源代码文件的最上方: ```KOTLIN package my.demo import kotlin.text.* // ... ``` 源代码所在的目录结构不必与包结构保持一致: 源代码文件可以放置在文件系统的任意位置. 参见 [包](packages.html). ## 程序入口点(entry point) Kotlin 应用程序的入口点是 `main` 函数: ```KOTLIN fun main() { println("Hello world!") } ``` `main` 函数的另一种形式可以接受数量不定的 `String` 参数: ```KOTLIN fun main(args: Array) { println(args.contentToString()) } ``` ## 向标准输出(Standard Output)打印信息 `print` 函数会将传递给它的参数打印到标准输出(Standard Output): ```KOTLIN fun main() { //sampleStart print("Hello ") print("world!") //sampleEnd } ``` `println` 函数会打印它的参数, 并在末尾加上换行(Line Break), 因此之后的打印信息会出现在下一行: ```KOTLIN fun main() { //sampleStart println("Hello world!") println(42) //sampleEnd } ``` ## 从标准输入(Standard Input)读取信息 `readln()` 函数会从标准输入(Standard Input)读取信息. 这个函数将用户输入的整个行读取为字符串. 你可以使用 `println()`, `readln()`, 和 `print()` 函数, 打印消息, 要求用户输入, 并显示用户输入的内容: ```KOTLIN // 打印消息, 要求用户输入 println("Enter any word: ") // 读取并保存用户的输入. 例如: Happiness val yourWord = readln() // 将用户的输入和一个消息一起打印输出 print("You entered the word: ") print(yourWord) // 输出结果为 You entered the word: Happiness ``` 更多详情请参见 [读取标准输入](read-standard-input.html). ## 函数 以下函数接受两个 `Int` 类型参数, 并返回 `Int` 类型结果: ```KOTLIN //sampleStart fun sum(a: Int, b: Int): Int { return a + b } //sampleEnd fun main() { print("sum of 3 and 5 is ") println(sum(3, 5)) } ``` 以下函数使用表达式语句作为函数体, 返回类型由自动推断决定: ```KOTLIN //sampleStart fun sum(a: Int, b: Int) = a + b //sampleEnd fun main() { println("sum of 19 and 23 is ${sum(19, 23)}") } ``` 以下函数不返回有意义的结果: ```KOTLIN //sampleStart fun printSum(a: Int, b: Int): Unit { println("sum of $a and $b is ${a + b}") } //sampleEnd fun main() { printSum(-1, 8) } ``` 返回值为 `Unit` 类型时, 可以省略: ```KOTLIN //sampleStart fun printSum(a: Int, b: Int) { println("sum of $a and $b is ${a + b}") } //sampleEnd fun main() { printSum(-1, 8) } ``` 参见 [函数](functions.html). ## 变量 在 Kotlin 中, 声明变量时以关键字, `val` 或 `var` 开始, 之后是变量名称. 使用 `val` 关键字声明的变量只能赋值一次. 这是不可变的, 只读的局部变量, 在初始化之后就不能再次给它设定不同的值: ```KOTLIN fun main() { //sampleStart // 声明变量 x, 并初始化赋值为 5 val x: Int = 5 // 输出结果为 5 //sampleEnd println(x) } ``` 使用关键字 `var` 声明的变量可以多次赋值. 这是可变的变量, 在初始化之后也可以改变它的值: ```KOTLIN fun main() { //sampleStart // 声明变量 x, 并初始化赋值为 5 var x: Int = 5 // 将新的值 6 赋值给变量 x x += 1 // 输出结果为 6 //sampleEnd println(x) } ``` Kotlin 支持类型推断, 能够自动识别声明的变量的数据类型. 在声明一个变量时, 你可以省略变量名之后的类型: ```KOTLIN fun main() { //sampleStart // 声明变量 x, 赋值为 5; 变量类型自动推断为 `Int` val x = 5 // 5 //sampleEnd println(x) } ``` 变量只有在初始化之后才能使用. 可以在变量声明时初始化, 也可以先声明变量, 以后再初始化. 后一种情况下, 必须指明数据类型: ```KOTLIN fun main() { //sampleStart // 在声明变量 x 时初始化; 不需要指定类型 val x = 5 // 声明变量 c, 不初始化; 需要指定类型 val c: Int // 在声明变量 c 之后再初始化 c = 3 // 5 // 3 //sampleEnd println(x) println(c) } ``` 也可以将变量声明在顶级(top level): ```KOTLIN //sampleStart val PI = 3.14 var x = 0 fun incrementX() { x += 1 } // x = 0; PI = 3.14 // incrementX() // x = 1; PI = 3.14 //sampleEnd fun main() { println("x = $x; PI = $PI") incrementX() println("incrementX()") println("x = $x; PI = $PI") } ``` 关于属性的声明, 更多详情请参见 [属性(Property)](properties.html). ## 创建类与实例 要定义类, 请使用 `class` 关键字: ```KOTLIN class Shape ``` 类的属性(Property)可以在类声明部分或类主体部分中列出: ```KOTLIN class Rectangle(val height: Double, val length: Double) { val perimeter = (height + length) * 2 } ``` 会自动生成一个默认构造器, 参数是在类声明部分中定义的那些属性: ```KOTLIN class Rectangle(val height: Double, val length: Double) { val perimeter = (height + length) * 2 } fun main() { val rectangle = Rectangle(5.0, 2.0) println("The perimeter is ${rectangle.perimeter}") } ``` 类之间的继承关系使用冒号(`:`)表示. 类默认为 `final`; 要允许一个类被后代继承, 请将它标记为 `open`: ```KOTLIN open class Shape class Rectangle(val height: Double, val length: Double): Shape() { val perimeter = (height + length) * 2 } ``` 关于构造器与继承, 更多详情请参见 [类](classes.html) 和 [对象与实例](object-declarations.html). ## 注释 与大多数现代编程语言一样, Kotlin 支持单行(或者叫做 行尾)注释, 也支持多行 (或者叫做 块) 注释: ```KOTLIN // 这是一条行尾注释 /* 这是一条块注释 可以包含多行内容. */ ``` Kotlin 的块注释允许嵌套: ```KOTLIN /* 注释从这里开始 /* 包含一个嵌套的注释 */ 到这里结束. */ ``` 关于文档注释的语法, 详情请参见 [Kotlin 代码中的文档](kotlin-doc.html). ## 字符串模板 ```KOTLIN fun main() { //sampleStart var a = 1 // 在字符串模板内使用简单的变量名称 val s1 = "a is $a" a = 2 // 在字符串模板内使用任意的表达式: val s2 = "${s1.replace("is", "was")}, but now is $a" //sampleEnd println(s2) } ``` 详情请参见 [字符串模板](strings.html#string-templates). ## 条件表达式 ```KOTLIN //sampleStart fun maxOf(a: Int, b: Int): Int { if (a > b) { return a } else { return b } } //sampleEnd fun main() { println("max of 0 and 42 is ${maxOf(0, 42)}") } ``` 在 Kotlin 中, `if` 也可以用作表达式: ```KOTLIN //sampleStart fun maxOf(a: Int, b: Int) = if (a > b) a else b //sampleEnd fun main() { println("max of 0 and 42 is ${maxOf(0, 42)}") } ``` 参见 [if 表达式](control-flow.html#if-expression). ## for 循环 ```KOTLIN fun main() { //sampleStart val items = listOf("apple", "banana", "kiwifruit") for (item in items) { println(item) } //sampleEnd } ``` 或者: ```KOTLIN fun main() { //sampleStart val items = listOf("apple", "banana", "kiwifruit") for (index in items.indices) { println("item at $index is ${items[index]}") } //sampleEnd } ``` 参见 [for 循环](control-flow.html#for-loops). ## while 循环 ```KOTLIN fun main() { //sampleStart val items = listOf("apple", "banana", "kiwifruit") var index = 0 while (index < items.size) { println("item at $index is ${items[index]}") index++ } //sampleEnd } ``` 参见 [while 循环](control-flow.html#while-loops). ## when 表达式 ```KOTLIN //sampleStart fun describe(obj: Any): String = when (obj) { 1 -> "One" "Hello" -> "Greeting" is Long -> "Long" !is String -> "Not a string" else -> "Unknown" } //sampleEnd fun main() { println(describe(1)) println(describe("Hello")) println(describe(1000L)) println(describe(2)) println(describe("other")) } ``` 参见 [when 表达式和语句](control-flow.html#when-expressions-and-statements). ## 值范围(Range) 使用 `in` 操作符检查一个数值是否在某个值范围(Range)之内: ```KOTLIN fun main() { //sampleStart val x = 10 val y = 9 if (x in 1..y+1) { println("fits in range") } //sampleEnd } ``` 检查一个数值是否在某个值范围之外: ```KOTLIN fun main() { //sampleStart val list = listOf("a", "b", "c") if (-1 !in 0..list.lastIndex) { println("-1 is out of range") } if (list.size !in list.indices) { println("list size is out of valid list indices range, too") } //sampleEnd } ``` 在一个值范围内进行遍历迭代: ```KOTLIN fun main() { //sampleStart for (x in 1..5) { print(x) } //sampleEnd } ``` 或者, 在一个数列(progression)上进行遍历迭代: ```KOTLIN fun main() { //sampleStart for (x in 1..10 step 2) { print(x) } println() for (x in 9 downTo 0 step 3) { print(x) } //sampleEnd } ``` 参见 [值范围(Range)与数列(Progression)](ranges.html). ## 集合(Collection) 在一个集合上进行遍历迭代: ```KOTLIN fun main() { val items = listOf("apple", "banana", "kiwifruit") //sampleStart for (item in items) { println(item) } //sampleEnd } ``` 使用 `in` 运算符检查一个集合是否包含某个对象: ```KOTLIN fun main() { val items = setOf("apple", "banana", "kiwifruit") //sampleStart when { "orange" in items -> println("juicy") "apple" in items -> println("apple is fine too") } //sampleEnd } ``` 使用 [Lambda 表达式](lambdas.html) 对集合元素进行过滤和变换: ```KOTLIN fun main() { //sampleStart val fruits = listOf("banana", "avocado", "apple", "kiwifruit") fruits .filter { it.startsWith("a") } .sortedBy { it } .map { it.uppercase() } .forEach { println(it) } //sampleEnd } ``` 参见 [集合(Collection)概述](collections-overview.html). ## 可为 null 的值与 null 值检查 当一个引用可能为 `null` 值时, 对应的类型声明必须明确地标记为可为 null. 类型名称末尾带 `?` 符号表示可为 null 值. 例如, `Int?`. 当 `str` 中的字符串内容不是一个整数时, 返回 `null`: ```KOTLIN fun parseInt(str: String): Int? { return str.toIntOrNull() } ``` 以下示例演示如何使用一个返回值可为 null 的函数: ```KOTLIN fun parseInt(str: String): Int? { return str.toIntOrNull() } //sampleStart fun printProduct(arg1: String, arg2: String) { val x = parseInt(arg1) val y = parseInt(arg2) // 直接使用 `x * y` 会导致错误, 因为它们可能为 null. if (x != null && y != null) { // 在进行过 null 值检查之后, x 和 y 的类型会被自动转换为非 null 变量 println(x * y) } else { println("'$arg1' or '$arg2' is not a number") } } //sampleEnd fun main() { printProduct("6", "7") printProduct("a", "7") printProduct("a", "b") } ``` 或者: ```KOTLIN fun parseInt(str: String): Int? { return str.toIntOrNull() } fun printProduct(arg1: String, arg2: String) { val x = parseInt(arg1) val y = parseInt(arg2) //sampleStart // ... if (x == null) { println("Wrong number format in arg1: '$arg1'") return } if (y == null) { println("Wrong number format in arg2: '$arg2'") return } // 在进行过 null 值检查之后, x 和 y 的类型会被自动转换为非 null 变量 println(x * y) //sampleEnd } fun main() { printProduct("6", "7") printProduct("a", "7") printProduct("99", "b") } ``` 参见 [Null 值安全](null-safety.html). ## 类型检查与自动类型转换 `is` 运算符可以检查一个表达式的值是不是某个类型的实例. 如果对一个不可变的局部变量或属性进行过类型检查, 那么之后的代码就不必再对它进行显式地类型转换, 而可以直接将它当作需要的类型来使用: ```KOTLIN //sampleStart fun getStringLength(obj: Any): Int? { if (obj is String) { // 在这个分支中, `obj` 的类型会被自动转换为 `String` return obj.length } // 在类型检查所影响的分支之外, `obj` 的类型仍然是 `Any` return null } //sampleEnd fun main() { fun printLength(obj: Any) { println("Getting the length of '$obj'. Result: ${getStringLength(obj) ?: "Error: The object is not a string"} ") } printLength("Incomprehensibilities") printLength(1000) printLength(listOf(Any())) } ``` 或者: ```KOTLIN //sampleStart fun getStringLength(obj: Any): Int? { if (obj !is String) return null // 在这个分支中, `obj` 的类型会被自动转换为 `String` return obj.length } //sampleEnd fun main() { fun printLength(obj: Any) { println("Getting the length of '$obj'. Result: ${getStringLength(obj) ?: "Error: The object is not a string"} ") } printLength("Incomprehensibilities") printLength(1000) printLength(listOf(Any())) } ``` 甚至还可以: ```KOTLIN //sampleStart fun getStringLength(obj: Any): Int? { // 在 `&&` 运算符的右侧, `obj` 的类型会被自动转换为 `String` if (obj is String && obj.length >= 0) { return obj.length } return null } //sampleEnd fun main() { fun printLength(obj: Any) { println("Getting the length of '$obj'. Result: ${getStringLength(obj) ?: "Error: The object is not a string"} ") } printLength("Incomprehensibilities") printLength("") printLength(1000) } ``` 参见 [类](classes.html) 和 [类型转换](typecasts.html). # 关键字与操作符 ## 硬关键字(Hard Keyword) 以下符号始终会被解释为关键字, 不能用作标识符(identifiers): * `as` * 用于 [类型转换](typecasts.html#unsafe-cast-operator). * [为 import 指定一个别名](packages.html#imports). * `as?` 用于 [安全的类型转换](typecasts.html#unsafe-cast-operator). * `break` [结束一个循环](returns.html). * `class` 声明一个 [类](classes.html). * `continue` [跳转到最内层循环的下一次执行](returns.html). * `do` 开始一个 [do/while 循环](control-flow.html#while-loops) (条件判定在后的循环). * `else` 定义 [if 表达式](control-flow.html#if-expression) 的一个分支, 这个分支在条件为 false 时执行. * `false` 指定 [布尔类型](booleans.html) 的 'false' 值. * `for` 开始一个 [for 循环](control-flow.html#for-loops). * `fun` 声明一个 [函数](functions.html). * `if` 开始一个 [if 表达式](control-flow.html#if-expression). * `in` * 指定 [for 循环](control-flow.html#for-loops) 的迭代对象. * 用作中缀操作符, 判断一个值是否在 [一个值范围](ranges.html) 之内, 或者是否属于一个集合, 或者是否属于其他 [定义了 'contains' 方法](operator-overloading.html#in-operator) 的实体. * 在 [when 表达式](control-flow.html#when-expressions-and-statements) 中做同样的判断. * 将一个类型参数标记为 [反向类型变异](generics.html#declaration-site-variance). * `!in` * 用作操作符, 判断一个值是否 不属于 [一个值范围](ranges.html), 或者是否 不属于 一个集合, 或者是否 不属于 其他 [定义了 'contains' 方法](operator-overloading.html#in-operator) 的实体. * 在 [when 表达式](control-flow.html#when-expressions-and-statements) 中做同样的判断. * `interface` 声明一个 [接口](interfaces.html). * `is` * 判断 [一个值是不是某个类型](typecasts.html#is-and-is-operators). * 在 [when 表达式](control-flow.html#when-expressions-and-statements) 中做同样的判断. * `!is` * 判断 [一个值是否不是某个类型](typecasts.html#is-and-is-operators). * 在 [when 表达式](control-flow.html#when-expressions-and-statements) 中做同样的判断. * `null` 是一个常数, 表示一个不指向任何对象的引用. * `object` [同时声明一个类和它的对象实例](object-declarations.html). * `package` 指定 [当前源代码文件的包](packages.html). * `return` [从最内层的函数或匿名函数中返回](returns.html). * `super` * [引用一个方法或属性在超类中的实现](inheritance.html#calling-the-superclass-implementation). * [在次级构造器中调用超类构造器](classes.html#inheritance). * `this` * 引用 [当前接受者](this-expressions.html). * [在次级构造器中调用同一个类的另一个构造器](classes.html#constructors-and-initializer-blocks). * `throw` [抛出一个异常](exceptions.html). * `true` 指定 [布尔类型](booleans.html) 的 'true' 值. * `try` [开始一个异常处理代码段](exceptions.html). * `typealias` 声明一个 [类型别名](type-aliases.html). * `typeof` 保留, 将来使用. * `val` 声明一个只读的 [属性](properties.html), 或者一个只读的 [局部变量](basic-syntax.html#variables). * `var` 声明一个可变的 [属性](properties.html), 或者一个可变的 [局部变量](basic-syntax.html#variables). * `when` 开始一个 [when 表达式](control-flow.html#when-expressions-and-statements) (执行其中一个分支). * `while` 开始一个 [while 循环](control-flow.html#while-loops) (条件判定在前的循环). ## 软关键字(Soft Keyword) 以下符号在适当的场合下可以是关键字, 在其他场合可以用作标识符: * `by` * [将一个接口的实现委托给另一个对象](delegation.html). * [将一个属性的访问器函数实现委托给另一个对象](delegated-properties.html). * `catch` 开始一个 [处理特定的异常类型](exceptions.html) 的代码段. * `constructor` 声明一个 [主构造器, 或次级构造器](classes.html#constructors-and-initializer-blocks). * `delegate` 用作一种 [注解的使用目标(target)](annotations.html#annotation-use-site-targets). * `dynamic` 在 Kotlin/JS 代码中引用一个 [动态类型](dynamic-type.html). * `field` * 声明一个 [明确的后端域变量(Backing Field)](properties.html#explicit-backing-fields). * 用作一种 [注解的使用目标(target)](annotations.html#annotation-use-site-targets). * `file` 用作一种 [注解的使用目标(target)](annotations.html#annotation-use-site-targets). * `finally` 开始一个 [try 代码段结束时始终会被执行](exceptions.html) 的代码段. * `get` * 声明 [属性的取值方法](properties.html). * 用作一种 [注解的使用目标(target)](annotations.html#annotation-use-site-targets). * `import` [从另一个包中将一个声明导入到当前源代码文件](packages.html). * `init` 开始一个 [初始化代码段](classes.html#constructors-and-initializer-blocks). * `param` 用作一种 [注解的使用目标(target)](annotations.html#annotation-use-site-targets). * `property` 用作一种 [注解的使用目标(target)](annotations.html#annotation-use-site-targets). * `receiver` 用作一种 [注解的使用目标(target)](annotations.html#annotation-use-site-targets). * `set` * 声明 [属性的设值方法](properties.html). * 用作一种 [注解的使用目标(target)](annotations.html#annotation-use-site-targets). * `setparam` 用作一种 [注解的使用目标(target)](annotations.html#annotation-use-site-targets). * `value` 与 `class` 关键字一起使用, 声明一个 [内联类(inline class)](inline-classes.html). * `where` 指定 [泛型类型参数的约束](generics.html#upper-bounds). ## 修饰符关键字(Modifier Keyword) 以下符号在声明的修饰符列表中用做关键字, 在其他场合可以用作标识符: * `abstract` 将一个类或一个成员标注为 [抽象元素](classes.html#abstract-classes). * `actual` 在 [跨平台项目](multiplatform-expect-actual.html) 中, 表示某个特定平台上的具体实现. * `annotation` 声明一个 [注解类](annotations.html). * `companion` 声明一个 [同伴对象](object-declarations.html#companion-objects). * `const` 将一个属性标注为 [编译期常数值](properties.html#compile-time-constants). * `crossinline` 禁止 [传递给内联函数的 lambda 表达式中的非局部的返回](inline-functions.html#returns). * `data` 指示编译器, [为类生成常用的成员函数](data-classes.html). * `enum` 声明一个 [枚举类](enum-classes.html). * `expect` 标注一个 [与平台相关的声明](multiplatform-expect-actual.html), 在各个平台模块中, 需要存在对应的具体实现. * `external` 标注一个声明在 Kotlin 代码之外 (可以通过 [JNI](java-interop.html#using-jni-with-kotlin) 实现, 或者用 [JavaScript](js-interop.html#external-modifier) 实现). * `final` 禁止 [覆盖成员](inheritance.html#overriding-methods). * `infix` 允许使用 [中缀标记法](functions.html#infix-notation) 来调用函数. * `inline` 告诉编译器 [将函数以及传递给函数的 lambda 表达式内联到函数的调用处](inline-functions.html). * `inner` 允许在 [嵌套内](nested-classes.html) 中引用外部类的实例. * `internal` 将一个声明标注为 [只在当前模块中可以访问](visibility-modifiers.html). * `lateinit` 允许 [在构造器之外初始化非 null 的属性](properties.html#late-initialized-properties-and-variables). * `noinline` 关闭 [对传递给内联函数的 lambda 表达式的内联](inline-functions.html#noinline). * `open` 允许 [继承类, 或者覆盖成员](classes.html#inheritance). * `operator` 将函数标记为 [操作符重载, 或实现一个规约](operator-overloading.html). * `out` 将类型参数标记为 [协变的](generics.html#declaration-site-variance). * `override` 将成员标记为 [对超类成员的覆盖](inheritance.html#overriding-methods). * `private` 将声明标记为 [只在当前类中, 或当前源代码文件中可以访问](visibility-modifiers.html). * `protected` 将声明标记为 [只在当前类, 以及它的子类中可以访问](visibility-modifiers.html). * `public` 将声明标记为 [在任何位置都可以访问](visibility-modifiers.html). * `reified` 将内联函数的类型参数标记为 [在运行时刻可以访问](inline-functions.html#reified-type-parameters). * `sealed` 声明一个 [封闭类](sealed-classes.html) (子类受到限制的类). * `suspend` 将函数, 或 lambda 表达式, 标注为挂起函数, 或挂起lambda 表达式 (可在 [协程](coroutines-overview.html) 中使用). * `tailrec` 将一个函数标注为 [尾递归](functions.html#tail-recursive-functions) (允许编译器用迭代来代替递归). * `vararg` 允许 [对某个参数传递可变数量的参数值](functions.html#variable-number-of-arguments-varargs). ## 特殊标识符 以下表述符在特定情况下由编译器定义, 在其他场合可以用作通常的标识符: * `field` 在属性访问函数的内部, 用来引用 [属性的后端域变量](properties.html#backing-fields). * `it` 在 lambda 表达式内部, 用来 [引用 lambda 表达式的隐含参数](lambdas.html#it-implicit-name-of-a-single-parameter). ## 操作符与特殊符号 Kotlin 支持以下操作符与特殊符号: * `+`, `-`, `*`, `/`, `%` - 算数运算符 * `*` 也被用来 [向一个不定数量参数传递数组](functions.html#variable-number-of-arguments-varargs). * `=` * 赋值操作符. * 用来指定 [参数的默认值](functions.html#parameters-with-default-values). * `+=`, `-=`, `*=`, `/=`, `%=` - [计算并赋值](operator-overloading.html#augmented-assignments). * `++`, `--` - [递增与递减操作符](operator-overloading.html#increments-and-decrements). * `&&`, `||`, `!` - '与', '或', '非' 逻辑运算符 (用于位运算, 请使用对应的 [中缀函数](numbers.html#bitwise-operations)). * `==`, `!=` - [相等和不等比较操作符](operator-overloading.html#equality-and-inequality-operators) (对非基本类型, 会翻译为对 `equals()` 函数的调用). * `===`, `!==` - [引用相等比较操作符](equality.html#referential-equality). * `<`, `>`, `<=`, `>=` - [比较操作符](operator-overloading.html#comparison-operators) (对非基本类型, 会翻译为对 `compareTo()` 函数的调用). * `[`, `]` - [下标访问操作符](operator-overloading.html#indexed-access-operator) (会翻译为对 `get` 和 `set` 函数的调用). * `!!` [断言一个表达式的值不为 null](null-safety.html#not-null-assertion-operator). * `?.` 执行一个 [安全调用](null-safety.html#safe-call-operator) (如果接受者不为 null, 则调用一个方法, 或调用一个属性的访问函数). * `?:` 如果这个运算符左侧的表达式值为 null, 则返回右侧的表达式值(也就是 [elvis 操作符](null-safety.html#elvis-operator)). * `::` 创建一个 [成员的引用](reflection.html#function-references), 或者一个 [类引用](reflection.html#class-references). * `..`, `..<` 创建 [值范围](ranges.html). * `:` 在声明中, 用作名称与类型之间的分隔符. * `?` 将一个类型标记为 [可为 null](null-safety.html#nullable-types-and-non-nullable-types). * `->` * 在 [lambda 表达式](lambdas.html#lambda-expression-syntax) 中, 用作参数与函数体之间的分隔符. * 在 [函数类型](lambdas.html#function-types) 中, 用作参数与返回类型之间的分隔符. * 在 [when 表达式](control-flow.html#when-expressions-and-statements) 的分支中, 用作分支条件与分支体之间的分隔符. * `@` * 引入一个 [注解](annotations.html#usage). * 定义, 或者引用一个 [循环标签](returns.html#break-and-continue-labels). * 定义, 或者引用一个 [lambda 表达式标签](returns.html#return-to-labels). * 引用一个 [外层范围的 'this' 表达式](this-expressions.html#qualified-this). * 引用一个 [外部类的超类](inheritance.html#calling-the-superclass-implementation). * `;` 用于在同一行中分隔多条语句. * `$` 在 [字符串模板](strings.html#string-templates) 中引用变量或表达式. * `_` * 在 [lambda 表达式](lambdas.html#underscore-for-unused-variables) 中代替未使用的参数. * 在 [解构声明](destructuring-declarations.html#underscore-for-unused-variables) 中代替未使用的参数. 关于操作符优先顺序, 请参见 Kotlin 语法中的 [这一章节](https://kotlinlang.org/grammar/#expressions) . # 包(Package)与导入(Import) 源代码文件的开始部分可以是包声明: ```KOTLIN package org.example fun printMessage() { /*...*/ } class Message { /*...*/ } // ... ``` 源代码内的所有内容, 比如类, 函数, 全部都包含在所声明的包之内. 因此, 上面的示例代码中, `printMessage()` 函数的完整名称将是 `org.example.printMessage`, `Message` 类的完整名称将是 `org.example.Message`. 如果没有指定包, 那么源代码文件中的内容将属于 默认 包, 这个包没有名称. ## 默认导入 以下各个包会被默认导入到每一个 Kotlin 源代码文件: * [kotlin.*](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/index.html) * [kotlin.annotation.*](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.annotation/index.html) * [kotlin.collections.*](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/index.html) * [kotlin.comparisons.*](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.comparisons/index.html) * [kotlin.io.*](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/index.html) * [kotlin.ranges.*](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.ranges/index.html) * [kotlin.sequences.*](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/index.html) * [kotlin.text.*](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/index.html) 根据编译的目标平台不同, 还会导入以下包: * JVM 平台: * java.lang.* * [kotlin.jvm.*](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/index.html) * JavaScript 平台: * [kotlin.js.*](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.js/index.html) ## 导入(Import) 除默认导入(Import)的内容之外, 各源代码可以包含自己独自的 `import` 指令. 我们可以导入一个单独的名称: ```KOTLIN import org.example.Message // 导入后 Message 就可以直接访问, 不必指定完整的限定符 ``` 也可以导入某个范围之内所有可访问的内容, 比如包, 类, 对象, 等等: ```KOTLIN import org.example.* // 导入后 'org.example' 内的一切都可以访问了 ``` 如果发生了名称冲突, 你可以使用 `as` 关键字, 给重名实体指定新的名称(新名称仅在当前范围内有效): ```KOTLIN import org.example.Message // 导入后 Message 可以访问了 import org.test.Message as TestMessage // 可以使用新名称 TestMessage 来访问 'org.test.Message' ``` `import` 关键字不仅可以用来导入类; 还可以用来导入其他声明: * 顶级(top-level) 函数和属性 * [对象声明](object-declarations.html#object-declarations-overview) 中定义的函数和属性 * [枚举常数](enum-classes.html) ## 顶级(top-level) 声明的可见度 如果一个顶级(top-level) 声明被标注为 `private`, 它将成为私有的, 只有在它所属的文件内可以访问(参见 [可见度修饰符](visibility-modifiers.html)). # 注解 注解是一种标签, 可以用来向你的代码中的元素添加元数据(metadata). 工具和框架会在编译期间和运行期间处理这些元数据, 并根据元数据执行不同的操作. 你可以注解你的代码, 来简化和自动化一些常见任务, 例如生成样板代码, 强制执行代码规范, 或编写文档. Tip: 如果你想开发自己的注解处理器, 可以使用 [Kotlin 符号处理(Kotlin Symbol Processing, KSP)](ksp-overview.html) API. ## 声明 注解是一种特殊的类. 要声明一个注解, 请在类的声明之前使用 `annotation` 关键字: ```KOTLIN annotation class Fancy ``` 注解的其他属性, 可以通过向注解类添加元注解(meta-annotation)的方法来指定: * [@Target](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.annotation/-target/index.html) 指定这个注解可被用于哪些元素(比如类, 函数, 属性, 表达式); * [@Retention](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.annotation/-retention/index.html) 指定这个注解的信息是否被保存到编译后的 class 文件中, 以及在运行时是否可以通过反射访问到它 (默认情况下, 这两个设定都是 true); * [@Repeatable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.annotation/-repeatable/index.html) 允许在单个元素上多次使用同一个注解; * [@MustBeDocumented](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.annotation/-must-be-documented/index.html) 表示这个注解是公开 API 的一部分, 在自动产生的 API 文档的类或者函数签名中, 应该包含这个注解的信息. ```KOTLIN @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPE_PARAMETER, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION) @Retention(AnnotationRetention.SOURCE) @MustBeDocumented annotation class Fancy ``` ## 注解的使用 ```KOTLIN @Fancy class Foo { @Fancy fun baz(@Fancy foo: Int): Int { return (@Fancy 1) } } ``` 如果你需要对一个类的主构造器添加注解, 那么必须在构造器声明中添加 `constructor` 关键字, 然后在这个关键字之前添加注解: ```KOTLIN class Foo @Inject constructor(dependency: MyDependency) { ... } ``` 也可以对属性的访问器函数添加注解: ```KOTLIN class Foo { var x: MyDependency? = null @Inject set } ``` ## 构造器 注解可以拥有带参数的构造器. ```KOTLIN annotation class Special(val why: String) @Special("example") class Foo {} ``` 允许使用的参数类型包括: * 与 Java 基本类型对应的数据类型(Int, Long, 等等.) * 字符串 * 类 (`Foo::class`) * 枚举 * 其他注解 * 由以上数据类型构成的数组 注解的参数不能是可为 null 的类型, 因为 JVM 不支持在注解的属性中保存 `null` 值. 如果一个注解被用作另一个注解的参数, 那么在它的名字之前不使用 `@` 前缀: ```KOTLIN annotation class ReplaceWith(val expression: String) annotation class Deprecated( val message: String, val replaceWith: ReplaceWith = ReplaceWith("")) @Deprecated("This function is deprecated, use === instead", ReplaceWith("this === other")) ``` 如果你需要指定一个类作为注解的参数, 请使用 Kotlin 类 (参见 [KClass](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-class/index.html)). Kotlin 编译器会将它自动转换为 Java 类, 因此 Java 代码可以正常访问这个注解和它的参数. ```KOTLIN import kotlin.reflect.KClass annotation class Ann(val arg1: KClass<*>, val arg2: KClass) @Ann(String::class, Int::class) class MyClass ``` ## 创建注解类的实例 在 Java 中, 注解类型是一种形式的接口, 因此你不能实现一个注解类, 并使用它的实例. Kotlin 使用不同的机制, 允许你在任意代码中调用注解类的构造器, 然后使用得到的实例. ```KOTLIN annotation class InfoMarker(val info: String) fun processInfo(marker: InfoMarker): Unit = TODO() fun main(args: Array) { if (args.isNotEmpty()) processInfo(getAnnotationReflective(args)) else processInfo(InfoMarker("default")) } ``` 关于创建注解类的实例, 更多详情请参见 [这篇 KEEP 文档](https://github.com/Kotlin/KEEP/blob/master/proposals/annotation-instantiation.md). ## Lambda 表达式 注解也可以用在 Lambda 上. 此时, Lambda 表达式的函数体内容将会生成一个`invoke()` 方法, 注解将被添加到这个方法上. 这个功能对于 [Quasar](https://docs.paralleluniverse.co/quasar/) 这样的框架非常有用, 因为这个框架使用注解来进行并发控制. ```KOTLIN annotation class Suspendable val f = @Suspendable { Fiber.sleep(10) } ``` ## 注解的使用目标(Use-site Target) 当你对一个属性或一个主构造器的参数添加注解时, 从一个 Kotlin 元素会产生出多个 Java 元素, 因此在编译产生的 Java 字节码中, 你的注解存在多个可能的适用目标. 为了明确指定注解应该使用在哪个元素上, 可以使用以下语法: ```KOTLIN class Example(@field:Ann val foo, // 只对 Java 域变量添加注解 @get:Ann val bar, // 只对属性的 Java get 方法添加注解 @param:Ann val quux) // 只对 Java 构造器参数添加注解 ``` 同样的语法也可以用来对整个源代码文件添加注解. 你可以添加一个目标为 `file` 的注解, 放在源代码文件的最顶端, package 指令之前, 如果这个源代码属于默认的包, 没有 package 指令, 则放在所有的 import 语句之前: ```KOTLIN @file:JvmName("Foo") package org.jetbrains.demo ``` 如果你有目标相同的多个注解, 那么可以目标之后添加方括号, 然后将所有的注解放在方括号之内, 这样就可以避免重复指定相同的目标(`all` 目标除外): ```KOTLIN class Example { @set:[Inject VisibleForTesting] var collaborator: Collaborator } ``` Kotlin 支持的所有注解使用目标如下: * `file` * `field` * `property` (使用这个目标的注解, 在 Java 中无法访问) * `get` (属性的 get 方法) * `set` (属性的 set 方法) * `all` (针对属性的元目标(meta-target), 详情请参见 [all 元目标(meta-target)](#all-meta-target) 小节) * `receiver` (扩展函数或扩展属性的接受者参数) 要对扩展函数的接受者参数添加注解, 请使用以下语法: ```KOTLIN fun @receiver:Fancy String.myExtension() { ... } ``` * `param` (构造器的参数) * `setparam` (属性 set 方法的参数) * `delegate` (保存代理属性的代理对象实例的域变量) ### 没有指定使用目标时的默认值 如果不指定注解的使用目标, 那么编译器将会根据你对这个注解使用的 `@Target` 注解来自动选定使用目标. 如果存在多个可用的目标, 编译器将会按照以下顺序, 选择一个或多个: * 构造器参数目标(`param`). * 属性目标(`property`). * 如果域变量目标(`field`)可以使用, 而且属性目标(`property`)不能使用, 则使用域变量目标(`field`). 如果 `param`, `property`, 或 `field` 都不可使用, 那么注解不正确, 你需要明确的指定使用目标. 我们来使用 [Jakarta Bean Validation 中的 @Email 注解](https://jakarta.ee/specifications/bean-validation/3.0/apidocs/jakarta/validation/constraints/email): ```JAVA @Target(value={METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER,TYPE_USE}) public @interface Email { } ``` 关于这个注解的使用, 请考虑下面的示例: ```KOTLIN data class User(val username: String, // 这里的 @Email 现在会等于 @param:Email @field:Email @Email val email: String) { // 这里的 @Email 继续等于 @field:Email @Email val secondaryEmail: String? = null } ``` 在这个示例中, 对于 `email` 属性, `@Email` 注解同时应用于构造器参数目标和域变量目标, 因为这个属性: * 声明在主构造器中. * 没有自定义的 get 方法和 set 方法, 因此编译器会生成后端域变量(Backing Field). 对于 `secondaryEmail` 属性, `@Email` 注解只应用于域变量目标, 因为这个属性: * 没有声明在主构造器中. * 没有自定义的 get 方法和 set 方法, 因此编译器会生成后端域变量(Backing Field). ### `all` 元目标(meta-target) `all` 目标可以更容易的将同一个注解不仅应用于参数和属性或域变量, 而且应用于对应的 get 方法和 set 方法. 具体来说, 如果可用, 那么标注了 `all` 的注解会: * 如果属性在主构造器中定义, 传播到构造器参数 (`param`). * 传播到属性本身 (`property`). * 如果属性拥有后端域变量(Backing Field), 传播到后端域变量 (`field`). * 传播到 get 方法 (`get`). * 如果属性定义为 `var`, 传播到 set 方法参数 (`setparam`). * 如果类存在 `@JvmRecord` 注解, 传播到 Java 专用的目标 `RECORD_COMPONENT`. 我们来使用 [Jakarta Bean Validation 中的 @Email 注解](https://jakarta.ee/specifications/bean-validation/3.0/apidocs/jakarta/validation/constraints/email), 它的定义如下: ```JAVA @Target(value={METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER,TYPE_USE}) public @interface Email { } ``` 在下面的示例中, 这个 `@Email` 注解被应用于所有相关的目标: ```KOTLIN data class User( val username: String, // 将 `@Email` 应用到 `param`, `field` 和 `get` @all:Email val email: String, // 将 `@Email` 应用到 `param`, `field`, `get`, 和 `setparam` @all:Email var name: String, ) { // 将 `@Email` 应用到 `field` 和 `getter` (不应用于 `param`, 因为这个属性声明不在构造器中) @all:Email val secondaryEmail: String? = null } ``` 你可以对任何属性使用 `all` 目标, 无论是在主构造器之内还是之外. #### 限制 `all` 目标存在一些限制: * 它不会将注解传播到类型, 潜在的扩展接受者, 或上下文接受者或参数. * 它不能与多个注解一起使用: ```KOTLIN @all:[A B] // 禁止这种用法, 请使用 `@all:A @all:B` val x: Int = 5 ``` * 它不能用于 [委托属性](delegated-properties.html). ## Java 注解 Kotlin 100% 兼容 Java 注解: ```KOTLIN import org.junit.Test import org.junit.Assert.* import org.junit.Rule import org.junit.rules.* class Tests { // 对属性的 get 方法使用 @Rule 注解 @get:Rule val tempFolder = TemporaryFolder() @Test fun simple() { val f = tempFolder.newFile() assertEquals(42, getTheAnswer()) } } ``` 由于 Java 注解中没有定义参数的顺序, 因此不可以使用通常的函数调用语法来给注解传递参数. 相反, 你需要使用命名参数语法: ```JAVA // Java public @interface Ann { int intValue(); String stringValue(); } ``` ```KOTLIN // Kotlin @Ann(intValue = 1, stringValue = "abc") class C ``` 与 Java 一样, 有一个特殊情况就是 `value` 参数; 这个参数的值可以不使用明确的参数名来指定: ```JAVA // Java public @interface AnnWithValue { String value(); } ``` ```KOTLIN // Kotlin @AnnWithValue("abc") class C ``` ### 使用数组作为注解参数 如果 Java 注解的 `value` 参数是数组类型, 那么在 Kotlin 中会变为 `vararg` 类型: ```JAVA // Java public @interface AnnWithArrayValue { String[] value(); } ``` ```KOTLIN // Kotlin @AnnWithArrayValue("abc", "foo", "bar") class C ``` 对于其他数组类型的参数, 为其赋值时你需要使用数组字面值, 或使用 `arrayOf` 函数: ```JAVA // Java public @interface AnnWithArrayMethod { String[] names(); } ``` ```KOTLIN @AnnWithArrayMethod(names = ["abc", "foo", "bar"]) class C ``` ### 访问注解实例的属性值 Java 注解实例的值, 在 Kotlin 代码中可以通过属性的形式访问: ```JAVA // Java public @interface Ann { int value(); } ``` ```KOTLIN // Kotlin fun foo(ann: Ann) { val i = ann.value } ``` ### 不生成 JVM 1.8+ 注解目标(Target)的能力 如果一个 Kotlin 注解的 Kotlin 注解目标(Target)中包含 `TYPE`, 那么映射的 Java 注解目标会包含 `java.lang.annotation.ElementType.TYPE_USE`. 同样的, Kotlin 注解目标 `TYPE_PARAMETER` 会映射为 Java 注解目标 `java.lang.annotation.ElementType.TYPE_PARAMETER`. 对于 API 级别低于 26 的 Android 用户来说, 这会造成问题, 因为在 API 中不存在这些注解目标. 要避免生成 `TYPE_USE` 和 `TYPE_PARAMETER` 注解目标, 请使用新的编译器参数 `-Xno-new-java-annotation-targets`. ## 可重复注解 就像 [在 Java 中](https://docs.oracle.com/javase/tutorial/java/annotations/repeating.html) 一样, Kotlin 也有可重复注解, 它可以对同个代码元素使用多次. 要让你的注解成为可重复注解, 请在它的声明中使用 [@kotlin.annotation.Repeatable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.annotation/-repeatable/) 元注解(meta-annotation). 这样会使得这个注解在 Kotlin 和 Java 中都成为可重复注解. 在 Kotlin 中, 也支持 Java 中定义的可重复注解. 与 Java 中使用的方法的主要区别在于, 不存在 容器注解(containing annotation), Kotlin 编译器会使用预定义的名称自动生成容器注解. 对于下面示例中的注解, 会生成名为 `@Tag.Container` 的容器注解: ```KOTLIN @Repeatable annotation class Tag(val name: String) // 编译器生成名为 @Tag.Container 的容器注解 ``` 你可以对容器注解设置自定义的名称, 方法是使用 [@kotlin.jvm.JvmRepeatable](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.jvm/-jvm-repeatable/) 元注解(meta-annotation), 指定一个明确声明的容器注解类作为参数: ```KOTLIN @JvmRepeatable(Tags::class) annotation class Tag(val name: String) annotation class Tags(val value: Array) ``` 要通过反射取得 Kotlin 或 Java 的可重复注解, 请使用 [KAnnotatedElement.findAnnotations()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect.full/find-annotations.html) 函数. 关于 Kotlin 的可重复注解, 更多详情请参见 [这篇 KEEP](https://github.com/Kotlin/KEEP/blob/master/proposals/repeatable-annotations.md). # 可见度修饰符 类, 对象, 接口, 构造器, 函数, 属性, 以及属性的设值方法, 都可以使用 可见度修饰符. 属性的取值方法永远与属性本身的可见度一致, 因此不需要控制其可见度. Kotlin 中存在 4 种可见度修饰符: `private`, `protected`, `internal`, 以及 `public`. 默认的可见度为 `public`. 本节中, 我们将介绍这些可见度标识符如何应用于不同范围的不同类型. ## 包 函数, 属性, 类, 对象, 接口, 都可以直接在包之内声明为"顶级的(top-level)": ```KOTLIN // 源代码文件名: example.kt package foo fun baz() { ... } class Bar { ... } ``` * 如果你不使用可见度修饰符, 默认会使用 `public`, 其含义是, 你声明的东西在任何位置都可以访问. * 如果你将声明的东西标记为 `private`, 那么它将只在同一个源代码文件内可以访问. * 如果标记为 `internal`, 那么它将在同一个[模块(module)](#modules)内的任何位置都可以访问. * 对于顶级(top-level)声明, `protected` 修饰符是无效的. Note: 如果一个包中的顶级声明是可见的, 在另一个包中使用它时需要 [导入(import)](packages.html#imports). 示例: ```KOTLIN // 文件名: example.kt package foo private fun foo() { ... } // 只在 example.kt 文件内可访问 public var bar: Int = 5 // 这个属性在任何地方都可以访问 private set // 但它的设值方法只在 example.kt 文件内可以访问 internal val baz = 6 // 在同一个模块(module)内可以访问 ``` ## 类成员 对于类内部声明的成员: * `private` 表示这个成员只在这个类(以及它的所有成员)之内可以访问. * `protected` 表示这个成员的可见度与 `private` 一样, 但它在子类中也可以访问. * `internal` 表示在 本模块之内, 凡是能够访问到这个类的地方, 同时也能访问到这个类的 `internal` 成员. * `public` 表示凡是能够访问到这个类的地方, 同时也能访问这个类的 `public` 成员. Note: 在 Kotlin 中, 外部类(outer class)不能访问其内部类(inner class)的 `private` 成员. 如果你覆盖一个 `protected` 或 `internal` 成员, 并且没有明确指定可见度, 那么覆盖后成员的可见度将与覆盖前的成员一样. 示例: ```KOTLIN open class Outer { private val a = 1 protected open val b = 2 internal open val c = 3 val d = 4 // 默认为 public protected class Nested { public val e: Int = 5 } } class Subclass : Outer() { // a 不可访问 // b, c 和 d 可以访问 // Nested 和 e 可以访问 override val b = 5 // 'b' 可见度为 protected override val c = 7 // 'c' 可见度为 internal } class Unrelated(o: Outer) { // o.a, o.b 不可访问 // o.c 和 o.d 可以访问(属于同一模块) // Outer.Nested 不可访问, Nested::e 也不可访问 } ``` ### 构造器 要指定类的主构造器的可见度, 请使用以下语法: Note: 你需要明确添加一个 `constructor` 关键字: ```KOTLIN class C private constructor(a: Int) { ... } ``` 这里构造器是 `private` 的. 所有构造器默认都是 `public` 的, 因此使得凡是可以访问到类的地方都可以访问到类的构造器 (因此 一个 `internal` 类的构造器只能在同一个模块内访问). 对于封闭类(Sealed Class), 构造器默认为 `protected` 的. 更多详情请参见 [封闭类(Sealed Class)](sealed-classes.html#constructors). ### 局部声明 局部变量, 局部函数, 以及局部类, 都不能指定可见度修饰符. ## 模块(Module) `internal` 修饰符表示这个成员只能在同一个模块内访问. 更确切地说, 一个模块(module)是指一起编译的一组 Kotlin 源代码文件, 例如: * 一个 IntelliJ IDEA 模块. * 一个 Maven 工程. * 一个 Gradle 源代码集(source set) (`test` 源代码集例外, 它可以访问 `main` 中的 `internal` 声明). # 编码规约 对任何编程语言来说, 都需要一种广为人知, 并且易于遵守的编码规约. 这里我们对使用 Kotlin 的项目, 给出一些编码规约和代码组织的指导原则. ## 在 IDE 中配置代码规则 最流行的2个 Kotlin IDE - [IntelliJ IDEA](https://www.jetbrains.com/idea/) 和 [Android Studio](https://developer.android.com/studio/) 对代码规则提供了强大的支持. 你可以配置代码规则来自动格式化你的代码, 是代码符合统一的规则. ### 应用代码规则 1. 进入设置界面 Settings/Preferences | Editor | Code Style | Kotlin. 2. 点击 Set from.... 3. 选择 Kotlin style guide . ### 验证你的代码是否符合代码规则 1. 进入设置界面 Settings/Preferences | Editor | Inspections | General. 2. 选中检查项 Incorrect formatting. 对于本编码规约中提到的其他问题 (比如命名规约), 相应的检查项目默认已经启用了. 详情请参见 [使用 IntelliJ IDEA 迁移到 Kotlin 编码风格](code-style-migration-guide.html) 向导. ## 源代码组织 ### 目录结构 在纯 Kotlin 语言的项目中, 建议源代码文件的目录结构遵循包的结构, 但省略共通的源代码根目录. 比如, 如果项目内的所有源代码都在 `org.example.kotlin` 包及其子包之下, 那么 `org.example.kotlin` 包对应的文件应该直接保存到源代码的根目录下, 而 `org.example.kotlin.network.socket` 包下的文件应该保存在源代码根目录下的 `network/socket` 子目录下. Note: 对于 JVM 平台: 在混合使用 Kotlin 和 Java 的项目中, Kotlin 源代码文件应该与 Java 源代码文件放在相同的源代码根目录下, 并且遵循相同的目录结构: 每个文件应该保存在它的 package 语句对应的目录之下. ### 源代码文件名 如果 Kotlin 源代码文件只包含单个类或接口 (以及相关的顶级声明), 那么源代码文件的名称应该与类名相同, 再加上 `.kt` 扩展名. 这个规则适用于所有类型的类和接口. 如果源代码文件包含多个类, 或者只包含顶级声明, 请选择一个能够描述文件所包含内容的名称, 用这个名称作为源代码文件名. 文件名如果包含多个单词, 请使用 [驼峰式大小写](https://en.wikipedia.org/wiki/Camel_case), 将每个单词的首字母大写. 例如, `ProcessDeclarations.kt`. 文件的名称应该描述其中包含的代码的功能. 因此, 应该避免在文件名中使用无意义的单词, 比如 `Util`. #### 跨平台项目 在跨平台项目中, 在平台相关源代码集中, 带有顶级(top-level)声明的文件应该带有后缀, 后缀关联到源代码集名称. 例如: * jvmMain/kotlin/Platform.jvm.kt * androidMain/kotlin/Platform.android.kt * iosMain/kotlin/Platform.ios.kt 对于 common 源代码集, 带有顶级声明的文件不应该带有后缀. 例如, `commonMain/kotlin/Platform.kt`. ##### 技术细节 我们推荐在跨平台项目中遵循这样的文件命名风格, 是因为 JVM 的限制: 它不允许存在顶层成员 (函数, 属性). 为了解决这个问题, Kotlin JVM 编译器会创建封装类(wrapper class), (也就是所谓的 "File Facade"), 通过这些封装类来包含顶层成员的声明. File Facade 拥有一个根据文件名称得到的内部名称. 而且, JVM 不允许多个类使用相同的完全限定名 (FQN). 这可能会导致 Kotlin 项目在 JVM 上无法编译: ``` root |- commonMain/kotlin/myPackage/Platform.kt // 包含 'fun count() { }' |- jvmMain/kotlin/myPackage/Platform.kt // 包含 'fun multiply() { }' ``` 这时, 两个 `Platform.kt` 文件属于相同的包, 因此 Kotlin JVM 编译器生成两个 File Facade, 它们的 FQN 都是 `myPackage.PlatformKt`. 因此发生 "Duplicate JVM classes" 错误. 避免这个错误的最简单的方法是, 遵照上面所说的规约, 将某个文件改名. 这样的命名规约可以帮助避免名称冲突, 同时保持代码的可读性. Tip: 在两种场景下, 上面的命名规约可以省略, 但我们仍然建议遵循这种命名规约: * 非 JVM 平台 对重复的 File Facade 不会发生错误. 但是, 这种命名规约可以帮助你保持文件名称的一致性. * 在 JVM 平台上, 如果源代码文件不包含顶层声明, 就不会生成File Facade, 因此你不会遇到名称冲突的问题. 但是, 只要一次简单的代码重构, 或代码添加一个顶层函数, 就可以造成 "Duplicate JVM classes" 错误, 这种命名规约可以帮助你避免这样的情况. ### 源代码文件的组织 如果多个声明 (类, 顶级函数, 或顶级属性) 在语义上相互之间相关密切, 并且文件大小合理(不超过几百行的规模), 那么我们鼓励将这些放在同一个 Kotlin 源代码文件中. 尤其是, 当为类定义扩展函数时, 如果与这个类的所有使用者都有关系, 那么应该将它们与这个类放在一起. 如果定义的扩展函数, 只对特定的使用者有意义, 请将它们放在这个使用者的代码之后. 不要仅仅为了保存某个类的所有扩展函数而创建一个单独的源代码文件. ### 类的布局 类的内容按以下顺序排列: 1. 属性声明, 以及初始化代码端 2. 次构造器 3. 方法声明 4. 同伴对象 请不要将方法声明按照字母顺序排列, 也不要按照可见度顺序排列, 也不要将常规方法与扩展方法分开. 相反, 要将关系紧密的代码放在一起, 以便让他人从上到下阅读代码时, 能够理解代码的逻辑含义. 你应该选择一个排序原则 (将逻辑含义上比较顶层的代码在前, 或者反过来), 然后在所有的代码中都遵循相同的原则. 将嵌套类放在使用它的代码之后. 如果嵌套类是为了供外部使用, 没有被类内部的代码使用, 那么请将它放在最后, 放在同伴对象之后. ### 接口实现类的布局 实现一个接口时, 将实现类中的成员方法顺序, 保持与接口中的声明顺序一致 (如果需要的话, 中间可以插入被实现方法用到的其它私有方法). ### 重载方法的布局 将同一个类中的同名重载方法放在一起. ## 命名规约 Kotlin 中的包和类的命名规则非常简单: * 包名称总是使用小写字母, 并且不使用下划线(`org.example.project`). 通常不鼓励使用多个单词的名称, 但如果的确需要, 你可以将多个单词直接连接在一起, 或者使用驼峰式大小写(`org.example.myProject`). * 类和对象的名称使用首字母大写的驼峰式大小写: ```KOTLIN open class DeclarationProcessor { /*...*/ } object EmptyDeclarationProcessor : DeclarationProcessor() { /*...*/ } ``` ### 函数名称 函数, 属性, 以及局部变量的名称以小写字母开头, 使用驼峰式大小写, 不使用下划线: ```KOTLIN fun processDeclarations() { /*...*/ } var declarationCount = 1 ``` 例外情况: 用于创建类实例的工厂函数, 可以使用与它创建的抽象类型相同的名称: ```KOTLIN interface Foo { /*...*/ } class FooImpl : Foo { /*...*/ } fun Foo(): Foo { return FooImpl() } ``` ### 测试方法名称 在测试代码中 (而且只有在测试代码中), 可以使用由反引号括起的, 带空格的方法名. 注意, 对于 Android 运行环境, 这样的方法名只在 API level 30 才开始支持. 测试代码中的方法名, 也允许使用下划线. ```KOTLIN class MyTestCase { @Test fun `ensure everything works`() { /*...*/ } @Test fun ensureEverythingWorks_onAndroid() { /*...*/ } } ``` ### 属性名称 对于常数 (标记了 `const` 的属性, 或不存在自定义的 `get` 函数的顶级 `val` 属性, 或对象的 `val` 属性, 并且其值是深层不可变数据), 应该使用下划线分隔的全大写名称, 遵循 [吼叫式蛇形大小写](https://en.wikipedia.org/wiki/Snake_case) 规则: ```KOTLIN const val MAX_COUNT = 8 val USER_NAME_FIELD = "UserName" ``` 顶级属性, 或对象属性, 如果它的值是对象, 或者包含可变的数据, 那么应该使用驼峰式大小写名称: ```KOTLIN val mutableCollection: MutableSet = HashSet() ``` 如果属性指向单体对象, 那么可以使用与 `object` 声明相同的命名方式: ```KOTLIN val PersonComparator: Comparator = /*...*/ ``` 对于枚举常数, 可以使用下划线分隔的全大写([吼叫式蛇形大小写](https://en.wikipedia.org/wiki/Snake_case)) 名称 (`enum class Color { RED, GREEN }`), 也可以使用首字母大写的驼峰式大小写名称, 由你的具体用法来决定. ### 后端属性名称 如果类拥有两个属性, 它们在概念上是相同的, 但其中一个是公开 API 的一部分, 而另一个属于内部的实现细节, 此时请使用下划线作为私有属性名的前缀: ```KOTLIN class C { private val _elementList = mutableListOf() val elementList: List get() = _elementList } ``` ### 选择好的名称 类的名称通常使用名词, 或名词短语, 要能够解释这个类 是 什么: `List`, `PersonReader`. 方法名称通常使用动词, 或动词短语, 说明这个方法 做 什么: `close`, `readPersons`. 方法名称还应该能够说明这个方法是变更这个对象, 或者还是返回一个新的实例. 比如 `sort` 是对集合(collection)本身的内容排序, 而 `sorted` 则是返回这个集合的一个副本, 其中包含排序后内容. 名称应该解释清楚这个类或方法的目的是什么, 因此最好在命名时避免使用含义不清的词语(`Manager`, `Wrapper`). 在名称中使用缩写字母时, 要遵循以下规则: * 对于只包含 2 个字母的缩写, 请全部使用大写. 例如, `IOStream`. * 对于超过 2 个字母的缩写, 请将首字母大写, 其他字母小写. 例如, `XmlFormatter` 或 `HttpInputStream`. ## 代码格式化 ### 缩进 缩进时使用 4 个空格. 不要使用 tab. 对于大括号, 请将开括号放在结构开始处的行末, 将闭括号放在单独的一行, 与它所属的结构缩进到同样的位置. ```KOTLIN if (elements != null) { for (element in elements) { // ... } } ``` Note: 在 Kotlin 中, 分号是可以省略的, 因此折行很重要. 语言设计时预想使用 Java 风格的大括号, 如果你使用不同的格式化风格, 你的代码执行时的行为可能会与你预想的不同. ### 水平空格 * 二元运算符前后应该加入空格 (`a + b`). 例外情况是: 不要在 "值范围" 运算符前后加入空格 (`0..i`). * 一元运算符前后不要加入空格 (`a++`) * 流程控制关键字(`if`, `when`, `for` 以及 `while`) 以及对应的开括号之间, 要加入空格. * 对于主构造器声明, 方法声明, 以及方法调用, 不要在开括号之前加入空格. ```KOTLIN class A(val x: Int) fun foo(x: Int) { ... } fun bar() { foo(1) } ``` * 不要在 `(`, `[` 之后加入空格, 也不要在 `]`, `)` 之前加入空格. * 不要在 `.` 或 `?.` 前后加入空格: `foo.bar().filter { it > 2 }.joinToString()`, `foo?.bar()`. * 在 `//` 之后要加入空格: `// 这是一段注释`. * 对于用来表示类型参数的尖括号, 不要在它前后加入空格: `class Map { ... }`. * 不要在 `::` 前后加入空格: `Foo::class`, `String::length`. * 对于用来表示可空类型的 `?`, 不要在它之前加入空格: `String?`. 一般来说, 不要进行任何形式的水平对齐. 如果将一个标识符改为不同长度的名称, 不应该影响到它的任何声明, 以及任何使用的格式. ### 冒号 以下场景, 要在 `:` 之前加入空格: * 用作类型与父类型之间的分隔符时. * 委托给超类的构造器, 或者委托给同一个类的另一个构造器时. * 用在 `object` 关键字之后时. 如果 `:` 用作某个声明与它的类型之间的分隔符时, 不要它前面加入空格. 在 `:` 之后, 一定要加入一个空格. ```KOTLIN abstract class Foo : IFoo { abstract fun foo(a: Int): T } class FooImpl : Foo() { constructor(x: String) : this(x) { /*...*/ } val x = object : IFoo { /*...*/ } } ``` ### 类头部 如果类的主构造器只有少量参数, 可以写成单独的一行: ```KOTLIN class Person(id: Int, name: String) ``` 如果类的头部很长, 应该调整代码格式, 将主构造器(primary constructor)的每一个参数放在单独的行中, 并对其缩进. 同时, 闭括号也应放在新的一行. 如果使用类的继承, 那么对超类构造器的调用, 以及实现的接口的列表, 应该与闭括号放在同一行内: ```KOTLIN class Person( id: Int, name: String, surname: String ) : Human(id, name) { /*...*/ } ``` 对于多个接口的情况, 对超类构造器的调用应该放在最前, 然后将每个接口放在单独的行中: ```KOTLIN class Person( id: Int, name: String, surname: String ) : Human(id, name), KotlinMaker { /*...*/ } ``` 如果类的父类型列表很长, 请在冒号之后换行, 并将所有的父类型名称缩进到同样的位置: ```KOTLIN class MyFavouriteVeryLongClassHolder : MyLongHolder(), SomeOtherInterface, AndAnotherOne { fun foo() { /*...*/ } } ``` 当类头部很长时, 为了将类头部和类主体部分更清楚地分隔开, 可以在类头部之后加入一个空行(如上面的例子所示), 也可以将大括号放在单独的一行: ```KOTLIN class MyFavouriteVeryLongClassHolder : MyLongHolder(), SomeOtherInterface, AndAnotherOne { fun foo() { /*...*/ } } ``` 对构造器的参数, 使用通常的缩进(4 个空格). 这是为了让主构造器中声明的属性, 与类主体部分声明的属性的缩进保持一致. ### 修饰符顺序 如果一个声明带有多个修饰符, 修饰符一定要按照下面的顺序排列: ```KOTLIN public / protected / private / internal expect / actual final / open / abstract / sealed / const external override lateinit tailrec vararg suspend inner enum / annotation / fun // 在 `fun interface` 中, `fun` 是修饰符 companion inline / value infix operator data ``` 所有的注解要放在修饰符之前: ```KOTLIN @Named("Foo") private val foo: Foo ``` 除非你在开发一个库, 否则应该省略多余的修饰符(比如 `public`). ### 注解(Annotation) 注解放在它修饰的声明之前, 放在单独的行中, 使用相同的缩进: ```KOTLIN @Target(AnnotationTarget.PROPERTY) annotation class JsonExclude ``` 无参数的注解可以放在同一行中: ```KOTLIN @JsonExclude @JvmField var x: String ``` 单个无参数的注解可以与它修饰的声明放在同一行中: ```KOTLIN @Test fun foo() { /*...*/ } ``` ### 文件注解 文件注解放在文件注释之后(如果存在的话), 在 `package` 语句之前, 与 `package` 语句之间用空行隔开 (为了强调注解的对象是文件, 而不是包). ```KOTLIN /** License, copyright and whatever */ @file:JvmName("FooBar") package foo.bar ``` ### 函数 如果函数签名无法排列在一行之内, 请使用下面的语法: ```KOTLIN fun longMethodName( argument: ArgumentType = defaultValue, argument2: AnotherArgumentType, ): ReturnType { // body } ``` 函数参数使用通常的缩进(4 个空格). 这是为了与构造器参数保持一致 如果函数体只包含单独的一个表达式, 应当使用表达式函数体. ```KOTLIN fun foo(): Int { // 这是不好的风格 return 1 } fun foo() = 1 // 这是好的风格 ``` ### 函数体表达式 如果函数体表达式太长, 它的第一行无法与函数声明放在同一行之内, 那么应该将 `=` 符号放在第一行, 然后表达式函数体放在下一行, 缩进 4 个空格. ```KOTLIN fun f(x: String, y: String, z: String) = veryLongFunctionCallWithManyWords(andLongParametersToo(), x, y, z) ``` ### 属性 对于简单的只读属性, 应该使用单行格式: ```KOTLIN val isEmpty: Boolean get() = size == 0 ``` 对更复杂一些的属性, 一定要将 `get` 和 `set` 关键字放在单独的行: ```KOTLIN val foo: String get() { /*...*/ } ``` 对于带有初始化器(initializer)的属性, 如果初始化器很长, 请在等号之后换行, 然后对初始化器缩进 4 个空格: ```KOTLIN private val defaultCharset: Charset? = EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file) ``` ### 控制流语句 如果 `if` 或 `when` 语句的条件部分有多行代码, 一定要将主体部分用大括号括起. 将条件部分的每一个子句, 从语句开始的位置缩进 4 个空格. 将条件部分的闭括号, 与主体部分的开括号一起, 放在单独一行: ```KOTLIN if (!component.isSyncing && !hasAnyKotlinRuntimeInScope(module) ) { return createKotlinNotConfiguredPanel(module) } ``` 这样可以将条件部分与主体部分对齐. 将 `else`, `catch`, `finally` 关键字, 以及 `do-while` 循环语句的 `while` 关键字, 与它之后的开括号放在同一行中: ```KOTLIN if (condition) { // 主体部分 } else { // 其它部分 } try { // 主体部分 } finally { // 清除处理 } ``` 在 `when` 语句中, 如果一个条件分支包含了多行语句, 应该将它与临近的条件分支用空行分隔开: ```KOTLIN private fun parsePropertyValue(propName: String, token: Token) { when (token) { is Token.ValueToken -> callback.visitValue(propName, token.value) Token.LBRACE -> { // ... } } } ``` 对于比较短的分支, 与条件部分放在同一行中, 不用大括号. ```KOTLIN when (foo) { true -> bar() // 这是比较好的风格 false -> { baz() } // 这是不好的风格 } ``` ### 方法调用 如果参数列表很长, 请在开括号之后换行. 参数缩进 4 个空格. 关系紧密的多个参数放在同一行中. ```KOTLIN drawSquare( x = 10, y = 10, width = 100, height = 100, fill = true ) ``` 在 `=` 前后加入空格, 将参数名与参数值分隔开. ### 链式调用(chained call)的换行 对链式调用(chained call)换行时, 将 `.` 字符或 `?.` 操作符放在下一行, 使用单倍缩进: ```KOTLIN val anchor = owner ?.firstChild!! .siblings(forward = true) .dropWhile { it is PsiComment || it is PsiWhiteSpace } ``` 链式调用中的第一个调用, 在它之前通常应该换行, 但如果能让代码更合理, 也可以省略换行. ### Lambda 表达式 在 Lambda 表达式中, 在大括号前后应该加入空格, 分隔参数与表达式体的箭头前后也要加入空格. 如果一个函数调用可以接受单个 Lambda 表达式作为参数, 那么 Lambda 表达式应该尽可能写到函数调用的圆括号之外. ```KOTLIN list.filter { it > 10 } ``` 如果为 Lambda 表达式指定标签, 请不要在标签与表达式体的开括号之间加入空格: ```KOTLIN fun foo() { ints.forEach lit@{ // ... } } ``` 在多行的 Lambda 表达式中声明参数名称时, 请将参数名放在第一行, 后面放箭头, 然后换行: ```KOTLIN appendCommaSeparated(properties) { prop -> val propertyValue = prop.get(obj) // ... } ``` 如果参数列表太长, 无法放在一行之内, 请将箭头放在单独的一行: ```KOTLIN foo { context: Context, environment: Env -> context.configureEnv(environment) } ``` ### 尾随逗号(Trailing Comma) 尾随逗号是指, 在一系列元素的最末尾之后出现的逗号: ```KOTLIN class Person( val firstName: String, val lastName: String, val age: Int, // 尾随逗号 ) ``` 使用尾随逗号可以带来下面这些益处: * 版本控制中的差分比较更加清晰 – 因为差分只会出现在真正修改过的代码行. * 更加易于添加元素, 或改变元素顺序 – 修改元素时不再需要添加或删除逗号. * 简化了代码生成工作, 比如, 对于对象的初始化代码. 最后一个元素也可以带有逗号. 尾随逗号完全是可选的 – 没有尾随逗号, 你的代码仍然可以工作. Kotlin 编码风格向导鼓励在声明处使用尾随逗号, 在调用处则由你自己决定. 要在 IntelliJ IDEA 的代码格式化工具中启用尾随逗号, 请进入设置界面 Settings/Preferences | Editor | Code Style | Kotlin, 打开 Other 页, 然后选中 Use trailing comma 选项. #### 枚举 ```KOTLIN enum class Direction { NORTH, SOUTH, WEST, EAST, // 尾随逗号 } ``` #### 值参数 ```KOTLIN fun shift(x: Int, y: Int) { /*...*/ } shift( 25, 20, // 尾随逗号 ) val colors = listOf( "red", "green", "blue", // 尾随逗号 ) ``` #### 类的属性和参数 ```KOTLIN class Customer( val name: String, val lastName: String, // 尾随逗号 ) class Customer( val name: String, lastName: String, // 尾随逗号 ) ``` #### 函数值参数 ```KOTLIN fun powerOf( number: Int, exponent: Int, // 尾随逗号 ) { /*...*/ } constructor( x: Comparable, y: Iterable, // 尾随逗号 ) {} fun print( vararg quantity: Int, description: String, // 尾随逗号 ) {} ``` #### 带有可选类型的参数 (包括属性的 set 函数) ```KOTLIN val sum: (Int, Int, Int) -> Int = fun( x, y, z, // 尾随逗号 ): Int { return x + y + x } println(sum(8, 8, 8)) ``` #### 下标后缀 ```KOTLIN class Surface { operator fun get(x: Int, y: Int) = 2 * x + 4 * y - 10 } fun getZValue(mySurface: Surface, xValue: Int, yValue: Int) = mySurface[ xValue, yValue, // 尾随逗号 ] ``` #### Lambda 表达式的参数 ```KOTLIN fun main() { val x = { x: Comparable, y: Iterable, // 尾随逗号 -> println("1") } println(x) } ``` #### when 语句的分支条件 ```KOTLIN fun isReferenceApplicable(myReference: KClass<*>) = when (myReference) { Comparable::class, Iterable::class, String::class, // 尾随逗号 -> true else -> false } ``` #### 集合字面值 (在注解中) ```KOTLIN annotation class ApplicableFor(val services: Array) @ApplicableFor([ "serializer", "balancer", "database", "inMemoryCache", // 尾随逗号 ]) fun run() {} ``` #### 类型参数(Type argument) ```KOTLIN fun foo() {} fun main() { foo< Comparable, Iterable, // 尾随逗号 >() } ``` #### 类型参数(Type parameter) ```KOTLIN class MyMap< MyKey, MyValue, // 尾随逗号 > {} ``` #### 解构声明 ```KOTLIN data class Car(val manufacturer: String, val model: String, val year: Int) val myCar = Car("Tesla", "Y", 2019) val ( manufacturer, model, year, // 尾随逗号 ) = myCar val cars = listOf() fun printMeanValue() { var meanValue: Int = 0 for (( _, _, year, // 尾随逗号 ) in cars) { meanValue += year } println(meanValue/cars.size) } printMeanValue() ``` ## 文档注释 对于比较长的文档注释, 请将开头的 `/**` 放在单独的行, 后面的每一行都用星号开始: ```KOTLIN /** * 这是一段文档注释, * 其中包含多行. */ ``` 比较短的注释可以放在一行之内: ```KOTLIN /** 这是一段比较短的文档注释. */ ``` 通常来说, 不要使用 `@param` 和 `@return` 标记. 相反, 对参数和返回值的描述应该直接合并到文档注释之内, 在提到参数的地方应该添加链接. 只有参数或返回值需要很长的解释, 无法写在文档注释中, 这时才应该使用 `@param` 和 `@return` 标记. ```KOTLIN // 不要写这样的注释: /** * 对于给定的数值, 返回其绝对值. * @param number 需要返回绝对值的对象数值. * @return 绝对值. */ fun abs(number: Int): Int { /*...*/ } // 应该这样: /** * 对于给定的 [number], 返回其绝对值. */ fun abs(number: Int): Int { /*...*/ } ``` ## 避免冗余的结构 通常来说, 如果 Kotlin 代码中的某个语法结构是可省略的, 并且被 IDE 标记显示为可省略的, 那么你就应该在代码中省略这部分. 不要仅仅"为了解释清楚", 就在代码中留下不必须的语法元素. ### Unit 返回类型 如果函数的返回值为 Unit 类型, 那么返回值的类型声明应当省略: ```KOTLIN fun foo() { // 此处省略了 ": Unit" } ``` ### 分号 尽可能省略分号. ### 字符串模板 向字符串模板中插入简单变量时, 不要使用大括号. 只有对比较长的表达式, 才应该使用大括号: ```KOTLIN println("$name has ${children.size} children") ``` 使用 [多 $ 符号字符串插值](strings.html#multi-dollar-string-interpolation), 将美元符号 `$` 用作字符串字面值: ```KOTLIN val KClass<*>.jsonSchema : String get() = $$""" { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://example.com/product.schema.json", "$dynamicAnchor": "meta", "title": "$${simpleName ?: qualifiedName ?: "unknown"}", "type": "object" } """ ``` ## 各种语言特性的惯用法 ### 数据的不可变性 尽量使用不可变的数据, 而不是可变的数据. 如果局部变量或属性的值在初始化之后不再变更, 尽量将它们声明为 `val`, 而不是 `var`. 对于内容不发生变化的集合, 一定要使用不可变的集合接口(`Collection`, `List`, `Set`, `Map`) 来声明. 当使用工厂方法创建集合类型时, 一定要尽可能使用返回不可变集合类型的函数: ```KOTLIN // 这是不好的风格: 对于内容不再变化的值, 使用了可变的集合类型 fun validateValue(actualValue: String, allowedValues: HashSet) { ... } // 这是比较好的风格: 改用了不可变的集合类型 fun validateValue(actualValue: String, allowedValues: Set) { ... } // 这是不好的风格: arrayListOf() 的返回类型为 ArrayList, 这是一个可变的集合类型 val allowedValues = arrayListOf("a", "b", "c") // 这是比较好的风格: listOf() 的返回类系为 List val allowedValues = listOf("a", "b", "c") ``` ### 参数默认值 尽可能使用带默认值的参数来声明函数, 而不是声明多个不同参数的重载函数. ```KOTLIN // 不好的风格 fun foo() = foo("a") fun foo(a: String) { /*...*/ } // 比较好的风格 fun foo(a: String = "a") { /*...*/ } ``` ### 类型别名 如果你的某个函数类型, 或者某个带类型参数的类型, 在代码中多次用到, 那么应该尽量为它定义一个类型别名: ```KOTLIN typealias MouseClickHandler = (Any, MouseEvent) -> Unit typealias PersonIndex = Map ``` 如果你使用 private 或 internal 的类型别名来避免名称冲突, 建议改为使用 [包(Package)与导入(Import)](packages.html) 中介绍的 `import ... as ...` 功能. ### Lambda 表达式参数 在比较短, 而且没有嵌套的 Lambda 表达式, 建议使用 `it` 规约, 而不要明确声明参数. 在有参数的嵌套 Lambda 表达式中, 参数一定要明确声明. ### 在 Lambda 表达式中返回 不要在 Lambda 表达式中使用多个带标签的返回. 应该考虑重构你的 Lambda 表达式, 使它只有一个退出点. 如果无法做到, 或者代码不够清晰, 那么可以考虑把 Lambda 改为一个匿名函数. 在 Lambda 表达式中, 不要使用带标签的返回语句作为最后一条语句. ### 命名参数 如果一个方法接受同一种基本类型的多个参数, 或者如果参数为 `Boolean` 类型, 除非通过代码的上下文, 可以非常清楚地确定所有参数的含义, 否则此时应该使用命名参数语法. ```KOTLIN drawSquare(x = 10, y = 10, width = 100, height = 100, fill = true) ``` ### 条件语句 尽量使用 `try`, `if` 以及 `when` 的表达式形式. ```KOTLIN return if (x) foo() else bar() ``` ```KOTLIN return when(x) { 0 -> "zero" else -> "nonzero" } ``` 上面的写法比下面的代码要好: ```KOTLIN if (x) return foo() else return bar() ``` ```KOTLIN when(x) { 0 -> return "zero" else -> return "nonzero" } ``` ### if 和 when 对于二元的条件分支, 尽量使用 `if` 而不是 `when`. 比如, 这里应该用 `if`: ```KOTLIN if (x == null) ... else ... ``` 而不是用 `when`: ```KOTLIN when (x) { null -> // ... else -> // ... } ``` 如果存在三个或更多的条件分支, 尽量使用 `when`. ### when 表达式中的保护条件 在 `when` 表达式或语句中组合多个 boolean 表达式时, 要使用带括号的 [保护条件(Guard Condition)](control-flow.html#guard-conditions-in-when-expressions): ```KOTLIN when (status) { is Status.Ok if (status.info.isEmpty() || status.info.id == null) -> "no information" } ``` 而不要写成: ```KOTLIN when (status) { is Status.Ok if status.info.isEmpty() || status.info.id == null -> "no information" } ``` ### 在条件中使用可为 null 的 Boolean 值 如果需要在条件语句中使用可为空的 `Boolean`, 请使用 `if (value == true)` 或者 `if (value == false)` 进行判断. ### 循环 尽量使用高阶函数(`filter`, `map` 等等.) 来进行循环处理. 例外情况: `forEach` (应该尽量使用通常的 `for` 循环, 除非 `forEach` 函数的接受者对象可能为空, 或者 `forEach` 是一个很长的链式调用的一部分). 应该使用多个高阶函数组成的复杂表达式, 还是应该使用一个循环语句, 选择之前应该理解这两种操作各自的代价, 并且注意考虑性能问题. ### 在数值范围上循环 对于终端开放(open-ended)的值范围(不包含其末尾元素), 那么应该使用 `..<` 操作符进行循环: ```KOTLIN for (i in 0..n - 1) { /*...*/ } // 不好的风格 for (i in 0.. 1) { | return a |}""".trimMargin() println(a) //sampleEnd } ``` 详情请参见 [Java 与 Kotlin 的多行字符串的区别](java-to-kotlin-idioms-strings.html#use-multiline-strings). ### 函数 vs 属性 有些场景下, 无参数的函数可以与只读属性相互替代. 虽然它们在语义上是相似的, 但从编程风格上的角度看, 存在一些规约来决定在什么时候应该使用函数, 什么时候应该使用属性. 当底层算法满足以下条件时, 应该选择使用只读属性, 而不是使用函数: * 不会抛出异常. * 计算过程消费的资源不多(或者在初次运行时缓存了计算结果). * 对象状态没有发生变化时, 多次调用会返回相同的结果. ### 扩展函数 应该尽量多的使用扩展函数. 如果你的某个函数主要是为某个对象服务, 应该考虑将它转变为这个对象的一个扩展函数. 为了尽量减小 API 污染, 应该将扩展函数的可见度尽量限制在合理的程度. 如果需要, 尽量使用局部扩展函数, 成员扩展函数, 或者可见度为 private 的顶级扩展函数. ### 中缀函数 如果一个函数服务于两个参数, 而且这两个参数的角色很类似, 只有这种情况下才应该将函数声明为 `infix` 函数. 好的例子比如: `and`, `to`, `zip`. 坏的例子比如: `add`. 如果方法会变更它的接受者对象, 那么不应该将它声明为 `infix` 方法. ### 工厂函数 如果你为一个类声明一个工厂方法, 请不要使用与类相同的名称. 尽量使用一个不同的名称, 解释清楚工厂函数的行为有什么不同之处. 只有当工厂函数的确实不存在什么特殊意义的时候, 这时你才可以使用与类相同的名称作为函数名. ```KOTLIN class Point(val x: Double, val y: Double) { companion object { fun fromPolar(angle: Double, radius: Double) = Point(...) } } ``` 如果某个对象拥有多个不同参数的重载构造器, 这些构造器不会调用超类中的不同的构造器, 而且无法缩减成带默认值参数的单个构造器, 这时应该将这些构造器改为工厂函数. ### 平台数据类型 对于 public 的函数或方法, 如果返回一个平台类型的表达式, 那么应该明确声明它在 Kotlin 中的类型: ```KOTLIN fun apiCall(): String = MyJavaApi.getProperty("name") ``` (包级或者类级的)任何属性, 如果使用平台类型的表达式进行初始化, 那么应该明确声明它在 Kotlin 中的类型: ```KOTLIN class Person { val name: String = MyJavaApi.getProperty("name") } ``` 局部变量值, 如果使用平台类型的表达式进行初始化, 那么可以为它声明类型, 也可以省略: ```KOTLIN fun main() { val name = MyJavaApi.getProperty("name") println(name) } ``` ### 作用域函数(Scope Function): `apply`, `with`, `run`, `also`, `let` Kotlin 提供了一组函数, 用来在某个指定的对象上下文中执行一段代码, 这些函数包括: `let`, `run`, `with`, `apply`, 以及 `also`. 对于具体的问题, 应该如何选择正确的作用域函数, 详情请参见 [作用域函数(Scope Function)](scope-functions.html). ## 针对库开发的编码规约 开发库时, 为了保证 API 的稳定性, 建议还要遵守以下规约: * 始终明确指定成员的可见度 (以免不小心将某个声明暴露成 public API). * 始终明确指定函数的返回类型, 以及属性类型 (以免修改实现代码时, 不小心改变了返回类型). * 对所有的 public 成员编写 [KDoc](kotlin-doc.html) 文档注释 (这是为了对库生成文档), 例外情况是, 方法或属性的覆盖不需要提供新的注释. 关于为你的库编写 API 时的最佳实践, 以及需要考虑的问题, 请参见 [库开发者指南](api-guidelines-introduction.html). # 惯用法 本章介绍 Kotlin 中的一些常见的习惯用法. 如果你有自己的好的经验, 可以将它贡献给我们. 你可以将你的修正提交到 git, 并创建一个 Pull Request. ## 创建 DTO 类(或者叫 POJO/POCO 类) ```KOTLIN data class Customer(val name: String, val email: String) ``` 以上代码将创建一个 `Customer` 类, 其中包含以下功能: * 所有属性的 getter 函数(对于 `var` 型属性还有 setter 函数) * `equals()` 函数 * `hashCode()` 函数 * `toString()` 函数 * `copy()` 函数 * 所有属性的 `component1()`, `component2()`, ... 函数(参见 [数据类](data-classes.html)) ## 对函数参数指定默认值 ```KOTLIN fun foo(a: Int = 0, b: String = "") { ... } ``` ## 过滤 List 中的元素 ```KOTLIN val positives = list.filter { x -> x > 0 } ``` 甚至还可以写得更短: ```KOTLIN val positives = list.filter { it > 0 } ``` 详情请参见 [Java 与 Kotlin 过滤处理的区别](java-to-kotlin-collections-guide.html#filter-elements). ## 在集合中检查元素是否存在 ```KOTLIN if ("john@example.com" in emailsList) { ... } if ("jane@example.com" !in emailsList) { ... } ``` ## 在字符串内插入变量值 ```KOTLIN println("Name $name") ``` 详情请参见 [Java 与 Kotlin 字符串拼接处理的区别](java-to-kotlin-idioms-strings.html#concatenate-strings). ## 安全的读取标准输入 ```KOTLIN // 读取一个字符串, 如果输入不能转换为整数, 返回 null. 例如: Hi there! val wrongInt = readln().toIntOrNull() println(wrongInt) // 输出结果为 null // 读取一个能够转换为整数的字符串, 返回整数值. 例如: 13 val correctInt = readln().toIntOrNull() println(correctInt) // 输出结果为 13 ``` 详情请参见 [读取标准输入](read-standard-input.html). ## 类型实例检查 ```KOTLIN when (x) { is Foo -> ... is Bar -> ... else -> ... } ``` ## 只读 List ```KOTLIN val list = listOf("a", "b", "c") ``` ## 只读 Map ```KOTLIN val map = mapOf("a" to 1, "b" to 2, "c" to 3) ``` ## 访问 Map 中的条目 ```KOTLIN println(map["key"]) map["key"] = value ``` ## 使用成对变量来遍历 Map, 或遍历 Pair 组成的 List ```KOTLIN for ((k, v) in map) { println("$k -> $v") } ``` 上例中的 `k`, `v` 可以使用任何方便的变量名, 比如 `name` 和 `age`. ## 在数值范围中遍历 ```KOTLIN for (i in 1..100) { ... } // 终端封闭的(closed-ended)数值范围: 包括 100 for (i in 1..<100) { ... } // 终端开放的(open-ended)数值范围: 不包括 100 for (x in 2..10 step 2) { ... } for (x in 10 downTo 1) { ... } (1..10).forEach { ... } ``` ## 延迟计算(Lazy)属性 ```KOTLIN val p: String by lazy { // 只在第一次访问时计算属性值 // 在这里计算字符串值 } ``` ## 扩展函数 ```KOTLIN fun String.spaceToCamelCase() { ... } "Convert this to camelcase".spaceToCamelCase() ``` ## 创建单例(Singleton) ```KOTLIN object Resource { val name = "Name" } ``` ## 使用内联的值类(Inline value class) 创建类型安全的值 ```KOTLIN @JvmInline value class EmployeeId(private val id: String) @JvmInline value class CustomerId(private val id: String) ``` 如果你不小心混淆了 `EmployeeId` 和 `CustomerId`, 会发生编译错误. Note: 只对 JVM 后端需要 `@JvmInline` 注解. ## 为抽象类(Abstract Class)创建实例 ```KOTLIN abstract class MyAbstractClass { abstract fun doSomething() abstract fun sleep() } fun main() { val myObject = object : MyAbstractClass() { override fun doSomething() { // ... } override fun sleep() { // ... } } myObject.doSomething() } ``` ## If not null 的简写表达方式 ```KOTLIN val files = File("Test").listFiles() println(files?.size) // 如果 files 不为 null, 这里会打印 size 值 ``` ## If-not-null-else 的简写表达方式 ```KOTLIN val files = File("Test").listFiles() // 简单的 fallback 值: println(files?.size ?: "empty") // 如果 files 为 null, 这里会打印 "empty" // 如果要通过一个代码段来计算更加复杂的 fallback 值, 可以使用 `run` val filesSize = files?.size ?: run { val someSize = getSomeSize() someSize * 2 } println(filesSize) ``` ## 当值为 null 时, 执行某个表达式 ```KOTLIN val values = ... val email = values["email"] ?: throw IllegalStateException("Email is missing!") ``` ## 从可能为空的集合中取得第一个元素 ```KOTLIN val emails = ... // 可能为空 val mainEmail = emails.firstOrNull() ?: "" ``` 详情请参见 [Java 与 Kotlin 集合第一个元素的获取方法的区别](java-to-kotlin-collections-guide.html#get-the-first-and-the-last-items-of-a-possibly-empty-collection). ## 当值不为 null 时, 执行某个语句 ```KOTLIN val value = ... value?.let { ... // 这个代码段将在 data 不为 null 时执行 } ``` ## 当值不为 null 时, 进行映射变换 ```KOTLIN val value = ... val mapped = value?.let { transformValue(it) } ?: defaultValue // 如果 value 为 null, 会 transform 处理结果为 null, 则返回 defaultValue ``` ## 在函数的 return 语句中使用 when 语句 ```KOTLIN fun transform(color: String): Int { return when (color) { "Red" -> 0 "Green" -> 1 "Blue" -> 2 else -> throw IllegalArgumentException("Invalid color param value") } } ``` ## 将 try-catch 用作一个表达式 ```KOTLIN fun test() { val result = try { count() } catch (e: ArithmeticException) { throw IllegalStateException(e) } // 使用 result } ``` ## 将 if 用作一个表达式 ```KOTLIN fun foo(param: Int) { val result = if (param == 1) { "one" } else if (param == 2) { "two" } else { "three" } } ``` ## 返回值为 Unit 类型的多个方法, 可以通过 Builder 风格的方式来串联调用 ```KOTLIN fun arrayOfMinusOnes(size: Int): IntArray { return IntArray(size).apply { fill(-1) } } ``` ## 使用单个表达式来定义一个函数 ```KOTLIN fun theAnswer() = 42 ``` 以上代码等价于: ```KOTLIN fun theAnswer(): Int { return 42 } ``` 这种用法与其他惯用法有效地结合起来, 可以编写出更简短的代码. 比如. 可以与 `when` 表达式结合起来: ```KOTLIN fun transform(color: String): Int = when (color) { "Red" -> 0 "Green" -> 1 "Blue" -> 2 else -> throw IllegalArgumentException("Invalid color param value") } ``` ## 在同一个对象实例上调用多个方法(with 函数) ```KOTLIN class Turtle { fun penDown() fun penUp() fun turn(degrees: Double) fun forward(pixels: Double) } val myTurtle = Turtle() with(myTurtle) { // 描绘一个边长 100 像素的正方形 penDown() for (i in 1..4) { forward(100.0) turn(90.0) } penUp() } ``` ## 配置对象属性 (apply 函数) ```KOTLIN val myRectangle = Rectangle().apply { length = 4 breadth = 5 color = 0xFAFAFA } ``` 这种方法可以非常方便地配置对象构造函数参数以外的那些属性. ## 类似 Java 7 中针对资源的 try 语句 ```KOTLIN val stream = Files.newInputStream(Paths.get("/some/file.txt")) stream.buffered().reader().use { reader -> println(reader.readText()) } ``` ## 需要泛型类型信息的泛型函数 ```KOTLIN // public final class Gson { // ... // public T fromJson(JsonElement json, Class classOfT) throws JsonSyntaxException { // ... inline fun Gson.fromJson(json: JsonElement): T = this.fromJson(json, T::class.java) ``` ## 交换两个变量的值 ```KOTLIN var a = 1 var b = 2 a = b.also { b = a } ``` ## 将代码标记为未完成 (TODO) Kotlin 标准库有一个 `TODO()` 函数, 它永远会抛出一个 `NotImplementedError`. 这个函数的返回值是 `Nothing`, 因此无论代码中需要的返回类型是什么, 都可以使用这个函数. 这个函数还有一个参数重载(overload)的版本, 接受一个参数, 用来解释具体的原因: ```KOTLIN fun calcTaxes(): BigDecimal = TODO("Waiting for feedback from accounting") ``` IntelliJ IDEA 的 Kotlin 插件能够理解 `TODO()` 函数的意义, 并会在 TODO 工具窗口中自动添加一条 TODO 项. ## 下一步做什么? * 使用 Kotlin 的编程风格来解决 [Advent of Code 谜题](advent-of-code.html). * 学习如何执行 [Java 与 Kotlin 中常见的字符串处理任务](java-to-kotlin-idioms-strings.html). * 学习如何执行 [Java 与 Kotlin 中常见的集合(Collection)处理任务](java-to-kotlin-collections-guide.html). * 学习如何 [在 Java 与 Kotlin 中处理可空性(Nullability)](java-to-kotlin-nullability-guide.html). # 类型概述 在 Kotlin 中, 一切都是对象, 这就意味着, 你可以对任何变量访问它的成员函数和属性. 有些数据类型使用优化过的内部表现形式, 在运行时使用 Java 的基本类型(Primitive Value)来表达, (比如, 数值, 字符, 以及布尔值), 但对于使用者来说, 它们就和通常的类一样. 本章介绍 Kotlin 中使用的基本类型: * [数值](numbers.html) 以及对应的 [无符号数值](unsigned-integer-types.html) * [布尔值](booleans.html) * [字符](characters.html) * [字符串](strings.html) * [数组](arrays.html) 关于 Kotlin 的其他类型, 比如 `Nothing`, `Any`, 和 `Unit`, 请阅读 Kotlin API 参考文档: * [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/) * [Nothing](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-nothing.html) * [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/) Tip: 参见 [在 Kotlin 中如何进行类型检查和类型转换](typecasts.html). # 数值类型 Kotlin 的数值类型表示: * 整数值 ([Byte](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-byte/), [Short](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-short/), [Int](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-int/), 以及 [Long](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-long/)) * 浮点值 ([Float](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-float/) 以及 [Double](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-double/)) 使用数值类型来存储和处理数值数据, 例如算术运算, 计数器, 测量值, 以及其他计算. ## 选择数值类型 在大多数情况下, 可以参考以下规则, 为你的任务确定正确的数值类型: * 对整数使用 `Int`. * 对超出 `Int` 范围的整数使用 `Long`. * 对小数使用 `Double`. * 当可以接受或需要较低精度时使用 `Float`. * 当 API 或数据格式要求时使用 `Byte` 和 `Short`. Tip: Kotlin 还提供了 Beta 功能的 [无符号整数类型](unsigned-integer-types.html). ## 整数类型 Kotlin 提供了 4 种整数类型, 有着不同的大小和数值范围: | 类型 |大小(bits) |最小值 |最大值 | -------------------------- | `Byte` |8 |-128 |127 | | `Short` |16 |-32768 |32767 | | `Int` |32 |-2,147,483,648 (-231) |2,147,483,647 (231 - 1) | | `Long` |64 |-9,223,372,036,854,775,808 (-263) |9,223,372,036,854,775,807 (263 - 1) | ### 声明整数值 Kotlin 对整数值支持以下字面值(literal)形式: * 10 进制数: `123` * 16 进制数: `0x0F` * 2 进制数: `0b00001011` Note: Kotlin 不支持 8 进制数的字面值. 要声明一个数值, 请明确指定类型: ```KOTLIN val one: Int = 1 // 使用下划线提高可读性 val oneBillion: Long = 1_000_000_000 val hexBytes: Int = 0x7F_EC_DE_5E val bytes: Int = 0b01010010_01101001_10010100_10010010 val oneByte: Byte = 1 val oneShort: Short = 1 ``` 也可以添加 `L` 后缀, 声明一个 `Long` 类型的值: ```KOTLIN val oneLong = 1L ``` 当你明确声明数值类型时, 编译器会检查值是否在该类型的范围内: ```KOTLIN // 值在 Byte 范围内 val oneByte: Byte = 1 // 错误: 值不在 Byte 范围内 val tooBig: Byte = 128 ``` 当你没有指定数值类型时, 如果值在 `Int` 范围内, Kotlin 推断类型为 `Int`. 否则推断类型为 `Long`: ```KOTLIN val million = 1_000_000 // Int 类型 val threeBillion = 3_000_000_000 // Long 类型 ``` 如果值可以为空, 请使用可为 null 的类型: ```KOTLIN val maybeAbsent: Int? = null ``` ## 浮点类型 对于带小数部分的数值, Kotlin 提供了 `Float` 和 `Double` 类型. 浮点类型遵循 [IEEE 754 标准](https://en.wikipedia.org/wiki/IEEE_754). `Float` 代表 单精度(Single Precision). `Double` 代表 双精度(Double Precision). 浮点类型的大小和精度有所不同: | 类型 |大小(bits) |有效位数 |指数位数 |十进制位数 | ----------------------------------- | `Float` |32 |24 |8 |6-7 | | `Double` |64 |53 |11 |15-16 | ### 声明浮点值 要声明浮点数字面值, 请包含小数点 (`.`), 或使用指数表示法: ```KOTLIN val pi = 3.14 val avogadro = 6.02214076e23 ``` 默认情况下, Kotlin 将浮点数字面值推断为 `Double`. 要声明 `Float`, 请添加 `f` 或 `F` 后缀: ```KOTLIN val pi = 3.14 // Double 类型 val eFloat = 2.7182817f // Float 类型 ``` Note: 如果 `Float` 字面值包含的精度超过 `Float` 所能存储的范围, Kotlin 会对其进行舍入. 如果值可以为空, 请使用可为 null 的类型: ```KOTLIN val maybeAbsent: Double? = null ``` ## 算术运算 Kotlin 对数值支持标准的算术运算: `+`, `-`, `*`, `/`, 以及 `%`. 使用这些运算符来进行常见的计算: ```KOTLIN fun main() { //sampleStart println(1 + 2) // 输出结果为: 3 println(2_500_000_000L - 1L) // 输出结果为: 2499999999 println(3.14 * 2.71) // 输出结果为: 8.5094 println(10.0 / 3) // 输出结果为: 3.3333333333333335 //sampleEnd } ``` 结果类型取决于操作数的类型. 详情请参见 [混合数值表达式](#mixed-numeric-expressions). Tip: 你可以在自定义数值类中重载这些运算符. 详情请参见 [运算符重载(Operator overloading)](operator-overloading.html). ### 整数除法 整数值之间的除法返回的永远是整数结果. 编译器会舍弃小数部分: ```KOTLIN fun main() { //sampleStart val intValue = 5 / 2 println(intValue) // 输出结果为: 2 val longValue = 5L / 2 println(longValue) // 输出结果为: 2 //sampleEnd } ``` 要返回浮点结果, 请将至少一个操作数转换为 `Float` 或 `Double`: ```KOTLIN fun main() { //sampleStart val a = 5 / 2.0 println(a) // 输出结果为: 2.5 val b = 5 / 2.toDouble() println(b) // 输出结果为: 2.5 //sampleEnd } ``` ## 类型转换 数值类型互相之间不是子类型(subtype). Kotlin 要求明确的转换, 以免在不知情的情况下丢失数据, 或发生预想之外的行为. 例如, 期望 `Double` 类型的函数, 如果不进行转换, 不能接受 `Int` 或 `Float` 值: ```KOTLIN fun main() { //sampleStart fun printDouble(x: Double) { print(x) } val x = 1.0 val xInt = 1 val xFloat = 1.0f val one: Double = 1 // 错误: 初始值类型不匹配 printDouble(x) // OK printDouble(xInt) // 错误: 参数类型不匹配 printDouble(xFloat) // 错误: 参数类型不匹配 //sampleEnd } ``` 所有的数值类型都支持转换为其他数值类型. 要将数值转换为其他类型, 请使用明确转换的函数: * `toByte()` * `toShort()` * `toInt()` * `toLong()` * `toFloat()` * `toDouble()` 例如, 以下代码将 `Int` 值转换为 `Double`: ```KOTLIN fun main() { //sampleStart val intValue: Int = 1 val doubleValue = intValue.toDouble() println(doubleValue) // 输出结果为: 1.0 //sampleEnd } ``` 将浮点值转换为整数类型时, 编译器会舍弃小数部分: ```KOTLIN fun main() { //sampleStart val d: Double = 1.5 val l: Long = d.toLong() println(l) // 输出结果为: 1 //sampleEnd } ``` ### 混合数值表达式 Kotlin 不支持对赋值语句或函数参数进行隐式类型转换. 但是, 你可以在算术表达式中组合不同的数值类型. 在这种情况下, Kotlin 根据操作数类型确定结果类型, 算术运算符会自动处理转换: ```KOTLIN val intNumber: Int = 1 val longNumber: Long = 1000 val result = intNumber + longNumber // 结果为: 1001, Long 类型 ``` 如果你尝试将结果赋给较小的类型, 编译器会报告错误: ```KOTLIN val intNumber: Int = 1 val longNumber: Long = 1000 val result: Int = intNumber + longNumber // 错误: 初始值类型不匹配 ``` ## 数据溢出 数值类型只能表示其定义范围内的值. 如果运算结果超出该范围, 就会发生溢出. 如果将值转换为较小的数值类型, 转换后的值可能无法保留原来的数值. 即使编译器允许这样的代码, 这种行为也可能影响你的代码的运行结果. ### 运算中的溢出 每种整数类型只能存储其定义范围内的值. 当算术运算的结果超过该范围时, 就会发生_数据溢出_: ```KOTLIN fun main(){ //sampleStart val intNumber: Int = 2147483647 // Int 的最大值是 2147483647 println(intNumber + 1) // 输出结果为: -2147483648 //sampleEnd } ``` 这里, 结果发生了回绕(wrap around), 因为值超过了 `Int` 的表达范围. Note: 当发生整数溢出时, 编译器不会自动产生错误. ### 取反中的溢出 取反操作也可能发生溢出. 例如, `Int.MIN_VALUE` 的正数对应值, 无法用 `Int` 表示. ```KOTLIN fun main(){ //sampleStart val min = Int.MIN_VALUE println(-min) // 输出结果为: -2147483648 //sampleEnd } ``` ### 窄化转换 当你将值转换为较小的整数类型时, 结果可能无法保留原来的数值: ```KOTLIN fun main() { //sampleStart val large: Int = 130 val narrowed: Byte = large.toByte() println(narrowed) // 输出结果为: -126 //sampleEnd } ``` 但是, 由于浮点类型遵循 [IEEE 754 标准](https://en.wikipedia.org/wiki/IEEE_754), 非常大的结果可能变为 `Infinity`: ```KOTLIN fun main() { //sampleStart println(Double.MAX_VALUE * 2) // 输出结果为: Infinity //sampleEnd } ``` ## 位运算 Kotlin 为 `Int` 和 `Long` 提供了 位运算. 这些运算由一组 [中缀函数](functions.html#infix-notation) 和 `inv()` 表示. ```KOTLIN fun main() { //sampleStart val x = 1 println(x shl 2) // 输出结果为: 4 println(x and 0x000FF000) // 输出结果为: 0 //sampleEnd } ``` 位运算包括: * `shl()` – 带符号左移 * `shr()` – 带符号右移 * `ushr()` – 无符号右移 * `and()` – 按位与(AND) * `or()` – 按位或(OR) * `xor()` – 按位异或(XOR) * `inv()` – 按位取反 ## 浮点值的比较 在 Kotlin 中, 浮点数的比较取决于操作数的静态类型. 当操作数静态的判定为 `Float` 或 `Double` 类型时, 对这些数值的操作以及由它们构成的范围, 将遵循 [IEEE 754 浮点数值运算标准](https://en.wikipedia.org/wiki/IEEE_754). 但是, 在使用泛型的情况下 (例如 `Any`, `Comparable<...>`, 或 `Collection`), 对于没有静态的判定为浮点值类型的操作数, 行为有所不同. 在这种情况下, Kotlin 使用 `Float` 和 `Double` 的 `equals()` 和 `compareTo()` 实现. 因此判定结果是: * `NaN` 会被判定为等于它自己 * `NaN` 会被判定为大于任何其他数值, 包括正无穷大(`POSITIVE_INFINITY`) * `-0.0` 会被判定为小于 `0.0` 以下示例演示静态的判定为浮点值类型的操作数, 与通过泛型类型使用的操作数之间的差别: ```KOTLIN //sampleStart fun generalizedEquals(a: Any, b: Any): Boolean { return a == b } fun main() { // 操作数静态的判定为浮点值类型 println(Double.NaN == Double.NaN) // 输出结果为: false println(0.0 == -0.0) // 输出结果为: true // 操作数通过非浮点静态类型使用 println(generalizedEquals(Double.NaN, Double.NaN)) // 输出结果为: true println(generalizedEquals(0.0, -0.0)) // 输出结果为: false } //sampleEnd ``` ## JVM 上数值的装箱(Box)和缓存 在 JVM 平台, 非 null 的数值通常使用基本类型存储, 例如 `int`, `long`, 或 `double`. 但是, 当你使用 [泛型](generics.html), 或可为 null 的数值类型(例如 `Int?`) 时, 值会被装箱(box), 并以对象的形式表示. JVM 通过缓存小数值的装箱表示, 使用一种 [内存优化技术](https://docs.oracle.com/javase/specs/jls/se22/html/jls-5.html#jls-5.1.7). 因此, 具有相同值的装箱数值, 可以是 [引用相等的](equality.html#referential-equality). 例如, JVM 缓存了 `-128` 到 `127` 范围内的装箱 `Integer` 值. 因此, 以下代码返回 `true`: ```KOTLIN fun main() { //sampleStart val score: Int = 100 val savedScore: Int? = score val displayedScore: Int? = score println(savedScore === displayedScore) // 输出结果为: true //sampleEnd } ``` 对于缓存范围之外的值, 装箱值是不同的对象. 在这种情况下, 即使它们的值 [结构相等](equality.html#structural-equality), 它们也不是引用相等的. 因此, 请使用 `==` 来比较数值: ```KOTLIN fun main() { //sampleStart val score: Int = 10000 val savedScore: Int? = score val displayedScore: Int? = score println(savedScore === displayedScore) // 输出结果为: false println(savedScore == displayedScore) // 输出结果为: true //sampleEnd } ``` # 无符号整数(Unsigned Integer)类型 除 [整数类型](numbers.html#integer-types) 外, Kotlin 还提供了以下无符号整数类型: | 类型 |大小 (位) |最小值 |最大值 | ------------------------ | `UByte` |8 位 |0 |255 | | `UShort` |16 位 |0 |65,535 | | `UInt` |32 位 |0 |4,294,967,295 (232 - 1) | | `ULong` |64 位 |0 |18,446,744,073,709,551,615 (264 - 1) | 无符号整数支持有符号整数的大多数运算符. Note: 无符号数值以 [内联类](inline-classes.html) 的方式实现, 内部存储属性包含对应的同等宽度的有符号数值类型. 如果你想要在无符号和有符号的整数类型之间转换, 请确认更新了你的代码, 让所有的函数调用和操作都支持新的类型. ## 无符号整数的数组和值范围 Warning: 无符号整数的数组以及对这些数组的操作目前处于 [Beta](components-stability.html) 状态. 随时可能发生不兼容的变化. 使用时需要明确同意(Opt-in)(详情请参见下文). 与基本类型相同, 每一种无符号整数类型都有一个对应的类来表示由它构成的数组: * `UByteArray`: 无符号 byte 构成的数组. * `UShortArray`: 无符号 short 构成的数组. * `UIntArray`: 无符号 int 构成的数组. * `ULongArray`: 无符号 long 构成的数组. 与有符号的整数数组类类似, 这些无符号整数的数组类提供了与 `Array` 类相似的 API, 并且不会产生数值对象装箱带来的性能损耗. 使用无符号整数数组时, 会出现编译警告, 表示这个功能还未达到稳定状态. 要消除这个警告, 请使用 `@ExperimentalUnsignedTypes` 注解, 标注使用者同意(Opt-in). 你的代码的使用者是否也需要明确同意使用你的 API, 这一点由你来决定, 但请注意, 无符号整数数组还不是稳定的功能, 因此由于语言本身的变化, 使用它们的 API 可能会出现错误. 详情请参见 [明确要求使用者同意的功能(Opt-in Requirement)](opt-in-requirements.html). 为了支持 `UInt` 和 `ULong` 类型的 [值范围与数列](ranges.html) 功能, 还提供了 `UIntRange`, `UIntProgression`, `ULongRange`, `ULongProgression` 类. ## 无符号整数的字面值(literal) 为了无符号整数使用的便利, 你可以在整数字面值上添加后缀, 来标记特定的无符号类型 (与使用 `F` 后缀标记 `Float` 类型, 或使用 `L` 后缀标记 `Long` 类型的方式类似): * `u` 和 `U` 字母用于标记无符号整数, 但不指明具体的无符号整数类型. 如果未指定期待的数据类型, 编译器会根据整数值的大小来决定使用 `UInt` 或 `ULong`: ```KOTLIN val b: UByte = 1u // 字面值类型为 UByte, 因为程序指定了期待的数据类型 val s: UShort = 1u // 字面值类型为 UShort, 因为程序指定了期待的数据类型 val l: ULong = 1u // 字面值类型为 ULong, 因为程序指定了期待的数据类型 val a1 = 42u // 字面值类型为 UInt: 因为程序未指定期待的数据类型, 而且整数值可以存入 UInt 内 val a2 = 0xFFFF_FFFF_FFFFu // 字面值类型为 ULong: 因为程序未指定期待的数据类型, 而且整数值无法存入 UInt 内 ``` * `uL` 和 `UL` 表示字面值为无符号的 Long: ```KOTLIN val a = 1UL // 字面值类型为 ULong, 即使这里未指定期待的数据类型, 而且整数值可以存入 UInt 内 ``` ## 使用场景 无符号数值的主要使用场景, 是利用整数的完整的二进制范围来表达正的数值. 比如, 要表达一个无法在有符号类型范围内表达的 16 进制常数, 例如 32 位 `AARRGGBB` 格式的颜色值: ```KOTLIN data class Color(val representation: UInt) val yellow = Color(0xFFCC00CCu) ``` 你可以使用无符号数值来初始化字节数组, 而不需要明确的 `toByte()` 字面值转换: ```KOTLIN val byteOrderMarkUtf8 = ubyteArrayOf(0xEFu, 0xBBu, 0xBFu) ``` 另一种使用场景是与原生 API 交互. Kotlin 允许表达在方法签名中包含无符号类型的原生声明. 方法映射不会用有符号整数代替无符号整数, 保持语义无变化. ### 不适合的场景 尽管无符号整数只能表达正的数值或 0, 但在应用程序的业务逻辑中要求非负整数的情况下, 并不适合使用无符号整数. 例如, 用作集合大小或集合下标值的数据类型. 原因如下: * 使用有符号的整数有助于发现数值溢出的异常情况, 以及标记错误条件, 比如 [List.lastIndex](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/last-index.html) 对空的 List 返回结果是 -1. * 无符号整数不能用作有符号整数的限定范围版本, 因为它们的值范围不是有符号整数值范围的子集. 有符号整数, 和无符号整数, 相互之间都不是子类型. # 布尔(Boolean)类型 [Boolean](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-boolean/) 类型表示逻辑值: `true` 和 `false`. `Boolean` 值可以用在需要回答是/否问题的函数中, 以及用在 `while`, `if`, 和 `when` 的条件表达式中. ## 声明 `Boolean` 变量 要声明一个 `Boolean` 变量, 请将 `true` 或 `false` 赋值给它. 可以明确指定 `Boolean` 类型, 也可以让 Kotlin 从值中推断类型: ```KOTLIN val isTrue: Boolean = true val isFalse = false // Kotlin 推断类型为 Boolean ``` 如果值可以为 `null`, 请使用 `Boolean?`: ```KOTLIN val isEnabled: Boolean? = null ``` Note: 不能将整数值赋给 `Boolean` 变量. 在 Kotlin 中, `0` 和 `1` 不是 `Boolean` 值. ## 生成 `Boolean` 值 可以使用比较表达式和函数来生成 `Boolean` 值: ```KOTLIN fun main() { //sampleStart val number = 10 val isPositive = number > 0 println(isPositive) // 输出结果为: true val language = "Kotlin" val isEmpty = language.isEmpty() println(isEmpty) // 输出结果为: false //sampleEnd } ``` 也可以在条件表达式和其他表达式中使用这些结果: ```KOTLIN fun main() { //sampleStart val number = 10 val isPositive = number > 0 // 结果为 true if (isPositive) { println("The number is positive.") } //sampleEnd } ``` ## `Boolean` 运算 Kotlin 提供了运算符和中缀函数来处理 `Boolean` 值. 可以使用它们对 `Boolean` 值取反, 或将多个 `Boolean` 值合并为一个结果. ### 取反(NOT) NOT 运算符对 `Boolean` 值取反. 要使用 NOT, 请在 `Boolean` 值前面放置 `!` 运算符: ```KOTLIN val isOn = true val isOff = !isOn // isOff 为 false ``` ### 逻辑与(AND) AND 运算符仅在两个操作数都为 `true` 时返回 `true`. 要使用逻辑与, 请在操作数之间放置 `&&` 运算符: ```KOTLIN val a = false && false // 结果为 false val b = false && true // 结果为 false val c = true && false // 结果为 false val d = true && true // 结果为 true ``` Note: 如果第一个操作数为 `false`, `&&` 运算符会跳过第二个操作数. 要对两个操作数都求值, 请改用 `and` [中缀函数](functions.html#infix-notation). ### 逻辑或(OR) OR 运算符在至少一个操作数为 `true` 时返回 `true`. 要使用逻辑或, 请在操作数之间放置 `||` 运算符: ```KOTLIN val a = false || false // 结果为 false val b = false || true // 结果为 true val c = true || false // 结果为 true val d = true || true // 结果为 true ``` Note: 如果第一个操作数为 `true`, `||` 运算符会跳过第二个操作数. 要对两个操作数都求值, 请改用 `or` [中缀函数](functions.html#infix-notation). ### 异或(XOR) 异或(XOR)运算在两个操作数值不相同时返回 `true`. 要使用 XOR, 请在操作数之间写 `xor`: ```KOTLIN val a = false xor false // 结果为 false val b = false xor true // 结果为 true val c = true xor false // 结果为 true val d = true xor true // 结果为 false ``` Note: `xor` 是一个 [中缀函数](functions.html#infix-notation), 不是运算符. 关于 `Boolean` 函数, 详情请参见 [API 参考文档](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-boolean/). ## 运算符优先级 如果一个表达式包含多个逻辑运算, 而且没有括号来指定求值顺序, Kotlin 会使用优先级规则. 优先级较高的运算会在优先级较低的运算之前求值. 对于本节描述的 `Boolean` 运算, 优先级顺序如下: 1. `!` 2. `xor` (以及其他中缀函数) 3. `&&` 4. `||` 在下面的示例中, 编译器先对 `&&` 求值, 再对 `||` 求值: ```KOTLIN fun main() { //sampleStart val result = true || false && false println(result) // 输出结果为: true //sampleEnd } ``` 要明确指定求值顺序, 请使用括号: ```KOTLIN fun main() { //sampleStart val result = (true || false) && false println(result) // 输出结果为: false //sampleEnd } ``` ## 在条件表达式中使用 `Boolean` [if](control-flow.html#if-expression), [when](control-flow.html#when-expressions-and-statements), 和 [while](control-flow.html#while-loops) 通过对 `Boolean` 表达式求值来控制程序流程. ### `if` 表达式 ```KOTLIN fun main() { //sampleStart val number = 4 val isEven = number % 2 == 0 // 条件已经是 `Boolean` 类型 // 不需要与 `true` 或 `false` 进行比较 if (isEven) { println("The number is even.") } else { println("The number is odd.") } //sampleEnd } ``` ### `when` 表达式 ```KOTLIN fun main() { //sampleStart val number = 3 when { number > 0 -> println("The number is positive.") number < 0 -> println("The number is negative.") else -> println("The number is zero.") } //sampleEnd } ``` ### `while` 循环 ```KOTLIN fun main() { //sampleStart var isCalculating = true while (isCalculating) { println("Calculating...") isCalculating = false } //sampleEnd } ``` # 字符 [Char](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-char/) 类型使用 1 个 UTF-16 码元(Code Unit), 表示单个字符. `Char` 用于表示单个字符值, 例如字母, 数字, 标点符号, 或空白字符. 对于字符序列, 请使用 [String](strings.html). Tip: `Char` 不是数值类型, 但每个字符都有一个数值型的 Unicode 值, 可以读取这个值. 详情请参见 [字符转换](#character-conversion). ## 语法 要声明一个字符, 请将值用单引号 (`' '`) 括起. 可以显式指定 `Char` 类型, 也可以让 Kotlin 从值中推断类型: ```KOTLIN val letter: Char = 'a' // Kotlin 推断类型为 Char, 因为值用单引号表达 val digit = '1' val symbol = '!' val space = ' ' val separator = ':' ``` 字符字面值(literal)必须包含恰好一个字符. 否则 Kotlin 编译器会报告错误: ```KOTLIN val invalid = 'AB' // 错误 val invalidEmpty = '' // 错误 ``` ### 可为 null 的值 要存储可为 null 的值, 请使用 `Char?`: ```KOTLIN val maybeAbsent: Char? = null ``` Note: 在 JVM 平台, 可为 null 的 `Char` 值会在需要的时候被装箱(box). 与 [数值类型](numbers.html#boxing-and-caching-numbers-on-the-jvm) 相同. ## Unicode 支持 Kotlin 将 `Char` 值表示为 UTF-16 码元(Code Unit). 也就是说, 单个 `Char` 存储 1 个 UTF-16 码元(Code Unit), 不一定是一个完整的 Unicode 字符. ### 基本多语言平面(Basic Multilingual Plane) 单个 `Char` 能够存储从 `\u0000` 到 `\uFFFF` 范围内的值. 这个范围覆盖了基本多语言平面(Basic Multilingual Plane, BMP), 包括几乎所有现代语言的字符, 以及大量的符号. 要通过 Unicode 值指定字符, 请使用 `\u`, 加上来自 [Unicode 表](https://www.unicode.org/charts/) 的 4 位 16 进制值: ```KOTLIN val unicodeNumber = '\u0031' // 等于 '1' ``` ### 补充字符 基本多语言平面(Basic Multilingual Plane) 范围之外的 Unicode 字符, 例如表情符号和一些历史文字, 无法用单个 `Char` 表示. 在 UTF-16 中, 它们被编码为 代理对(Surrogate Pair), 也就是在 `String` 中, 使用 2 个 `Char` 值, 共同表示 1 个 Unicode 字符: ```KOTLIN fun main() { //sampleStart val emoji = "🥦" println(emoji.length) // 输出结果为: 2 println(emoji[0]) // 输出结果为: 第 1 个代理字符 println(emoji[1]) // 输出结果为: 第 2 个代理字符 //sampleEnd } ``` Tip: 要单独处理 32 位符号, 请使用存储为 `Int` 值的 Unicode 码位(Code Point). ## 转义序列 对于难以直接在源代码中写出, 或具有特殊含义的特殊字符, 请使用转义序列. 每个转义序列以反斜线 (`\`) 开头. | 支持的转义序列 |描述 | --------------- | `\t` |制表符(Tab) | | `\b` |退格(Backspace) | | `\n` |换行(New Line, LF) | | `\r` |回车(Carriage Return, CR) | | `\'` |单引号(`'`) | | `\"` |双引号(`"`) | | `\\` |反斜线(`\`) | | `\$` |美元符号(`$`) | 例如: ```KOTLIN val newLine = '\n' val dollar = '\$' val backslash = '\\' ``` ## 操作 `Char` 支持比较, 检查, 大小写转换, 以及显式数值转换. ### 字符比较 要比较 `Char` 值, 请使用标准的 [比较运算符](keyword-reference.html#operators-and-special-symbols), 例如 `==`, `!=`, `<`, `>`, `<=` 和 `>=`. Kotlin 按照字符的数值型 Unicode 值进行比较, 并返回 `Boolean` 值: ```KOTLIN val before = 'a' < 'b' // 结果为: true val after = 'c' > 'd' // 结果为: false val different = 'A' == 'a' // 结果为: false val equal = 'A' == 'A' // 结果为: true ``` ### 字符处理 Kotlin 提供了用于字符值检查和大小写转换的函数. 例如: ```KOTLIN fun main() { //sampleStart val myChar = 'A' // 检查字符是否表示数字 println(myChar.isDigit()) // 输出结果为: false // 检查字符是否表示大写字母 println(myChar.isUpperCase()) // 输出结果为: true // 返回小写版本 println(myChar.lowercaseChar()) // 输出结果为: 'a' //sampleEnd } ``` Note: 更多可用的函数, 请参见 [API 参考文档](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-char/). ### 字符的算术运算 可以对字符加减一个整数值, 来得到另一个字符值: ```KOTLIN fun main() { //sampleStart val a = 'a' println(a + 1) // 输出结果为: b println(a + 2) // 输出结果为: c println(a - 32) // 输出结果为: A //sampleEnd } ``` Note: 这些操作遵循 Unicode 值, 而非特定语言的字母规则. 对可变变量, 也可以使用递增 (`++`) 和递减 (`--`) 运算符的前缀和后缀形式: ```KOTLIN fun main() { //sampleStart var a = 'A' a += 10 println(a) // 输出结果为: 'K' println(++a) // 输出结果为: 'L' 前缀递增 println(a++) // 输出结果为: 'L' 后缀递增 println(a) // 输出结果为: 'M' println(--a) // 输出结果为: 'L' 前缀递减 println(a--) // 输出结果为: 'L' 后缀递减 println(a) // 输出结果为: 'K' //sampleEnd } ``` ### 字符转换 要将 `Char` 转换为数值类型, 请使用显式转换: * 使用 `.code` 得到字符的数值型 Unicode 值: ```KOTLIN fun main() { //sampleStart val letter = 'A' println(letter.code) // 输出结果为: 65 //sampleEnd } ``` * 如果字符表示 10 进制数字, 请使用 `digitToInt()`: ```KOTLIN fun main() { //sampleStart val digit = '7' println(digit.digitToInt()) // 输出结果为: 7 //sampleEnd } ``` Tip: 如果字符有可能不是有效的数字, 请使用 `digitToIntOrNull()`. # 字符串 Kotlin 中的字符串由 [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/) 类型表达. Note: 在 JVM 平台, 使用 UTF-16 编码的 `String` 类型的对象, 大约使用每字符 2 个字节. 一般来说, 字符串值是一系列字符, 用双引号(`"`)括起: ```KOTLIN val str = "abcd 123" ``` 字符串中的元素是字符, 你可以通过下标操作符来访问: `s[i]`. 你可以使用 `for` 循环来遍历这些字符: ```KOTLIN fun main() { val str = "abcd" //sampleStart for (c in str) { println(c) } //sampleEnd } ``` 字符串是不可变的. 一旦初始化之后, 将不能改变它的值, 也不能为它赋予一个新的值. 所有改变字符串内容的操作, 返回值都是新的 `String` 对象, 而操作对象的原字符串不会改变: ```KOTLIN fun main() { //sampleStart val str = "abcd" // 创建一个新的 String 对象, 并打印 println(str.uppercase()) // 输出结果为: ABCD // 原字符串保持原来的值不变 println(str) // 输出结果为: abcd //sampleEnd } ``` 要拼接字符串, 可以使用 `+` 操作符. 这个操作符也可以将字符串与其他数据类型的值拼接起来, 只要表达式中的第一个元素是字符串类型: ```KOTLIN fun main() { //sampleStart val s = "abc" + 1 println(s + "def") // 输出结果为: abc1def //sampleEnd } ``` Note: 大多数情况下, 字符串拼接处理应该使用 [字符串模板](#string-templates) 或 [多行字符串(Multiline String)](#multiline-strings). ## 字符串的字面值(literal) Kotlin 中存在两种字符串字面值: * [转义(Escaped)字符串](#escaped-strings) * [多行(Multiline)字符串](#multiline-strings) ### 转义(Escaped)字符串 转义(Escaped)字符串 可以包含转义字符. 转义字符串的示例如下: ```KOTLIN val s = "Hello, world!\n" ``` 转义字符使用通常的反斜线(`\`)方式表示. 关于 Kotlin 支持的转义字符, 请参见 [字符](characters.html). ### 多行(Multiline)字符串 多行(Multiline)字符串 可以包含换行符和任意文本. 由三重引号表示(`"""`), 其内容不转义, 可以包含换行符和任意字符: ```KOTLIN val text = """ for (c in "foo") print(c) """ ``` 要删除多行字符串的前导空白(leading whitespace), 可以使用 [trimMargin()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/trim-margin.html) 函数: ```KOTLIN val text = """ |Tell me and I forget. |Teach me and I remember. |Involve me and I learn. |(Benjamin Franklin) """.trimMargin() ``` 默认情况下, 会使用管道符号 `|` 作为前导空白的标记前缀, 但你可以通过参数指定使用其它字符, 比如 `trimMargin(">")`. ## 字符串模板 字符串字面值内可以包含 模板表达式, 它是一小段代码, 会被执行, 其计算结果将被拼接为字符串内容的一部分. 在处理模板中的表达式时, Kotlin 会自动对表达式的计算结果调用 `.toString()` 函数, 将它转换为字符串. 模板表达式以 `$` 符号开始, `$` 符号之后可以是一个变量名: ```KOTLIN fun main() { //sampleStart val i = 10 println("i = $i") // 输出结果为: i = 10 val letters = listOf("a","b","c","d","e") println("Letters: $letters") // 输出结果为: Letters: [a, b, c, d, e] //sampleEnd } ``` `$` 符号之后也可以是表达式, 由大括号括起: ```KOTLIN fun main() { //sampleStart val s = "abc" println("$s.length is ${s.length}") // 输出结果为: abc.length is 3 //sampleEnd } ``` 在多行字符串(Multiline String)和转义字符串(Escaped String)中都可以使用模板. 但是, 多行字符串不支持反斜线转义表达方式. 如果要在多行字符串中的任何可以用作 [标识符](https://kotlinlang.org/grammar/#identifiers) 开始字符的符号之前插入美元符号 `$` 本身, 请使用以下语法: ```KOTLIN val price = """ ${'$'}_9.99 """ ``` Note: 要在字符串中避免使用 `${'$'}` 这样的序列, 你可以使用实验性的 [多 $ 符号字符串插值功能](#multi-dollar-string-interpolation). ### 多 `$` 符号字符串插值(Interpolation) 通过多 `$` 符号字符串插值, 你可以指定需要多少个连续的 `$` 符号才会触发插值(Interpolation). 插值是指将变量或表达式直接嵌入到字符串中的过程. 尽管对单行字符串你可以使用 [转义字符串字面值](#escaped-strings), 但 Kotlin 中的多行字符串不支持反斜线转义表达方式. 要将美元符号 (`$`) 用作字面值, 你必须使用 `${'$'}` 结构来防止发生字符串插值. 这个方法会让代码难以阅读, 尤其是字符串包含多个 `$` 符号的情况. 多 `$` 符号字符串插值功能会简化这个问题, 它允许你在单行和多行字符串中将 `$` 符号用作字面值. 例如: ```KOTLIN val KClass<*>.jsonSchema : String get() = $$""" { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://example.com/product.schema.json", "$dynamicAnchor": "meta", "title": "$${simpleName ?: qualifiedName ?: "unknown"}", "type": "object" } """ ``` 这里, `$$` 前缀规定需要 2 个连续的 `$` 符号才会触发字符串插值. 单个 `$` 符号会作为字面值. 你可以调整使用多少个 `$` 符号来触发插值. 例如, 使用 3 个连续的 `$` 符号 (`$$$`) 可以让 `$` 和 `$$` 都作为字面值, 使用 `$$$` 来启用插值: ```KOTLIN val productName = "carrot" val requestedData = $$$"""{ "currency": "$", "enteredAmount": "42.45 $$", "$$serviceField": "none", "product": "$$$productName" } """ println(requestedData) // 输出结果为: //{ // "currency": "$", // "enteredAmount": "42.45 $$", // "$$serviceField": "none", // "product": "carrot" //} ``` 这里, `$$$` 前缀允许字符串中包含 `$` 和 `$$`, 而不需要使用 `${'$'}` 结构进行转义. 多 `$` 符号字符串插值功能不会影响既有的, 使用单个 `$` 符号字符串插值的代码. 你可以继续和以前一样使用单个 `$`, 然后在需要在字符串中处理 `$` 符号字面值时, 使用多个 `$` 符号. ## 字符串格式化 Note: 使用 `String.format()` 函数进行字符串格式化, 只能用于 Kotlin/JVM 平台. 如果要按照你的需求来格式化一个字符串, 可以使用 [String.format()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/format.html) 函数. `String.format()` 函数接受一个格式字符串, 以及一个或多个参数. 格式字符串对每个参数包含一个占位符(通过 `%` 表达), 之后是格式说明符. 格式说明符是针对对应参数的格式指令, 由符号, 宽度, 精度以及转换类型组成. 总的来说, 格式说明符决定了输出的格式. 通用的格式说明符包括: `%d` 用于整数, `%f` 用于浮点数, 以及 `%s` 用于字符串. 你还可以使用 `argument_index$` 语法, 在格式字符串中, 使用不同的格式多次引用同一个参数. Note: 关于格式字符串的详细解释, 以及它的完整列表, 请参见 [Java Formatter 类的文档](https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html#summary). 我们来看一个示例程序: ```KOTLIN fun main() { //sampleStart // 格式化 1 个整数, 添加前导的 0, 使结果长度为 7 个字符 val integerNumber = String.format("%07d", 31416) println(integerNumber) // 输出结果为: 0031416 // 格式化 1 个浮点数, 显示正负号, 保留 4 位小数 val floatNumber = String.format("%+.4f", 3.141592) println(floatNumber) // 输出结果为: +3.1416 // 格式化 2 个字符串, 显示为大写文字, 每个字符串使用一个占位符 val helloString = String.format("%S %S", "hello", "world") println(helloString) // 输出结果为: HELLO WORLD // 格式化 1 个负数, 包含在括号中, 然后使用 `argument_index$`, 以不同的格式输出同一个数字 (没有括号). val negativeNumberInParentheses = String.format("%(d means %1\$d", -31416) println(negativeNumberInParentheses) //输出结果为: (31416) means -31416 //sampleEnd } ``` `String.format()` 函数提供了与字符串模板类似的功能. 但是, `String.format()` 函数的功能要更多一些, 因为可以使用更多的格式选项. 此外, 可以通过变量来指定格式字符串. 当格式字符串本身可变时, 这是很有用的功能 例如, 在根据用户的语言设定进行本地化翻译时. 使用 `String.format()` 函数时要小心, 因为在参数与对应的占位符之间, 很容易写错它们的个数或位置. # 数组 数组是一种数据结构, 其中包含固定数量的值, 所有的值为同一个类型, 或这个类型的子类型. Kotlin 中最常见的数组类型是对象类型的数组, 使用 [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/) 类表达. Note: 如果你在对象类型的数组中使用基本类型(Primitive Type), 会造成性能损失, 因为你的基本类型会被 [装箱](https://docs.oracle.com/javase/tutorial/java/data/autoboxing.html) 为对象. 要避免这种装箱造成的性能损失, 请使用 [基本类型数组](#primitive-type-arrays). ## 什么时候使用数组 当你需要满足某些特殊的低层级要求时, 可以在 Kotlin 中使用数组. 例如, 如果你的性能需求超过了通常的应用程序的需求, 或者需要构建自定义数据结构的情况. 如果你没有这种类型的限制, 请使用 [集合(Collection)](collections-overview.html). 集合与数组相比, 有以下优点: * 集合是只读的, 因此给了你更多的控制权, 使你能够编写意图清晰的, 更加健壮的代码. * 更容易对集合添加或删除元素. 与此相反, 数组的大小是固定的. 要对数组添加或删除元素, 只能每次创建新的数组, 这是非常效率低下的: ```KOTLIN fun main() { //sampleStart var riversArray = arrayOf("Nile", "Amazon", "Yangtze") // 使用 += 赋值操作创建新的 riversArray, // 复制原来的元素, 并添加 "Mississippi" riversArray += "Mississippi" println(riversArray.joinToString()) // 输出结果为 Nile, Amazon, Yangtze, Mississippi //sampleEnd } ``` * 你可以使用相等操作符(`==`) 来检查两个集合是否结构相等(Structurally Equal). 但不能对数组使用这个操作符. 相反, 你需要使用特殊的函数, 详情请参见 [比较数组](#compare-arrays). 关于集合, 详情请参见 [集合概述](collections-overview.html). ## 创建数组 在 Kotlin 中要创建数组, 你可以使用: * 函数, 例如 [arrayOf()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/array-of.html), [arrayOfNulls()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/array-of-nulls.html#kotlin$arrayOfNulls(kotlin.Int)) 或 [emptyArray()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/empty-array.html). * `Array` 构造器. 下面的示例使用 [arrayOf()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/array-of.html) 函数, 并将数组元素的值传递给它: ```KOTLIN fun main() { //sampleStart // 使用元素值 [1, 2, 3] 创建数组 val simpleArray = arrayOf(1, 2, 3) println(simpleArray.joinToString()) // 输出结果为 1, 2, 3 //sampleEnd } ``` 下面的示例使用 [arrayOfNulls()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/array-of-nulls.html#kotlin$arrayOfNulls(kotlin.Int)) 函数创建指定大小的数组, 并使用 `null` 元素填充数组: ```KOTLIN fun main() { //sampleStart // 使用元素值 [null, null, null] 创建数组 val nullArray: Array = arrayOfNulls(3) println(nullArray.joinToString()) // 输出结果为 null, null, null //sampleEnd } ``` 下面的示例使用 [emptyArray()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/empty-array.html) 函数创建空数组: ```KOTLIN var exampleArray = emptyArray() ``` Note: 由于 Kotlin 的类型推断功能, 在赋值语句的左侧或右侧都可以指定空数组的类型. 例如: ```KOTLIN var exampleArray = emptyArray() var exampleArray: Array = emptyArray() ``` `Array` 构造器的参数是, 数组大小, 以及一个函数, 这个函数对指定的数组下标返回对应的元素值: ```KOTLIN fun main() { //sampleStart // 创建一个 Array, 初始化为 0 值: [0, 0, 0] val initArray = Array(3) { 0 } println(initArray.joinToString()) // 输出结果为 0, 0, 0 // 创建一个 Array, 初始化为 ["0", "1", "4", "9", "16"] val asc = Array(5) { i -> (i * i).toString() } asc.forEach { print(it) } // 输出结果为 014916 //sampleEnd } ``` Note: 与大多数编程语言一样, 在 Kotlin 中, 数组下标从 0 开始. ### 嵌套的数组 数组可以相互嵌套, 创建多维数组: ```KOTLIN fun main() { //sampleStart // 创建一个 2 维数组 val twoDArray = Array(2) { Array(2) { 0 } } println(twoDArray.contentDeepToString()) // 输出结果为 [[0, 0], [0, 0]] // 创建一个 3 维数组 val threeDArray = Array(3) { Array(3) { Array(3) { 0 } } } println(threeDArray.contentDeepToString()) // 输出结果为 [[[0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0]]] //sampleEnd } ``` Note: 嵌套的数组不需要类型相同, 也不需要大小相同. ## 访问和修改元素 数组永远是可以修改的. 要访问和修改数组中的元素, 请使用 [下标访问操作符](operator-overloading.html#indexed-access-operator)`[]`: ```KOTLIN fun main() { //sampleStart val simpleArray = arrayOf(1, 2, 3) val twoDArray = Array(2) { Array(2) { 0 } } // 访问并修改元素 simpleArray[0] = 10 twoDArray[0][0] = 2 // 输出修改后的元素 println(simpleArray[0].toString()) // 输出结果为 10 println(twoDArray[0][0].toString()) // 输出结果为 2 //sampleEnd } ``` Kotlin 中的数组是 不可变的(invariant). 这意味着 Kotlin 不允许你将一个 `Array` 赋值给一个 `Array`, 以防止发生运行时错误. 相反, 你可以使用 `Array`. 更多详情请参见, [类型投射](generics.html#type-projections). ## 使用数组 在 Kotlin 中, 你可以使用数组, 向一个函数传递不定数量的参数, 或对数组元素本身执行操作. 例如, 比较数组, 变换数组内容, 或转换为集合. ### 向一个函数传递不定数量的参数 在 Kotlin 中, 你可以通过 [vararg](functions.html#variable-number-of-arguments-varargs) 参数, 向一个函数传递不定数量的参数. 如果你不能预先知道参数的数量, 这个功能是很有用的, 例如格式化消息, 或者创建 SQL 查询的情况. 要向一个函数传递一个数组, 其中包含不定数量的参数, 请使用 展开(spread) 操作符 (`*`). 展开操作符会将数组的每个元素作为独立的参数传递给指定的函数: ```KOTLIN fun main() { val lettersArray = arrayOf("c", "d") printAllStrings("a", "b", *lettersArray) // 输出结果为 abcd } fun printAllStrings(vararg strings: String) { for (string in strings) { print(string) } } ``` 更多详情请参见 [不定数量参数(varargs)](functions.html#variable-number-of-arguments-varargs). ### 比较数组 要比较两个数组是否包含相同的元素, 并且顺序也相同, 请使用 [.contentEquals()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/content-equals.html) 和 [.contentDeepEquals()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/content-deep-equals.html) 函数: ```KOTLIN fun main() { //sampleStart val simpleArray = arrayOf(1, 2, 3) val anotherArray = arrayOf(1, 2, 3) // 比较数组内容 println(simpleArray.contentEquals(anotherArray)) // 输出结果为 true // 使用中缀标记法(Infix notation), 在一个元素发生变化之后, 再次比较数组内容 simpleArray[0] = 10 println(simpleArray contentEquals anotherArray) // 输出结果为 false //sampleEnd } ``` Warning: 不要使用相等 (`==`) 和不等 (`!=`) [操作符](equality.html#structural-equality) 来比较数组内容. 这些操作符会检查赋值的变量是否指向相同的对象. 关于 Kotlin 中数组的行为为什么会如此, 详情请参见 [这篇 blog](https://blog.jetbrains.com/kotlin/2015/09/feedback-request-limitations-on-data-classes/#Appendix.Comparingarrays). ### 变换数组 Kotlin 有很多有用的函数, 可以对数组进行变换. 这篇文档重点介绍少数几个函数, 但并不是完整的功能列表. 关于所有函数的完整列表, 请参见我们的 [API 参考文档](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/). #### 求和 要得到一个数组中所有元素的和, 请使用 [.sum()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/sum.html) 函数: ```KOTLIN fun main() { //sampleStart val sumArray = arrayOf(1, 2, 3) // 对数组元素求和 println(sumArray.sum()) // 输出结果为 6 //sampleEnd } ``` Note: `.sum()` 函数只能用于 [数值类型](numbers.html) 的数组, 例如 `Int`. #### 随机打乱 要随机打乱数组中的元素, 请使用 [.shuffle()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/shuffle.html) 函数: ```KOTLIN fun main() { //sampleStart val simpleArray = arrayOf(1, 2, 3) // 随机打乱元素 [3, 2, 1] simpleArray.shuffle() println(simpleArray.joinToString()) // 再次随机打乱元素 [2, 3, 1] simpleArray.shuffle() println(simpleArray.joinToString()) //sampleEnd } ``` ### 将数组转换为集合 如果你同时使用不同的 API, 其中一些使用数组, 另一些使用集合, 那么你可以将数组转换为 [集合](collections-overview.html), 也可以反过来将集合转换为数组. #### 转换为 List 或 Set 要将数组转换为 `List` 或 `Set`, 请使用 [.toList()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/to-list.html) 和 [.toSet()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/to-set.html) 函数. ```KOTLIN fun main() { //sampleStart val simpleArray = arrayOf("a", "b", "c", "c") // 转换为 Set println(simpleArray.toSet()) // 输出结果为 [a, b, c] // 转换为 List println(simpleArray.toList()) // 输出结果为 [a, b, c, c] //sampleEnd } ``` #### 转换为 Map 要将数组转换为 `Map`, 请使用 [.toMap()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/to-map.html) 函数. 只有元素类型为 [Pair<K,V>](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-pair/) 的数组能够转换为 `Map`. `Pair` 实例的第 1 个值成为键(key), 第 2 个值成为值(value). 下面的示例使用 [中缀标记法(Infix notation)](functions.html#infix-notation) 来调用 [to](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/to.html) 函数, 创建 `Pair` 的元祖: ```KOTLIN fun main() { //sampleStart val pairArray = arrayOf("apple" to 120, "banana" to 150, "cherry" to 90, "apple" to 140) // 转换为 Map // 键(key)是水果, 值(value)是它们的卡路里数量 // 注意, 键必须是唯一的, 因此最后一个 "apple" 的值会覆盖第一个的值 println(pairArray.toMap()) // 输出结果为 {apple=140, banana=150, cherry=90} //sampleEnd } ``` ## 基本类型(Primitive Type)数组 如果你使用 `Array` 类来存储基本类型(Primitive Type), 这些元素值会被装箱为对象. 另一种选择是, 你可以使用基本类型数组, 它可以让你在数组中存储基本类型, 而不会发生装箱操作导致的性能损失副作用: | 基本类型数组 |相当于 Java 中的类型 | ------------------------- | [BooleanArray](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean-array/) |`boolean[]` | | [ByteArray](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-byte-array/) |`byte[]` | | [CharArray](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-char-array/) |`char[]` | | [DoubleArray](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double-array/) |`double[]` | | [FloatArray](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-float-array/) |`float[]` | | [IntArray](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int-array/) |`int[]` | | [LongArray](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-long-array/) |`long[]` | | [ShortArray](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-short-array/) |`short[]` | 这些类与 `Array` 类没有继承关系, 但它们有相同的一组函数和属性. 下面的示例创建一个 `IntArray` 类的实例: ```KOTLIN fun main() { //sampleStart // 创建一个数组, 元素类型为 Int, 大小为 5, 元素值初始化为 0 val exampleArray = IntArray(5) println(exampleArray.joinToString()) // 输出结果为 0, 0, 0, 0, 0 //sampleEnd } ``` Note: 要将基本类型数组转换为对象类型数组, 请使用 [.toTypedArray()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/to-typed-array.html) 函数. 要将对象类型数组转换为基本类型数组, 请使用 [.toBooleanArray()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/to-boolean-array.html), [.toByteArray()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/to-byte-array.html), [.toCharArray()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/to-char-array.html), 等等函数. ## 下一步做什么? * 为什么对大多数使用场景我们推荐使用集合, 请阅读我们的 [集合概述](collections-overview.html). * 学习其他 [基本类型](types-overview.html). * 如果你是 Java 开发者, 请阅读我们的 Java 到 Kotlin 迁移向导, 关于 [集合](java-to-kotlin-collections-guide.html) 的部分. # 类型检查与类型转换 在 Kotlin 中, 你可以在运行时对类型进行两种操作: 检查对象是否为特定类型, 或将对象转换为另一种类型. 类型 检查 帮助你确认正在处理的对象的种类, 而类型 转换* 则尝试将对象转换为另一种类型. Tip: 关于 泛型 的类型检查和转换, 例如 `List`, `Map`, 请参见 [泛型的类型检查和转换](generics.html#generics-type-checks-and-casts). ## 使用 `is` 与 `!is` 操作符进行类型检查 使用 `is` 操作符(或相反的 `!is` 操作符), 在运行时检查对象是否匹配某个类型: ```KOTLIN fun main() { val input: Any = "Hello, Kotlin" if (input is String) { println("Message length: ${input.length}") // 输出结果为: Message length: 13 } if (input !is String) { // 等价于 !(input is String) println("Input is not a valid message") } else { println("Processing message: ${input.length} characters") // 输出结果为: Processing message: 13 characters } } ``` 也可以使用 `is` 和 `!is` 操作符, 检查对象是否匹配某个子类型: ```KOTLIN interface Animal { val name: String fun speak() } class Dog(override val name: String) : Animal { override fun speak() = println("$name says: Woof!") } class Cat(override val name: String) : Animal { override fun speak() = println("$name says: Meow!") } //sampleStart fun handleAnimal(animal: Animal) { println("Handling animal: ${animal.name}") animal.speak() // 使用 is 操作符检查子类型 if (animal is Dog) { println("Special care instructions: This is a dog.") } else if (animal is Cat) { println("Special care instructions: This is a cat.") } } //sampleEnd fun main() { val pets: List = listOf( Dog("Buddy"), Cat("Whiskers"), Dog("Rex") ) for (pet in pets) { handleAnimal(pet) println("---") } // 输出结果为: // Handling animal: Buddy // Buddy says: Woof! // Special care instructions: This is a dog. // --- // Handling animal: Whiskers // Whiskers says: Meow! // Special care instructions: This is a cat. // --- // Handling animal: Rex // Rex says: Woof! // Special care instructions: This is a dog. // --- } ``` 这个示例使用 `is` 操作符检查, `Animal` 类实例是否为子类型 `Dog` 或 `Cat`, 来打印相关的护理说明. 你可以检查一个对象是否是其声明类型的超类型, 但这其实没有意义, 因为答案永远为 true. 每个类实例本来就已经是其超类型的实例. Tip: 要在运行时识别对象的类型, 请参见 [反射(Reflection)](reflection.html). ## 类型转换 在 Kotlin 中, 将对象的类型转换为另一种类型, 这称为 类型转换. 在某些情况下, 编译器会自动为你进行类型转换. 这称为智能类型转换. 如果需要显式的转换类型, 请使用 `as?` 或 `as` [类型转换操作符](#unsafe-cast-operator). ## 智能类型转换 对不可变值, 编译器会追踪它的类型检查和 [显式的类型转换](#unsafe-cast-operator), 然后自动插入隐式的(安全的)类型转换: ```KOTLIN fun logMessage(data: Any) { // data 被自动转换为 String 类型 if (data is String) { println("Received text: ${data.length} characters") } } fun main() { logMessage("Server started") // 输出结果为: Received text: 14 characters logMessage(404) } ``` 如果一个相反的类型检查导致了 return, 此时编译器足够智能, 能够判断出转换处理是安全的: ```KOTLIN fun logMessage(data: Any) { // data 被自动转换为 String 类型 if (data !is String) return println("Received text: ${data.length} characters") } fun main() { logMessage("User signed in") // 输出结果为: Received text: 14 characters logMessage(true) } ``` ### 控制流 智能类型转换不仅能够用于 `if` 条件表达式, 还能用于 [when 表达式](control-flow.html#when-expressions-and-statements): ```KOTLIN fun processInput(data: Any) { when (data) { // data 被自动转换为 Int 类型 is Int -> println("Log: Assigned new ID ${data + 1}") // data 被自动转换为 String 类型 is String -> println("Log: Received message \"$data\"") // data 被自动转换为 IntArray 类型 is IntArray -> println("Log: Processed scores, total = ${data.sum()}") } } fun main() { processInput(1001) // 输出结果为: Log: Assigned new ID 1002 processInput("System rebooted") // 输出结果为: Log: Received message "System rebooted" processInput(intArrayOf(10, 20, 30)) // 输出结果为: Log: Processed scores, total = 60 } ``` 以及 [while 循环](control-flow.html#while-loops): ```KOTLIN sealed interface Status data class Ok(val currentRoom: String) : Status data object Error : Status class RobotVacuum(val rooms: List) { var index = 0 fun status(): Status = if (index < rooms.size) Ok(rooms[index]) else Error fun clean(): Status { println("Finished cleaning ${rooms[index]}") index++ return status() } } fun main() { //sampleStart val robo = RobotVacuum(listOf("Living Room", "Kitchen", "Hallway")) var status: Status = robo.status() while (status is Ok) { // 编译器将 status 智能类型转换为 OK 类型, // 因此可以访问 currentRoom 属性. println("Cleaning ${status.currentRoom}...") status = robo.clean() } // 输出结果为: // Cleaning Living Room... // Finished cleaning Living Room // Cleaning Kitchen... // Finished cleaning Kitchen // Cleaning Hallway... // Finished cleaning Hallway //sampleEnd } ``` 在这个示例中, 封闭接口 `Status` 有两个实现: 数据类 `Ok` 和数据对象 `Error`. 只有数据类 `Ok` 才有 `currentRoom` 属性. 当 `while` 循环条件计算结果为 true 时, 编译器将 `status` 变量智能类型转换为 `Ok` 类型, 使得循环体内可以访问 `currentRoom` 属性. 如果你声明一个 `Boolean` 类型的变量, 然后在你的 `if`, `when`, 或 `while` 条件中使用它, 那么编译器收集的关于这个变量的所有信息, 在对应的代码块中都可以用于智能类型转换. 当你想要将布尔条件抽取到变量中时, 这个功能会很有用. 之后, 你可以给变量一个有意义的名字, 这样可以提高你的代码的可读性, 并可以在之后的代码中重用这个变量. 例如: ```KOTLIN class Cat { fun purr() { println("Purr purr") } } //sampleStart fun petAnimal(animal: Any) { val isCat = animal is Cat if (isCat) { // 编译器能够得到关于 isCat 的信息, // 因此它知道 animal 已经被智能转换为 Cat 类型. // 所以, 可以调用 purr() 函数. animal.purr() } } fun main(){ val kitty = Cat() petAnimal(kitty) // 输出结果为: Purr purr } //sampleEnd ``` ### 逻辑操作符 对于 `&&` 和 `||` 操作符, 如果在操作符左侧进行了(通常的或相反的)类型检查, 那么编译器能够在右侧进行智能类型转换: ```KOTLIN // 在 `||` 的右侧, x 被自动转换为 String 类型 if (x !is String || x.length == 0) return // 在 `&&` 的右侧, x 被自动转换为 String 类型 if (x is String && x.length > 0) { print(x.length) // x 被自动转换为 String 类型 } ``` 如果你将对象的多个类型检查用 `or` 操作符 (`||`) 组合起来, 智能类型转换的结果会是这些类型最接近的共通超类型: ```KOTLIN interface Status { fun signal() {} } interface Ok : Status interface Postponed : Status interface Declined : Status fun signalCheck(signalStatus: Any) { if (signalStatus is Postponed || signalStatus is Declined) { // signalStatus 被智能类型转换为共通超类型 Status signalStatus.signal() } } ``` Note: 共通超类型是 [联合类型(Union Type)](https://en.wikipedia.org/wiki/Union_type) 的一种 近似. 联合类型 [在 Kotlin 中目前不支持](https://youtrack.jetbrains.com/issue/KT-13108/Denotable-union-and-intersection-types). ### 内联函数 对传递给 [内联函数](inline-functions.html) 的 Lambda 函数中捕获的变量, 编译器能够进行智能类型转换. 内联函数会被当作具有隐含的 [callsInPlace](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.contracts/-contract-builder/calls-in-place.html) 契约(Contract). 这就意味着, 传递给内联函数的任何 Lambda 函数都会被原地调用(call in place). 由于 Lambda 函数被原地调用, 因此编译器知道 Lambda 函数不会泄露它的函数体中所包含的任何变量的引用. 编译器使用这些信息, 以及其它分析, 决定对捕获的变量能否安全的进行智能类型转换. 例如: ```KOTLIN interface Processor { fun process() } inline fun inlineAction(f: () -> Unit) = f() fun nextProcessor(): Processor? = null fun runProcessor(): Processor? { var processor: Processor? = null inlineAction { // 编译器知道 processor 是一个局部变量, inlineAction() 是一个内联函数, // 因此对 processor 的引用不会泄露. // 所以, 对 processor 可以安全的进行智能类型转换. // 如果 processor 不为 null, processor 会被智能类型转换 if (processor != null) { // 编译器知道 processor 不为 null, 因此不需要安全调用 processor.process() } processor = nextProcessor() } return processor } ``` ### 异常处理 智能类型转换信息会被传递给 `catch` 和 `finally` 代码块. 这能够让你的代码更加安全, 因为编译器会追踪你的对象是不是可为 null 的类型. 例如: ```KOTLIN //sampleStart fun testString() { var stringInput: String? = null // stringInput 被智能类型转换为 String 类型 stringInput = "" try { // 编译器知道 stringInput 不为 null println(stringInput.length) // 输出结果为: 0 // 编译器丢弃 stringInput 之前的智能类型转换信息. // 现在 stringInput 类型为 String?. stringInput = null // 触发异常 if (2 > 1) throw Exception() stringInput = "" } catch (exception: Exception) { // 编译器知道 stringInput 可以为 null // 因此 stringInput 继续保持可为 null 的类型. println(stringInput?.length) // 输出结果为: null } } //sampleEnd fun main() { testString() } ``` ### 智能类型转换的前提条件 只在编译器能够确保变量在检查和使用之间不会改变的情况下, 智能类型转换才有效. 在以下条件下可以使用智能类型转换: | `val` 局部变量 | 永远有效, 但 [局部的委托属性](delegated-properties.html) 例外. | | `val` 属性 | 如果属性是 `private` 的, 或 `internal` 的, 或者类型检查处理与属性定义出现在同一个 [模块(module)](visibility-modifiers.html#modules) 内, 那么智能类型转换是有效的. 对于 `open` 属性, 或存在自定义 get 方法的属性, 智能类型转换是无效的. | | `var` 局部变量 | 如果在类型检查语句与变量使用语句之间, 变量没有被改变, 而且它没有被 Lambda 表达式捕获并在 Lambda 表达式内修改它, 并且它不是一个局部的委托属性, 那么智能类型转换是有效的. | | `var` 属性 | 永远无效, 因为其他代码随时可能改变变量值. | ## `as` 与 `as?` 类型转换操作符 Kotlin 有两个类型转换操作符: `as` 和 `as?`. 两者都可以用于类型转换, 但行为不同. 如果使用 `as` 操作符进行转换, 失败时会在运行期抛出 `ClassCastException`. 因此它也被称为 不安全 操作符. 你可以在转换为非 null 类型时使用 `as`: ```KOTLIN fun main() { val rawInput: Any = "user-1234" // 成功转换为 String 类型 val userId = rawInput as String println("Logging in user with ID: $userId") // 输出结果为: Logging in user with ID: user-1234 // 触发 ClassCastException val wrongCast = rawInput as Int println("wrongCast contains: $wrongCast") // Exception in thread "main" java.lang.ClassCastException } ``` 如果改用 `as?` 操作符, 当转换失败时, 操作符会返回 `null`. 因此它也被称为 安全 操作符: ```KOTLIN fun main() { val rawInput: Any = "user-1234" // 成功转换为 String 类型 val userId = rawInput as? String println("Logging in user with ID: $userId") // 输出结果为: Logging in user with ID: user-1234 // 将 null 值赋给 wrongCast val wrongCast = rawInput as? Int println("wrongCast contains: $wrongCast") // 输出结果为: wrongCast contains: null } ``` 要安全地转换可为 null 的类型, 请使用 `as?` 操作符, 以防止转换失败时触发 `ClassCastException`. 你 可以 将 `as` 与可为 null 的类型一起使用. 这样的操作允许结果为 `null`, 但如果转换不成功, 仍然会抛出 `ClassCastException`. 因此, `as?` 是更安全的选择: ```KOTLIN fun main() { val config: Map = mapOf( "username" to "kodee", "alias" to null, "loginAttempts" to 3 ) // 不安全地转换为可为 null 的 String val username: String? = config["username"] as String? println("Username: $username") // 输出结果为: Username: kodee // 不安全地将 null 值转换为可为 null 的 String val alias: String? = config["alias"] as String? println("Alias: $alias") // 输出结果为: Alias: null // 转换为可为 null 的 String 失败, 抛出 ClassCastException // val unsafeAttempts: String? = config["loginAttempts"] as String? // println("Login attempts (unsafe): $unsafeAttempts") // Exception in thread "main" java.lang.ClassCastException // 转换为可为 null 的 String 失败, 返回 null val safeAttempts: String? = config["loginAttempts"] as? String println("Login attempts (safe): $safeAttempts") // 输出结果为: Login attempts (safe): null } ``` ### 向上转换和向下转换 在 Kotlin 中, 你可以将对象转换为超类型或子类型. 将对象转换为其超类实例的操作称为 向上转换(upcasting). 向上转换不需要任何特殊的语法或转换操作符. 例如: ```KOTLIN interface Animal { fun makeSound() } class Dog : Animal { // 实现 makeSound() 的行为 override fun makeSound() { println("Dog says woof!") } } fun printAnimalInfo(animal: Animal) { animal.makeSound() } fun main() { val dog = Dog() // 将 Dog 实例向上转换为 Animal printAnimalInfo(dog) // 输出结果为: Dog says woof! } ``` 在这个示例中, 当对 `Dog` 实例调用 `printAnimalInfo()` 函数时, 编译器将其向上转换为 `Animal`, 因为这是预期的参数类型. 由于实际对象仍然是 `Dog` 实例, 编译器动态地从 `Dog` 类中解析 `makeSound()` 函数, 打印 `"Dog says woof!"`. 在依赖抽象类型行为的 Kotlin API 中, 你经常会看到显式的向上转换. 在 Jetpack Compose 和 UI 工具包中也很常见, 这些工具包通常将所有 UI 元素视为超类型, 然后对特定子类进行操作: ```KOTLIN val textView = TextView(this) textView.text = "Hello, View!" // 从 TextView 向上转换为 View val view: View = textView // 使用 View 的函数 view.setPadding(20, 20, 20, 20) // Activity 期望 View 类型 setContentView(view) ``` 将对象转换为其子类实例的操作称为 向下转换(downcasting). 由于向下转换可能不安全, 你需要使用显式的转换操作符. 为了避免转换失败时抛出异常, 我们推荐使用安全转换操作符 `as?`, 当转换失败时返回 `null`: ```KOTLIN interface Animal { fun makeSound() } class Dog : Animal { override fun makeSound() { println("Dog says woof!") } fun bark() { println("BARK!") } } fun main() { // 使用 Dog 实例创建 animal 变量, 类型为 Animal val animal: Animal = Dog() // 将 animal 安全的向下转换为 Dog 类型 val dog: Dog? = animal as? Dog // 使用安全调用, 当 dog 不为 null 时调用 bark() dog?.bark() // 输出结果为: "BARK!" } ``` 在这个示例中, `animal` 被声明为 `Animal` 类型, 但它保存一个 `Dog` 实例. 代码将 `animal` 安全的转换为 `Dog` 类型, 并使用 [安全调用](null-safety.html#safe-call-operator) (`?.`) 来访问 `bark()` 函数. 在序列化处理中, 如果要将基类反序列化为特定的子类型, 会用到向下转换. 在使用 Java 库时, 如果返回类型为超类型的对象, 而你需要在 Kotlin 中转换为子类型, 那么也经常用到向下转换. # 类型别名 类型别名可以为已有的类型提供替代的名称. 如果类型名称太长, 你可以指定一个更短的名称, 然后使用新的名称. 这个功能有助于缩短那些很长的泛型类型名称. 比如, 缩短集合类型的名称通常是很吸引人的: ```KOTLIN typealias NodeSet = Set typealias FileTable = MutableMap> ``` 你也可以为函数类型指定不同的别名: ```KOTLIN typealias MyHandler = (Int, String, Any) -> Unit typealias Predicate = (T) -> Boolean ``` 你也可以为内部类和嵌套类指定新的名称: ```KOTLIN class A { inner class Inner } class B { inner class Inner } typealias AInner = A.Inner typealias BInner = B.Inner ``` 类型别名不会引入新的类型. 类型别名与它对应的真实类型完全等同. 如果你添加一个别名 `typealias Predicate`, 然后在你的代码中使用 `Predicate`, Kotlin 编译器会把你的代码扩展为 `(Int) -> Boolean`. 因此, 在需要通常的函数类型的地方, 可以使用你定义的类型别名的变量, 反过来也是如此: ```KOTLIN typealias Predicate = (T) -> Boolean fun foo(p: Predicate) = p(42) fun main() { val f: (Int) -> Boolean = { it > 0 } println(foo(f)) // 打印结果为 "true" val p: Predicate = { it > 0 } println(listOf(1, -2).filter(p)) // 打印结果为 "[1]" } ``` ## 嵌套的类型别名 在 Kotlin 中, 你可以在其他声明之内定义类型别名, 只要不从它们的外部类捕获类型参数: ```KOTLIN class Dijkstra { typealias VisitedNodes = Set private fun step(visited: VisitedNodes, ...) = ... } ``` 所谓捕获, 是指类型别名引用外部类中定义的类型参数: ```KOTLIN class Graph { // 不正确, 因为捕获了 Node 类型 typealias Path = List } ``` 为了解决这个问题, 要直接在类型别名中声明类型参数: ```KOTLIN class Graph { // 正确, 因为 Node 是一个类型别名参数 typealias Path = List } ``` 嵌套的类型别名可以改进封装性, 减少包层级的混乱, 简化内部实现, 使得代码更加清晰, 更易于维护. ### 嵌套的类型别名的规则 嵌套的类型别名遵循一些特定的规则, 以确保清晰而且一致的行为: * 嵌套的类型别名必须遵守现有的类型别名规则. * 从可见度的角度来说, 别名不能暴露超过它引用的类型所允许的内容. * 它们的作用域与 [嵌套类](nested-classes.html) 一样. 你可以在类的内部定义嵌套的类型别名, 它们会隐藏所有同名的父类型别名, 因为它们不会覆盖. * 嵌套的类型别名可以标注为 `internal` 或 `private`, 来限制它们的可见度. * Kotlin Multiplatform 的 [expect/actual 声明](multiplatform-expect-actual.html) 中不支持嵌套的类型别名. # 条件与循环 Kotlin 提供了灵活的工具来控制程序的流程. 使用 `if`, `when` 以及循环, 为你的条件定义清晰, 富有表达力的逻辑. ## if 表达式 在 Kotlin 中使用 `if`, 请在括号 `()` 内添加要检查的条件, 并在大括号 `{}` 内添加条件为 true 时要执行的操作. 你可以使用 `else` 和 `else if` 来添加更多的分支和检查. 也可以将 `if` 写成表达式, 这样可以将返回值直接赋值给一个变量. 在这种形式下, 必须有 `else` 分支. `if` 表达式的作用与其他语言中的三元运算符 (`条件 ? then 分支 : else 分支`) 一样. 例如: ```KOTLIN fun main() { val heightAlice = 160 val heightBob = 175 //sampleStart var taller = heightAlice if (heightAlice < heightBob) taller = heightBob // 使用 else 分支 if (heightAlice > heightBob) { taller = heightAlice } else { taller = heightBob } // 将 if 作为表达式使用 taller = if (heightAlice > heightBob) heightAlice else heightBob // 将 else if 作为表达式使用: val heightLimit = 150 val heightOrLimit = if (heightLimit > heightAlice) heightLimit else if (heightAlice > heightBob) heightAlice else heightBob println("Taller height is $taller") // 输出结果为: Taller height is 175 println("Height or limit is $heightOrLimit") // 输出结果为: Height or limit is 175 //sampleEnd } ``` `if` 表达式的每个分支都可以是一个代码块, 代码块中最后一个表达式的值将成为整个代码块的返回值: ```KOTLIN fun main() { //sampleStart val heightAlice = 160 val heightBob = 175 val taller = if (heightAlice > heightBob) { print("Choose Alice\n") heightAlice } else { print("Choose Bob\n") heightBob } println("Taller height is $taller") //sampleEnd } ``` ## when 表达式和 when 语句 `when` 是一个条件表达式, 根据多个可能的值或条件来运行代码. 它类似于 Java, C, 和其他语言中的 `switch` 语句. `when` 对它的参数求值, 然后将结果与各个分支逐一比较, 直到某个分支条件成立. 例如: ```KOTLIN fun main() { //sampleStart val userRole = "Editor" when (userRole) { "Viewer" -> print("User has read-only access") "Editor" -> print("User can edit content") else -> print("User role is not recognized") } // 输出结果为: User can edit content //sampleEnd } ``` 你可以将 `when` 用作 表达式 或 语句. 作为表达式, `when` 返回一个值, 供后面的代码使用. 作为语句, `when` 完成一个动作, 不返回结果: | 表达式 |语句 | ----------- | ```KOTLIN // 返回一个字符串值, 赋值给变量 text val text = when (x) { 1 -> "x == 1" 2 -> "x == 2" else -> "x is neither 1 nor 2" } ``` | ```KOTLIN // 不返回结果, 只是触发一个 print 语句 when (x) { 1 -> print("x == 1") 2 -> print("x == 2") else -> print("x is neither 1 nor 2") } ``` | 其次, 你可以使用 `when` 时带主语(subject), 也可以不带. 无论是否带主语, 行为都是相同的. 使用主语通常可以让你的代码更易于阅读和维护, 因为它清楚地表明了你在检查什么. | 带有主语 `x` |不带主语 | ------------------ | ```KOTLIN when(x) { ... } ``` | ```KOTLIN when { ... } ``` | 你使用 `when` 的方式决定了是否需要在分支中覆盖所有可能的情况. 覆盖所有可能情况称为 穷尽(exhaustive). ### 用作语句 如果你将 `when` 用作语句, 不需要覆盖所有可能的情况. 在下面的示例中, 有些情况没有覆盖, 因此不会触发任何分支. 但是, 不会发生错误: ```KOTLIN fun main() { //sampleStart val deliveryStatus = "OutForDelivery" when (deliveryStatus) { // 没有覆盖所有的情况 "Pending" -> print("Your order is being prepared") "Shipped" -> print("Your order is on the way") } //sampleEnd } ``` 和使用 `if` 一样, 每个分支都可以是一个代码块, 而且它的值是代码块中最后一个表达式的值. ### 用作表达式 如果你将 `when` 用作表达式, 必须 覆盖所有可能的情况. 第一个匹配的分支的值将成为整个表达式的值. 如果没有覆盖所有的情况, 编译器会报告错误. 如果你的 `when` 表达式带有主语, 可以使用 `else` 分支来确保覆盖所有可能的情况, 但 `else` 分支并不是必须的. 例如, 如果你的主语是 `Boolean`, [enum 类](enum-classes.html), [sealed 类](sealed-classes.html), 或这些类型的可为 null 的版本, 就可以覆盖所有情况而不必使用 `else` 分支: ```KOTLIN import kotlin.random.Random //sampleStart enum class Bit { ZERO, ONE } fun getRandomBit(): Bit { return if (Random.nextBoolean()) Bit.ONE else Bit.ZERO } fun main() { val numericValue = when (getRandomBit()) { // 不需要 else 分支, 因为已经覆盖了所有的情况 Bit.ZERO -> 0 Bit.ONE -> 1 } println("Random bit as number: $numericValue") // 输出结果为: Random bit as number: 0 //sampleEnd } ``` Tip: 为了简化 `when` 表达式, 并减少重复代码, 请试用上下文敏感的解析(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). 如果你的 `when` 表达式 不 带有主语, 那么 必须 使用 `else` 分支, 否则编译器会报告错误. 当所有的其他分支条件都不满足时, 就会计算 `else` 分支: ```KOTLIN fun main() { //sampleStart val localFileSize = 1200 val remoteFileSize = 1200 val message = when { localFileSize > remoteFileSize -> "Local file is larger than remote file" localFileSize < remoteFileSize -> "Local file is smaller than remote file" else -> "Local and remote files are the same size" } println(message) // 输出结果为: Local and remote files are the same size //sampleEnd } ``` ### when 的其他使用方式 `when` 表达式和语句提供了不同的方式来简化你的代码, 处理多个条件, 以及执行类型检查. 使用逗号, 将多个条件合并到一个分支中: ```KOTLIN fun main() { val ticketPriority = "High" //sampleStart when (ticketPriority) { "Low", "Medium" -> print("Standard response time") else -> print("High-priority handling") } //sampleEnd } ``` 使用能够计算结果为 `true` 或 `false` 的表达式, 作为分支条件: ```KOTLIN fun main() { val storedPin = "1234" val enteredPin = 1234 //sampleStart when (enteredPin) { // 表达式 storedPin.toInt() -> print("PIN is correct") else -> print("Incorrect PIN") } //sampleEnd } ``` 使用 `in` 或 `!in` 关键字, 检查一个值是否属于一个 [范围](ranges.html) 或集合: ```KOTLIN fun main() { val x = 7 val validNumbers = setOf(15, 16, 17) //sampleStart when (x) { in 1..10 -> print("x is in the range") in validNumbers -> print("x is valid") !in 10..20 -> print("x is outside the range") else -> print("none of the above") } //sampleEnd } ``` 使用 `is` 或 `!is` 关键字检查值的类型. 由于 [智能类型转换](typecasts.html#smart-casts) 功能, 你可以直接访问该类型的成员函数和属性: ```KOTLIN fun hasPrefix(input: Any): Boolean = when (input) { is String -> input.startsWith("ID-") else -> false } fun main() { val testInput = "ID-98345" println(hasPrefix(testInput)) // 输出结果为: true } ``` 使用 `when` 替代传统的 `if`-`else` `if` 串. 不带主语时, 分支条件就是简单的布尔表达式. 条件为 `true` 的第一个分支会执行: ```KOTLIN fun Int.isOdd() = this % 2 != 0 fun Int.isEven() = this % 2 == 0 fun main() { //sampleStart val x = 5 val y = 8 when { x.isOdd() -> print("x is odd") y.isEven() -> print("y is even") else -> print("x+y is odd") } // 输出结果为: x is odd //sampleEnd } ``` 最后, 使用以下语法, 将主语保存到一个变量中: ```KOTLIN fun main() { val message = when (val input = "yes") { "yes" -> "You said yes" "no" -> "You said no" else -> "Unrecognized input: $input" } println(message) // 输出结果为: You said yes } ``` 作为主语引入的这个变量, 它的有效范围仅限于这个 `when` 表达式或语句的 body 部之内. ### 保护条件(Guard Condition) 保护条件(Guard Condition)允许在 `when` 表达式或语句的分支中包含一个以上的条件, 让复杂的控制流变得更加明确和简洁. 只要 `when` 带有主语, 就可以使用保护条件. 在同一个分支中, 将保护条件放在主条件之后, 用 `if` 分隔: ```KOTLIN sealed interface Animal { data class Cat(val mouseHunter: Boolean) : Animal data class Dog(val breed: String) : Animal } fun feedDog() = println("Feeding a dog") fun feedCat() = println("Feeding a cat") //sampleStart fun feedAnimal(animal: Animal) { when (animal) { // 只带有主条件的分支 // 当 animal 是 Dog 时, 调用 feedDog() is Animal.Dog -> feedDog() // 带有主条件和保护条件的分支 // 当 animal 是 Cat, 并且不是 mouseHunter 时, 调用 feedCat() is Animal.Cat if !animal.mouseHunter -> feedCat() // 如果以上条件都不成立, 打印 "Unknown animal" else -> println("Unknown animal") } } fun main() { val animals = listOf( Animal.Dog("Beagle"), Animal.Cat(mouseHunter = false), Animal.Cat(mouseHunter = true) ) animals.forEach { feedAnimal(it) } // 输出结果为: // Feeding a dog // Feeding a cat // Unknown animal } //sampleEnd ``` 当你有多个用逗号分隔的条件时, 不能使用保护条件. 例如: ```KOTLIN 0, 1 -> print("x == 0 or x == 1") ``` 在单个 `when` 表达式或语句中, 可以组合使用带有保护条件和不带保护条件的分支. 带有保护条件的分支中的代码, 只有在主条件和保护条件的计算结果都为 `true` 时才会运行. 如果主条件不成立, 那么保护条件不会被计算. 由于 `when` 语句不需要覆盖所有情况, 在没有 `else` 分支的 `when` 语句中使用保护条件, 意味着如果所有条件都不成立, 则不会运行任何代码. 与语句不同, `when` 表达式必须覆盖所有情况. 如果在没有 `else` 分支的 `when` 表达式中使用保护条件, 编译器要求你处理所有可能的情况, 以避免运行期错误. 在单个分支中, 可以使用布尔操作符 `&&` (与) 或 `||` (或), 组合多个保护条件. 请在布尔表达式之外使用括号, 以 [避免混乱](coding-conventions.html#guard-conditions-in-when-expression): ```KOTLIN when (animal) { is Animal.Cat if (!animal.mouseHunter && animal.hungry) -> feedCat() } ``` 保护条件也支持 `else if`: ```KOTLIN when (animal) { // 检查 `animal` 是不是 `Dog` is Animal.Dog -> feedDog() // 保护条件, 检查 `animal` 是 `Cat`, 并且不是 `mouseHunter` is Animal.Cat if !animal.mouseHunter -> feedCat() // 如果以上条件都不成立, 而且 animal.eatsPlants 为 true, 调用 giveLettuce() else if animal.eatsPlants -> giveLettuce() // 如果以上条件都不成立, 打印 "Unknown animal" else -> println("Unknown animal") } ``` ## for 循环 使用 `for` 循环, 遍历 [集合(collection)](collections-overview.html), [数组(array)](arrays.html), 或 [范围(range)](ranges.html): ```KOTLIN for (item in collection) print(item) ``` `for` 循环体可以是用大括号 `{}` 括起来的代码块. ```KOTLIN fun main() { val shoppingList = listOf("Milk", "Bananas", "Bread") //sampleStart println("Things to buy:") for (item in shoppingList) { println("- $item") } // 输出结果为: // Things to buy: // - Milk // - Bananas // - Bread //sampleEnd } ``` ### 遍历数值范围 要遍历一个数值范围, 请使用 `..` 和 `..<` 运算符构造的 [范围表达式](ranges.html): ```KOTLIN fun main() { //sampleStart println("Closed-ended range:") for (i in 1..6) { print(i) } // 输出结果为: // Closed-ended range: // 123456 println("\nOpen-ended range:") for (i in 1..<6) { print(i) } // 输出结果为: // Open-ended range: // 12345 println("\nReverse order in steps of 2:") for (i in 6 downTo 0 step 2) { print(i) } // 输出结果为: // Reverse order in steps of 2: // 6420 //sampleEnd } ``` ### 遍历数组 如果希望使用下标变量来遍历数组或 list, 可以使用 `indices` 属性: ```KOTLIN fun main() { val routineSteps = arrayOf("Wake up", "Brush teeth", "Make coffee") //sampleStart for (i in routineSteps.indices) { println(routineSteps[i]) } // 输出结果为: // Wake up // Brush teeth // Make coffee //sampleEnd } ``` 或者, 也可以使用标准库中的 [.withIndex()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/with-index.html) 函数: ```KOTLIN fun main() { val routineSteps = arrayOf("Wake up", "Brush teeth", "Make coffee") //sampleStart for ((index, value) in routineSteps.withIndex()) { println("The step at $index is \"$value\"") } // 输出结果为: // The step at 0 is "Wake up" // The step at 1 is "Brush teeth" // The step at 2 is "Make coffee" //sampleEnd } ``` ### 使用迭代器 `for` 循环可以遍历任何提供了 [迭代器](iterators.html) 的值. 集合默认提供迭代器, 而范围和数组会被编译为基于下标的循环. 你可以创建自己的迭代器, 方法是提供一个名为 `iterator()` 的成员函数或扩展函数, 这个函数要返回 `Iterator<>`. `iterator()` 函数必须有一个 `next()` 函数和一个返回 `Boolean` 的 `hasNext()` 函数. 要为类创建自己迭代器, 最简单的方式是继承 [Iterable<T>](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/-iterable/) 接口, 并覆盖其中已有的 `iterator()`, `next()`, 和 `hasNext()` 函数. 例如: ```KOTLIN class Booklet(val totalPages: Int) : Iterable { override fun iterator(): Iterator { return object : Iterator { var current = 1 override fun hasNext() = current <= totalPages override fun next() = current++ } } } fun main() { val booklet = Booklet(3) for (page in booklet) { println("Reading page $page") } // 输出结果为: // Reading page 1 // Reading page 2 // Reading page 3 } ``` Tip: 更多详情请参见 [接口](interfaces.html) 和 [继承](inheritance.html). 或者, 也可以从头创建这些函数. 在这种情况下, 请在函数上添加 `operator` 关键字: ```KOTLIN //sampleStart class Booklet(val totalPages: Int) { operator fun iterator(): Iterator { return object { var current = 1 operator fun hasNext() = current <= totalPages operator fun next() = current++ }.let { object : Iterator { override fun hasNext() = it.hasNext() override fun next() = it.next() } } } } //sampleEnd fun main() { val booklet = Booklet(3) for (page in booklet) { println("Reading page $page") } // 输出结果为: // Reading page 1 // Reading page 2 // Reading page 3 } ``` ## while 循环 `while` 和 `do-while` 循环, 在满足条件时持续运行它们循环体中的代码. 它们之间的区别是, 检查循环条件的时刻不同: * `while` 先检查条件, 如果条件满足, 则运行循环体中的代码, 然后再次回到条件检查. * `do-while` 先运行循环体中的代码, 然后再检查条件. 如果条件满足, 就会继续循环. 因此 `do-while` 的循环体至少会运行一次, 无论条件是否成立. 对于 `while` 循环, 先将要检查的条件放在括号 `()` 内, 然后是循环体, 放在大括号 `{}` 内: ```KOTLIN fun main() { var carsInGarage = 0 val maxCapacity = 3 //sampleStart while (carsInGarage < maxCapacity) { println("Car entered. Cars now in garage: ${++carsInGarage}") } // 输出结果为: // Car entered. Cars now in garage: 1 // Car entered. Cars now in garage: 2 // Car entered. Cars now in garage: 3 println("Garage is full!") // 输出结果为: Garage is full! //sampleEnd } ``` 对于 `do-while` 循环, 先将循环体放在大括号 `{}` 内, 然后是要检查的条件, 放在括号 `()` 内: ```KOTLIN import kotlin.random.Random fun main() { var roll: Int //sampleStart do { roll = Random.nextInt(1, 7) println("Rolled a $roll") } while (roll != 6) // 输出结果为: // Rolled a 2 // Rolled a 6 println("Got a 6! Game over.") // 输出结果为: Got a 6! Game over. //sampleEnd } ``` ## 循环的中断(break)与继续(continue) Kotlin 支持循环内传统的 `break` 和 `continue` 操作符. 详情请参见 [返回与跳转](returns.html). # 返回与跳转: break 与 continue Kotlin 中存在 3 种跳出程序流程的表达式: * `return` 的默认行为是, 从最内层的函数或 [匿名函数](lambdas.html#anonymous-functions) 中返回. * `break` 结束最内层的循环. * `continue` 在最内层的循环中, 跳转到下一次循环. 所有这些表达式都可以用作更大的表达式的一部分: ```KOTLIN val s = person.name ?: return ``` 这些表达式的类型都是 [Nothing 类型](exceptions.html#the-nothing-type). ## Break 和 Continue 的位置标签 Kotlin 中的任何表达式都可以用 label 标签来标记. 标签由标识符后面加一个 `@` 符号构成, 比如 `abc@`, `fooBar@`. 要给一个表达式标记标签, 只需要将标签放在它之前. ```KOTLIN loop@ for (i in 1..100) { // ... } ``` 然后, 你就可以使用标签来限定 `break` 或 `continue` 的跳转对象: ```KOTLIN loop@ for (i in 1..100) { for (j in 1..100) { if (...) break@loop } } ``` 通过标签限定后, `break` 语句, 将会跳转到这个标签标记的循环语句之后. `continue` 语句则会跳转到循环语句的下一次循环. Note: 某些情况下, 你可以 非局部的(non-locally) 使用 `break` 和 `continue`, 但不必明确定义标签. 这种非局部的使用方法, 在内层的 [内联函数](inline-functions.html#break-and-continue) 所使用的 Lambda 表达式中有效. ## 使用标签控制 return 的目标 在 Kotlin 中, 通过使用字面值函数(function literal), 局部函数(local function), 以及对象表达式(object expression), 可以实现函数的嵌套. 通过标签限定的 `return` 语句, 可以从一个外层函数中返回. 最重要的使用场景是从 Lambda 表达式中返回. 如果需要从 Lambda 表达式返回, 可以对它标记一个标签, 然后使用这个标签来指明 `return` 的目标: ```KOTLIN //sampleStart fun foo() { listOf(1, 2, 3, 4, 5).forEach lit@{ if (it == 3) return@lit // 局部的返回(local return), 返回到 Lambda 表达式的调用者: 返回到 forEach 循环 print(it) } print(" done with explicit label") } //sampleEnd fun main() { foo() } ``` 这样, `return` 语句就只从 Lambda 表达式中返回. 通常, 使用 隐含标签 会更方便一些, 因为隐含标签的名称与 Lambda 表达式被传递去的函数名称相同. ```KOTLIN //sampleStart fun foo() { listOf(1, 2, 3, 4, 5).forEach { if (it == 3) return@forEach // 局部的返回(local return), 返回到 Lambda 表达式的调用者: 返回到 forEach 循环 print(it) } print(" done with implicit label") } //sampleEnd fun main() { foo() } ``` 另一种方法是, 你也可以使用 [匿名函数](lambdas.html#anonymous-functions) 来替代 Lambda 表达式. 匿名函数内的 `return` 语句会从匿名函数内返回. ```KOTLIN //sampleStart fun foo() { listOf(1, 2, 3, 4, 5).forEach(fun(value: Int) { if (value == 3) return // 局部的返回(local return), 返回到匿名函数的调用者: 返回到 forEach 循环 print(value) }) print(" done with anonymous function") } //sampleEnd fun main() { foo() } ``` 注意, 上面三个例子中局部返回的使用, 都与通常的循环中的 `continue` 关键字的使用很类似. 不存在与 `break` 直接等价的语法, 但可以模拟出来, 方法是增加一个外层的 `run` Lambda 表达式, 然后在它内部使用非局部的返回: ```KOTLIN //sampleStart fun foo() { run loop@{ listOf(1, 2, 3, 4, 5).forEach { if (it == 3) return@loop // 非局部的返回(non-local return), 从传递给 run 函数的 Lambda 表达式中返回 print(it) } } print(" done with nested loop") } //sampleEnd fun main() { foo() } ``` 这里可以使用非局部的返回, 是因为嵌套的 `forEach()` Lambda 表达式在这里是一个 [内联函数(Inline Function)](inline-functions.html). 当 return 语句指定了返回值时, 源代码解析器会将这样的语句优先识别为使用标签限定的 return 语句: ```KOTLIN return@a 1 ``` 这里的含义是 "返回到标签 `@a` 处, 返回值为 `1`", 而不是 "返回一个带标签的表达式 `(@a 1)`". Note: 某些情况下, 你可以从 Lambda 表达式中返回, 但不必使用标签. 这种 非局部的(non-local) 返回存在于 Lambda 表达式中, 但退出包含 Lambda 表达式的 [内联函数](inline-functions.html#returns). # 异常(Exception) 与错误处理 异常能够让你的代码运行更加可预测, 即使发生可能中断程序执行的运行期错误. Kotlin 默认将所有异常看作 不受控的(unchecked) 异常. 不受控的异常简化了异常的处理过程: 你可以捕获异常, 但你不需要明确的处理或 [声明](java-to-kotlin-interop.html#checked-exceptions) 异常. Tip: 关于 Kotlin 与 Java, Swift, 和 Objective-C 交互时如何处理异常, 详情请参见 [与 Java, Swift, 和 Objective-C 的异常互操作](#exception-interoperability-with-java-swift-and-objective-c) 小节. 处理异常包括 2 个主要操作: * 抛出异常: 指示问题发生. * 捕获异常: 通过解决问题, 或通知开发者或应用程序使用者, 手动处理意外的异常. 异常通过 [Exception](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-exception/) 类的子类来表示, `Exception` 是 [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/) 类的子类. 关于异常的层级结构, 详情请参见 [异常的层级结构](#exception-hierarchy) 小节. 由于 `Exception` 是一个 [open 类](inheritance.html), 因此你可以创建 [自定义异常](#create-custom-exceptions), 以满足你的 应用程序的特定需求. ## 抛出异常 你可以使用 `throw` 关键字, 手动抛出异常. 抛出一个异常, 表示在代码中发生了一个意外的运行期错误. 异常是 [对象](classes.html#creating-instances), 抛出异常会创建一个异常类的一个实例. 你可以抛出一个没有任何参数的异常: ```KOTLIN throw IllegalArgumentException() ``` 为了更好的理解问题的根源, 请包含更多信息, 例如自定义消息, 以及原始原因: ```KOTLIN val cause = IllegalStateException("Original cause: illegal state") // 如果 userInput 为负数, 抛出一个 IllegalArgumentException 异常 // 此外, 它还显示原始原因, 通过 cause IllegalStateException 表示 if (userInput < 0) { throw IllegalArgumentException("Input must be non-negative", cause) } ``` 在这个示例中, 当使用者输入负数值时, 会抛出一个 `IllegalArgumentException` 异常. 你可以创建自定义的错误消息, 并保留异常的原始原因(`cause`), `cause` 会被包含在 [栈追踪(stack trace)](#stack-trace) 中. ### 使用前提条件的检查函数抛出异常 Kotlin 提供了另一种方式, 使用前提条件的检查函数自动抛出异常. 前提条件的检查函数包括以下几种: | 前提条件的检查函数 |使用场景 |抛出的异常 | -------------------------- | [require()](#require-function) |校验使用者的输入 |[IllegalArgumentException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-illegal-argument-exception/) | | [check()](#check-function) |校验对象或变量的状态 |[IllegalStateException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-illegal-state-exception/) | | [error()](#error-function) |表示非法状态或条件 |[IllegalStateException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-illegal-state-exception/) | 这些函数适合于, 如果指定的条件不满足, 程序的流程就无法继续的情况. 使用这些函数可以简化你的代码, 并让这些检查处理变得更加高效. #### require() 函数 如果输入的参数对函数操作非常重要, 参数不正确函数就无法继续运行, 这样的情况下, 可以使用 [require()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/require.html) 函数来校验输入参数. 如果 `require()` 中的条件不满足, 它会抛出一个 [IllegalArgumentException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-illegal-argument-exception/) 异常: ```KOTLIN fun getIndices(count: Int): List { require(count >= 0) { "Count must be non-negative. You set count to $count." } return List(count) { it + 1 } } fun main() { // 这里会失败, 抛出一个 IllegalArgumentException 异常 println(getIndices(-1)) // 取消下面的行的注释, 查看一个能够运行的示例 // println(getIndices(3)) // 输出结果为: [1, 2, 3] } ``` Note: `require()` 函数允许编译器执行 [智能类型转换](typecasts.html#smart-casts). 检查成功之后, 变量会自动转换为非 null 类型. 这些函数经常用来进行 null 检查, 在继续处理之前确保变量不为 null. 例如: ```KOTLIN fun printNonNullString(str: String?) { // null 检查 require(str != null) // 检查成功之后, 'str' 可以确保不为 null, // 并被自动的智能类型转换为非 null 的 String 类型 println(str.length) } ``` #### check() 函数 可以使用 [check()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/check.html) 函数来校验一个对象或变量的状态. 如果检查失败, 表示存在需要解决的逻辑错误. 如果 `check()` 函数中指定的条件为 `false`, 它会抛出一个 [IllegalStateException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-illegal-state-exception/) 异常: ```KOTLIN fun main() { var someState: String? = null fun getStateValue(): String { val state = checkNotNull(someState) { "State must be set beforehand!" } check(state.isNotEmpty()) { "State must be non-empty!" } return state } // 如果你取消下面的行的注释, 那么程序会失败, 抛出 IllegalStateException 异常 // getStateValue() someState = "" // 如果你取消下面的行的注释, 那么程序会失败, 抛出 IllegalStateException 异常 // getStateValue() someState = "non-empty-state" // 输出结果为 "non-empty-state" println(getStateValue()) } ``` Note: `check()` 函数允许编译器执行 [智能类型转换](typecasts.html#smart-casts). 检查成功之后, 变量会自动转换为非 null 类型. 这些函数经常用来进行 null 检查, 在继续处理之前确保变量不为 null. 例如: ```KOTLIN fun printNonNullString(str: String?) { // null 检查 check(str != null) // 检查成功之后, 'str' 可以确保不为 null, // 并被自动的智能类型转换为非 null 的 String 类型 println(str.length) } ``` #### error() 函数 [error()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/error.html) 函数用来标记一个不合法的状态, 或代码中逻辑上不应该发生的条件. 它适合于你想要在你的代码中有意抛出一个异常的场景, 例如, 代码遇到了一个预料之外的状态. 这个函数在 `when` 表达式中特别有用, 它提供了一种清晰的方式, 处理逻辑上不应该发生的情况. 在下面的示例中, 使用了 `error()` 函数来处理一个未定义的用户角色. 如果用户角色不是预定义的角色之一, 就会抛出一个 [IllegalStateException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-illegal-state-exception/) 异常: ```KOTLIN class User(val name: String, val role: String) fun processUserRole(user: User) { when (user.role) { "admin" -> println("${user.name} is an admin.") "editor" -> println("${user.name} is an editor.") "viewer" -> println("${user.name} is a viewer.") else -> error("Undefined role: ${user.role}") } } fun main() { // 这段代码能够正常工作 val user1 = User("Alice", "admin") processUserRole(user1) // 输出结果为 Alice is an admin. // 这段代码会抛出一个 IllegalStateException 异常 val user2 = User("Bob", "guest") processUserRole(user2) } ``` ## 使用 try-catch 代码段处理异常 当一个异常被抛出时, 它会中断程序的正常执行. 你可以使用 `try` 和 `catch` 关键字, 优雅的处理异常, 保持你的程序稳定. `try` 代码段包含可能抛出一个异常的代码, 如果异常发生, `catch` 代码段会捕获并处理异常. 异常会被与异常的类型, 或异常的 [超类](inheritance.html) 匹配第一个 `catch` 代码段捕获. 你可以像下面这样, 共同使用 `try` 和 `catch` 关键字: ```KOTLIN try { // 可能抛出一个异常的代码 } catch (e: SomeException) { // 处理异常的代码 } ``` 将 `try-catch` 作为表达式使用, 是一种常见的方法, 这样就可以从 `try` 代码段或从 `catch` 代码段返回一个值: ```KOTLIN fun main() { val num: Int = try { // 如果 count() 成功结束, 它的返回值会赋值给 num count() } catch (e: ArithmeticException) { // 如果 count() 抛出一个异常, catch 代码段返回 -1, // 然后赋值给 num -1 } println("Result: $num") } // 模拟一个可能抛出 ArithmeticException 异常的函数 fun count(): Int { // 可以修改这个值, 返回不同的值给 num val a = 0 return 10 / a } ``` 你可以对同一个 `try` 代码段使用多个 `catch` 处理块. 你可以根据需要添加任意数量的 `catch` 代码段, 分别处理不同的异常. 如果你使用多个 `catch` 代码段, 要将它们按照从最具体的异常到最不具体的异常的顺序, 在你的代码中排列为从上到下的顺序. 这个排列顺序与程序的执行流程相同. 我们来看看这个使用 [自定义异常](#create-custom-exceptions) 的示例: ```KOTLIN open class WithdrawalException(message: String) : Exception(message) class InsufficientFundsException(message: String) : WithdrawalException(message) fun processWithdrawal(amount: Double, availableFunds: Double) { if (amount > availableFunds) { throw InsufficientFundsException("Insufficient funds for the withdrawal.") } if (amount < 1 || amount % 1 != 0.0) { throw WithdrawalException("Invalid withdrawal amount.") } println("Withdrawal processed") } fun main() { val availableFunds = 500.0 // 请修改这个值, 测试不同的场景 val withdrawalAmount = 500.5 try { processWithdrawal(withdrawalAmount.toDouble(), availableFunds) // 代码段的顺序是很重要的! } catch (e: InsufficientFundsException) { println("Caught an InsufficientFundsException: ${e.message}") } catch (e: WithdrawalException) { println("Caught a WithdrawalException: ${e.message}") } } ``` 处理 `WithdrawalException` 的一般性的 `catch` 代码段, 会捕获这个类型的所有异常, 包括具体的类型, 例如 `InsufficientFundsException`, 除非这些异常被更加具体的 `catch` 代码段在前面捕获. ### finally 代码段 `finally` 代码段包含的代码始终会执行, 无论 `try` 代码段成功结束, 还是抛出一个异常. 使用 `finally` 代码段, 你可以在 `try` 和 `catch` 代码段的执行之后清理代码. 在处理文件或网络连接这样的资源时, 这是非常重要的, 因为 `finally` 可以保证它们被正确的关闭或释放. 共同使用 `try-catch-finally` 代码段的方法通常如下: ```KOTLIN try { // 可能抛出一个异常的代码 } catch (e: YourException) { // 异常处理 } finally { // 始终会执行的代码 } ``` `try` 表达式的返回值, 由 `try` 或 `catch` 代码段中最后执行的表达式决定. 如果没有异常发生, 结果来自 `try` 代码段; 如果一个异常被处理了, 结果就来自 `catch` 代码段. `finally` 代码段始终会被执行, 但它不会改变 `try-catch` 代码段的结果. 我们来看一个示例的演示: ```KOTLIN fun divideOrNull(a: Int): Int { // try 代码段始终会被执行 // 这里发生一个异常(被 0 除), 导致立即跳转到 catch 代码段 try { val b = 44 / a println("try block: Executing division: $b") return b } // catch 代码段会被执行, 因为发生 ArithmeticException 异常 (当 a ==0 时, 会发生被 0 除的错误) catch (e: ArithmeticException) { println("catch block: Encountered ArithmeticException $e") return -1 } finally { println("finally block: The finally block is always executed") } } fun main() { // 修改这个值, 可以得到不同的结果. ArithmeticException 异常会返回: -1 divideOrNull(0) } ``` Note: 在 Kotlin 中, 对于实现了 [AutoClosable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-auto-closeable/) 接口的资源, 例如, `FileInputStream` 或 `FileOutputStream` 之类的文件流, 符合惯用法的管理方法是使用 [.use()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/use.html) 函数. 这个函数会在代码段执行完毕后自动关闭资源, 无论代码段是否抛出异常, 因此不需要使用 `finally` 代码段. 所以, Kotlin 不需要 [Java 的 try-with-resources](https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html) 那样的特殊的语法来进行资源管理. ```KOTLIN FileWriter("test.txt").use { writer -> writer.write("some text") // 在这个代码段之后, .use 函数会自动调用 writer.close(), 与 finally 代码段类似 } ``` 如果你的代码需要清理资源, 但不处理异常, 你也可以使用 `try` 和 `finally` 代码段, 但不使用 `catch` 代码段: ```KOTLIN class MockResource { fun use() { println("Resource being used") // 模拟一个被使用的资源 // 这里发生被 0 除错误, 抛出一个 ArithmeticException 异常 val result = 100 / 0 // 如果抛出异常, 这行不会执行 println("Result: $result") } fun close() { println("Resource closed") } } fun main() { val resource = MockResource() //sampleStart try { // 尝试使用资源 resource.use() } finally { // 即使发生异常, 确保资源始终会被关闭 resource.close() } // 如果抛出异常, 这行不会打印输出 println("End of the program") //sampleEnd } ``` 你可以看到, `finally` 代码段保证资源会被关闭, 无论是否有异常发生. 在 Kotlin 中, 你可以根据需求灵活的选择, 可以只使用 `catch` 代码段, 只使用 `finally` 代码段, 或者两者都使用, 但 `try` 代码段必须伴随至少一个 `catch` 代码段或 `finally` 代码段一起使用. ## 创建自定义异常 在 Kotlin 中, 你可以创建类, 扩展内建的 `Exception` 类, 定义自定义的异常. 通过这种方式, 你可以创建更加具体的错误类型, 以符合你的应用程序的需要. 要创建一个自定义的异常, 你可以定义一个类, 扩展 `Exception` 类: ```KOTLIN class MyException: Exception("My message") ``` 在这个示例中, 指定了默认的错误消息, "My message", 但如果你需要, 你可以不指定默认错误消息. Tip: Kotlin 中的异常是有状态的对象, 带有与它们创建时的上下文环境相关的信息, 称为 [栈追踪(stack trace)](#stack-trace). 不要使用 [对象声明](object-declarations.html#object-declarations-overview) 来创建异常. 相反, 要在每次需要时, 创建异常类的新实例. 通过这种方式, 你可以确保异常的状态准确的反映特定的上下文环境. 自定义异常也可以是任何既有的异常子类的子类, 例如子类 `ArithmeticException`: ```KOTLIN class NumberTooLargeException: ArithmeticException("My message") ``` Note: 如果你想要创建自定义异常的子类, 你必须将父类声明为 `open`, 因为 [类默认为 final](inheritance.html), 不能声明子类. 例如: ```KOTLIN // 将一个自定义异常声明为 open 类, 让它能够声明子类 open class MyCustomException(message: String): Exception(message) // 创建自定义异常的子类 class SpecificCustomException: MyCustomException("Specific error message") ``` 自定义异常的行为与内建的异常是一样的. 你可以使用 `throw` 关键字抛出自定义异常, 并使用 `try-catch-finally` 代码段处理它们. 我们来看一个示例的演示: ```KOTLIN class NegativeNumberException: Exception("Parameter is less than zero.") class NonNegativeNumberException: Exception("Parameter is a non-negative number.") fun myFunction(number: Int) { if (number < 0) throw NegativeNumberException() else if (number >= 0) throw NonNegativeNumberException() } fun main() { // 修改函数中的这个值, 得到不同的异常 myFunction(1) } ``` 在具有多种错误场景的应用程序中, 创建异常类的层级可以让代码更加清晰, 更加具体. 要做到这一点, 你可以使用一个 [抽象类](classes.html#abstract-classes) 或一个 [封闭类](sealed-classes.html#constructors) 作为基类, 实现共通的异常功能, 并为详细的异常类型创建具体的子类. 此外, 带默认值参数的自定义异常提供了一种灵活性, 能够使用不同的消息进行初始化, 实现更加精细的错误处理. 我们来看一个示例, 它使用封闭类 `AccountException` 作为异常类层级的基类, 以及子类 `APIKeyExpiredException` , 演示使用带默认值的参数实现更高级的异常详细信息: ```KOTLIN //sampleStart // 创建一个封闭类, 作为账户相关错误的异常类层级的基类 sealed class AccountException(message: String, cause: Throwable? = null): Exception(message, cause) // 创建 AccountException 的一个子类 class InvalidAccountCredentialsException : AccountException("Invalid account credentials detected") // 创建 AccountException 的一个子类, 能够指定自定义消息和错误原因 class APIKeyExpiredException(message: String = "API key expired", cause: Throwable? = null): AccountException(message, cause) // 修改占位函数的值, 得到不同的结果 fun areCredentialsValid(): Boolean = true fun isAPIKeyExpired(): Boolean = true //sampleEnd // 校验 account 证书 和 API key fun validateAccount() { if (!areCredentialsValid()) throw InvalidAccountCredentialsException() if (isAPIKeyExpired()) { // 示例, 抛出 APIKeyExpiredException, 指定具体的原因 val cause = RuntimeException("API key validation failed due to network error") throw APIKeyExpiredException(cause = cause) } } fun main() { try { validateAccount() println("Operation successful: Account credentials and API key are valid.") } catch (e: AccountException) { println("Error: ${e.message}") e.cause?.let { println("Caused by: ${it.message}") } } } ``` ## Nothing 类型 在 Kotlin 中, 每个表达式都有类型. 表达式 `throw IllegalArgumentException()` 的类型是 [Nothing](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-nothing.html), 这是一个内建类型, 它是所有其它类型的子类型, 也叫做 [底类型(Bottom Type)](https://en.wikipedia.org/wiki/Bottom_type). 也就是说, 在需要其它任何类型的地方, 都可以使用 `Nothing` 作为返回类型, 或泛型类型, 不会导致类型错误. `Nothing` 是 Kotlin 中的一个特殊类型, 用来表示未能成功执行完毕的函数或表达式, 原因可能是它们总是抛出异常, 或者进入了无法终结的执行路径, 例如无限循环. 你可以使用 `Nothing` 来标记还没有实现的函数, 或者设计为总是抛出异常的函数, 向编译器, 也向代码的阅读者, 明确的表示你的意图. 如果编译器在函数签名中推断出 `Nothing` 类型, 它会提出警告. 将返回类型明确的定义为 `Nothing`, 可以消除这个警告. 这段 Kotlin 代码演示 `Nothing` 类型的使用, 这里编译器将函数调用之后的代码标记为不可到达: ```KOTLIN class Person(val name: String?) fun fail(message: String): Nothing { throw IllegalArgumentException(message) // 这个函数永远不会成功返回. // 它始终抛出一个异常. } fun main() { // 创建一个 Person 的实例, 'name' 为 null val person = Person(name = null) val s: String = person.name ?: fail("Name required") // 在这个地方, 's' 可以确保已被初始化 println(s) } ``` Kotlin 的 [TODO()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-t-o-d-o.html) 函数, 也使用 `Nothing` 类型, 用作一个占位符, 用来突出表示未来需要实现的代码区域: ```KOTLIN fun notImplementedFunction(): Int { TODO("This function is not yet implemented") } fun main() { val result = notImplementedFunction() // 这段代码抛出一个 NotImplementedError 异常 println(result) } ``` 你可以看到, `TODO()` 函数永远会抛出一个 [NotImplementedError](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-not-implemented-error/) 异常. ## 异常类 我们来看看 Kotlin 中的一些常见的异常类型, 它们都是 [RuntimeException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-runtime-exception/) 类的子类: * [ArithmeticException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-arithmetic-exception/): 当一个算数操作无法执行时, 会发生这个异常, 例如被 0 除. ```KOTLIN val example = 2 / 0 // 抛出 ArithmeticException 异常 ``` * [IndexOutOfBoundsException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-index-out-of-bounds-exception/): 抛出这个异常表示, 一个某种类型的下标, 例如一个数组或字符串下标, 超出了范围. ```KOTLIN val myList = mutableListOf(1, 2, 3) myList.removeAt(3) // 抛出 IndexOutOfBoundsException 异常 ``` Note: 要避免发生这个异常, 请使用更加安全的替代方案, 例如 [getOrNull()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/get-or-null.html) 函数: ```KOTLIN val myList = listOf(1, 2, 3) // 返回 null, 而不是抛出 IndexOutOfBoundsException 异常 val element = myList.getOrNull(3) println("Element at index 3: $element") ``` * [NoSuchElementException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-no-such-element-exception/): 当一个元素在被访问的集合中不存在时, 会抛出这个异常. 这个错误发生在使用需要特定元素的方法的情况, 例如 [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 val emptyList = listOf() val firstElement = emptyList.first() // 抛出 NoSuchElementException 异常 ``` Note: 要避免发生这个异常, 请使用更加安全的替代方案, 例如 [firstOrNull()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/first-or-null.html) 函数: ```KOTLIN val emptyList = listOf() // 返回 null, 而不是抛出 NoSuchElementException 异常 val firstElement = emptyList.firstOrNull() println("First element in empty list: $firstElement") ``` * [NumberFormatException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-number-format-exception/): 当试图将一个字符串转换为数值类型, 但字符串格式不正确时, 会发生这个异常. ```KOTLIN val string = "This is not a number" val number = string.toInt() // 抛出 NumberFormatException 异常 ``` Note: 要避免发生这个异常, 请使用更加安全的替代方案, 例如 [toIntOrNull()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/to-int-or-null.html) 函数: ```KOTLIN val nonNumericString = "not a number" // 返回 null, 而不是抛出 NumberFormatException 异常 val number = nonNumericString.toIntOrNull() println("Converted number: $number") ``` * [NullPointerException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-null-pointer-exception/): 当一个应用程序尝试使用一个值为 `null` 的对象引用时, 会抛出这个异常. 尽管 Kotlin 的 null 安全性功能大大减少了发生 NullPointerException 的风险, 但仍然可能发生这个异常, 原因可能是有意的使用 `!!` 操作符, 或者与 Java 交互, 而 Java 缺乏 Kotlin 的 null 安全性. ```KOTLIN val text: String? = null println(text!!.length) // 抛出 a NullPointerException 异常 ``` 尽管 Kotlin 的所有异常都是不受控的(unchecked), 而且你不必明确的捕获异常, 但你仍然拥有灵活性, 可以在需要的时候捕获异常. ### 异常的层级结构 Kotlin 异常层级结构的根是 [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/) 类. 它有 2 个直接子类, [Error](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-error/) 和 [Exception](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-exception/): * `Error` 子类表示严重的基础性问题, 应用程序可能无法自行回复. 这些问题你通常不会尝试去处理, 例如 [OutOfMemoryError](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-out-of-memory-error/) 或 `StackOverflowError`. * `Exception` 子类用于你可能想要处理的条件. `Exception` 类型的子类, 例如 [RuntimeException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-runtime-exception/) 和 `IOException` (输入/输出异常), 处理应用程序中的异常事件. ![异常的层级结构 - Throwable 类](images/throwable.svg) `RuntimeException` 通常由程序代码中的检查不足引起, 可以通过编程的方式预防. Kotlin 会帮助阻止常见的 `RuntimeExceptions`, 例如 `NullPointerException`, 并对潜在的运行期错误提供编译期警告, 例如, 被 0 除. 下图描述 `RuntimeException` 的子类型的层级结构: ![Hierarchy of RuntimeExceptions](images/runtime-exception.svg) ## 栈追踪(stack trace) 栈追踪(stack trace) 由运行期环境生成的报告, 用于调试. 它显示导向程序中特定位置的函数调用序列, 尤其是错误异常发生的位置. 我们来看一个示例, 这里由于发生了 JVM 环境中的一个异常, 栈追踪(stack trace) 会自动打印输出: ```KOTLIN fun main() { //sampleStart throw ArithmeticException("This is an arithmetic exception!") //sampleEnd } ``` 在 JVM 环境中运行这段代码, 会产生下面的输出: ```TEXT Exception in thread "main" java.lang.ArithmeticException: This is an arithmetic exception! at MainKt.main(Main.kt:3) at MainKt.main(Main.kt) ``` 第 1 行是异常的描述, 包括: * 异常类型: `java.lang.ArithmeticException` * 线程: `main` * 异常消息: `"This is an arithmetic exception!"` 在异常描述之后, 以 `at` 开始的其它所有行, 是栈追踪(stack trace). 每一行称为一个 栈追踪元素(stack trace element) 或者叫一个 栈帧(stack frame): * `at MainKt.main (Main.kt:3)`: 这行显示方法名称 (`MainKt.main`), 以及调用这个方法的源代码文件和行号 (`Main.kt:3`). * `at MainKt.main (Main.kt)`: 这行显示异常发生在 `Main.kt` 文件的 `main()` 函数内. ## 与 Java, Swift, 和 Objective-C 的异常互操作 由于 Kotlin 将所有异常当作不受控的(unchecked), 因此, 当从区分受控和不受控异常的语言中调用这些异常时, 可能导致复杂的情况. 为了解决 Kotlin 和 Java, Swift, 和 Objective-C 之类语言之间, 对异常处理的这种差异, 你可以使用 [@Throws](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throws/) 注解. 这个注解会警告调用者可能出现的异常. 详情请参见 [在 Java 中调用 Kotlin](java-to-kotlin-interop.html#checked-exceptions) 和 [与 Swift/Objective-C 交互](native-objc-interop.html#errors-and-exceptions). # 函数 在 Kotlin 中声明函数的方法如下: * 使用 `fun` 关键字. * 在括号 `()` 中指定参数. * 如果需要, 包含 [返回值类型](#return-types). 例如: ```KOTLIN //sampleStart // 'double' 是函数名 // 'x' 是 Int 类型的参数 // 预期的返回值类型也是 Int fun double(x: Int): Int { return 2 * x } //sampleEnd fun main() { println(double(5)) // 输出结果为: 10 } ``` ## 函数的使用 调用函数使用标准的方式: ```KOTLIN val result = double(2) ``` 要调用 [成员函数](classes.html) 或 [扩展函数](extensions.html#extension-functions), 请使用点号 `.`: ```KOTLIN // 创建 Stream 类的实例, 然后调用 read() Stream().read() ``` ### 参数 函数参数的定义使用 Pascal 标记法: `name: Type`. 多个参数之间必须使用逗号分隔, 而且每个参数都必须明确指定类型: ```KOTLIN fun powerOf(number: Int, exponent: Int): Int { /*...*/ } ``` 在函数体内部, 接收到的参数是只读的(隐式声明为 `val`): ```KOTLIN fun powerOf(number: Int, exponent: Int): Int { number = 2 // 错误: 'val' 不能被重新赋值. } ``` 声明函数参数时, 可以使用 [尾随逗号(trailing comma)](coding-conventions.html#trailing-commas): ```KOTLIN fun powerOf( number: Int, exponent: Int, // 尾随逗号(trailing comma) ) { /*...*/ } ``` 尾随逗号有助于重构和代码维护: 在声明中移动参数时, 不必担心哪个参数将成为最后一个. Note: Kotlin 函数可以接收其他函数作为参数, 也可以作为参数传递. 详情请参见 [高阶函数与 Lambda 表达式](lambdas.html). ### 带有默认值的参数 你可以为函数参数指定一个默认值, 使参数变为可选参数. 当你在调用函数时不提供对应的参数值时, Kotlin 会使用默认值. 带有默认值的参数也称为 可选参数(optional parameters). 可选参数减少了对多个重载函数的需求, 因为你不必为了允许跳过一个有合理默认值的参数, 而声明不同版本的函数. 在参数声明后追加 `=` 来设置默认值: ```KOTLIN fun read( b: ByteArray, // 'off' 的默认值是 0 off: Int = 0, // 'len' 的默认值由 'b' 数组的大小计算得到 len: Int = b.size, ) { /*...*/ } ``` 如果 有 默认值的参数声明在 没有 默认值的参数 之前, 那么只能通过 [命名参数](#named-arguments) 来使用默认值: ```KOTLIN fun greeting( userId: Int = 0, message: String, ) { /*...*/ } fun main() { // 对 'userId' 使用默认值 0 greeting(message = "Hello!") // 错误: 没有为参数 'userId' 传递值 greeting("Hello!") } ``` [尾随 Lambda 表达式](lambdas.html#passing-trailing-lambdas) 是这个规则的例外情况, 因为最后一个参数必须对应传入的函数: ```KOTLIN fun main () { //sampleStart fun greeting( userId: Int = 0, message: () -> Unit, ) { println(userId) message() } // 对 'userId' 使用默认值 greeting() { println ("Hello!") } // 输出结果为: // 0 // Hello! //sampleEnd } ``` [覆盖方法](inheritance.html#overriding-methods) 总是使用基类方法的默认参数值. 覆盖一个有默认参数值的方法时, 必须在签名中省略默认参数值: ```KOTLIN open class Shape { open fun draw(width: Int = 10, height: Int = 5) { /*...*/ } } class Rectangle : Shape() { // 这里不允许指定默认值 // 但这个函数对 'width' 默认使用 10, 对 'height' 默认使用 5. override fun draw(width: Int, height: Int) { /*...*/ } } ``` #### 使用非常量表达式作为默认值 你可以为参数赋予一个非常量的默认值. 例如, 默认值可以是一个函数调用的结果, 或者使用其他参数值进行计算的结果, 就像这个示例中的 `len` 参数: ```KOTLIN fun read( b: ByteArray, off: Int = 0, len: Int = b.size, ) { /*...*/ } ``` 参数如果引用其他参数的值, 必须在声明顺序上位于后面. 在这个示例中, `len` 必须声明在 `b` 之后. 一般来说, 你可以将任意表达式赋值给参数的默认值. 但是, 只有在调用函数时 没有 传入对应参数, 需要赋予默认值的情况下, 默认值才会被计算. 例如, 以下函数只有在调用时没有传入 `print` 参数的情况下, 才会打印输出一行: ```KOTLIN fun main() { //sampleStart fun read( b: Int, print: Unit? = println("No argument passed for 'print'") ) { println(b) } // 先打印 "No argument passed for 'print'", 然后打印 "1" read(1) // 只打印 "1" read(1, null) //sampleEnd } ``` 如果函数声明中的最后一个参数是函数类型, 你可以将对应的 [Lambda 表达式](lambdas.html#lambda-expression-syntax) 参数以命名参数的方式传递, 也可以 [在括号之外传递](lambdas.html#passing-trailing-lambdas): ```KOTLIN fun main() { //sampleStart fun log( level: Int = 0, code: Int = 1, action: () -> Unit, ) { println (level) println (code) action() } // 对 'level' 传入 1, 对 'code' 使用默认值 1 log(1) { println("Connection established") } // 对 'level' 和 'code' 都使用默认值, 分别为 0 和 1 log(action = { println("Connection established") }) // 与前一次调用等价, 使用两个默认值 log { println("Connection established") } //sampleEnd } ``` ### 命名参数 调用函数时, 你可以指定一个或多个参数名. 当函数调用存在很多参数时, 这个功能会非常有用. 这种情况下, 很难将参数值与参数对应起来, 尤其是如果参数值是 `null` 或布尔值. 在函数调用中使用命名参数时, 可以按任意顺序列出这些参数. 比如, `reformat()` 函数有 4 个带有默认值的参数: ```KOTLIN fun reformat( str: String, normalizeCase: Boolean = true, upperCaseFirstLetter: Boolean = true, divideByCamelHumps: Boolean = false, wordSeparator: Char = ' ', ) { /*...*/ } ``` 调用这个函数时, 你可以对部分参数进行命名: ```KOTLIN reformat( "String!", normalizeCase = false, upperCaseFirstLetter = false, divideByCamelHumps = true, '_' ) ``` 可以省略所有那些带有默认值的参数: ```KOTLIN reformat("This is a long String!") ``` 也可以只省略带默认值的参数中 某些 参数, 而不是省略全部. 但是, 在第一个省略的参数之后, 必须对后续的所有参数指定命名: ```KOTLIN reformat( "This is a short String!", upperCaseFirstLetter = false, wordSeparator = '_' ) ``` 你可以通过命名对应的参数, 传递 [不定数量参数](#variable-number-of-arguments-varargs) (`vararg`). 在这个示例中, 参数是一个数组: ```KOTLIN fun mergeStrings(vararg strings: String) { /*...*/ } mergeStrings(strings = arrayOf("a", "b", "c")) ``` Note: 在 JVM 平台调用 Java 函数时, 不能使用命名参数语法, 因为 Java 字节码并不一定保留了函数参数的名称信息. ### 返回值类型 当你声明一个带有代码块体(将指令放在大括号 `{}` 内) 的函数时, 必须始终明确指定返回值类型. 唯一例外是函数返回 `Unit` 的情况, [这时指定返回值类型是可选的](#unit-returning-functions). Kotlin 不会推断代码块体函数的返回值类型. 这类函数的控制流可能比较复杂, 使得返回值类型对读者来说不够清晰, 有时甚至对编译器来说也是如此. 但是, 对于 [单表达式函数](#single-expression-functions), Kotlin 可以在你不指定的情况下推断返回值类型. ### 单表达式函数(Single-expression function) 当函数体只包含单个表达式时, 可以省略大括号, 在 `=` 之后直接指定函数体: ```KOTLIN fun double(x: Int): Int = x * 2 ``` 大多数情况下不必明确声明 [返回值类型](#return-types): ```KOTLIN // 编译器推断这个函数的返回值类型为 Int fun double(x: Int) = x * 2 ``` 编译器在从单个表达式推断返回值类型时, 有时会遇到问题. 在这种情况下, 你应该明确添加返回值类型. 例如, 递归或互相递归的函数(互相调用对方)以及类似 `fun empty() = null` 这样没有类型的表达式函数, 总是需要返回值类型. 当你使用推断的返回值类型时, 请确保检查实际结果, 因为编译器推断的类型可能对你来说不够理想. 在上面的示例中, 如果你希望 `double()` 函数返回 `Number` 而不是 `Int`, 必须明确声明这一点. ### 返回值为 Unit 的函数 如果一个函数有代码块体(大括号 `{}` 内的指令), 而且不返回有意义的值, 编译器会假设其返回值类型是 `Unit`. `Unit` 是一种只有一个值的类型, 这个值也叫做 `Unit`. 除了函数类型参数之外, 你不必指定 `Unit` 作为返回值类型. 你永远不必显式地 return `Unit`. 例如, 你可以声明一个 `printHello()` 函数, 不必返回 `Unit`: ```KOTLIN // 函数类型参数('action')的声明仍然需要明确的返回值类型 fun printHello(name: String?, action: () -> Unit) { if (name != null) println("Hello $name") else println("Hi there!") action() } fun main() { printHello("Kodee") { println("This runs after the greeting.") } // 输出结果为: // Hello Kodee // This runs after the greeting. printHello(null) { println("No name provided, but action still runs.") } // 输出结果为: No name provided, but action still runs } ``` 这段代码与下面这段冗长的声明是等价的: ```KOTLIN //sampleStart fun printHello(name: String?, action: () -> Unit): Unit { if (name != null) println("Hello $name") else println("Hi there!") action() return Unit } //sampleEnd fun main() { printHello("Kodee") { println("This runs after the greeting.") } // 输出结果为: // Hello Kodee // This runs after the greeting. printHello(null) { println("No name provided, but action still runs.") } // 输出结果为: No name provided, but action still runs } ``` 如果函数的返回值类型已明确指定, 你可以在表达式体中使用 `return` 语句: ```KOTLIN fun getDisplayNameOrDefault(userId: String?): String = getDisplayName(userId ?: return "default") ``` ### 不定数量参数(varargs) 要向函数传递不定数量的参数, 你可以对其中一个参数(通常是最后一个)标记 `vararg` 修饰符. 在函数内部, 你可以将类型为 `T` 的 `vararg` 参数用作 `T` 类型的数组: ```KOTLIN fun asList(vararg ts: T): List { val result = ArrayList() for (t in ts) // ts 是一个 Array result.add(t) return result } ``` 然后你就可以向函数传递不定数量的参数: ```KOTLIN fun asList(vararg ts: T): List { val result = ArrayList() for (t in ts) // ts 是一个 Array result.add(t) return result } fun main() { //sampleStart val list = asList(1, 2, 3) println(list) // 输出结果为: [1, 2, 3] //sampleEnd } ``` 只有一个参数可以标记为 `vararg`. 如果你在参数列表末尾以外的位置声明了 `vararg` 参数, 则必须使用命名参数对之后的参数传值. 如果参数是函数类型, 也可以在括号之外放置 Lambda 表达式来传递值. 调用 `vararg` 函数时, 可以逐个传递参数, 就像 `asList(1, 2, 3)` 的例子. 如果你已有一个数组, 希望将其内容作为 `vararg` 参数, 或这个参数的一部分, 传递给函数, 请使用 [展开(spread)操作符](arrays.html) (在数组名前加 `*` 前缀): ```KOTLIN fun asList(vararg ts: T): List { val result = ArrayList() for (t in ts) result.add(t) return result } fun main() { //sampleStart val a = arrayOf(1, 2, 3) // 函数接收的数组是 [-1, 0, 1, 2, 3, 4] list = asList(-1, 0, *a, 4) println(list) // 输出结果为: [-1, 0, 1, 2, 3, 4] //sampleEnd } ``` 如果要向 `vararg` 参数传递 [基本类型的数组](arrays.html#primitive-type-arrays), 你需要使用 [.toTypedArray()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/to-typed-array.html) 函数, 将它转换为一个通常的(有类型的)数组: ```KOTLIN // 'a' 是 IntArray, 它是基本类型的数组 val a = intArrayOf(1, 2, 3) val list = asList(-1, 0, *a.toTypedArray(), 4) ``` ### 中缀标记法(Infix notation) 你可以使用 `infix` 关键字声明函数, 使其可以不用括号或点号调用. 这有助于使代码中简单的函数调用更易于阅读. ```KOTLIN infix fun Int.shl(x: Int): Int { /*...*/ } // 使用通常的标记法调用函数 1.shl(2) // 使用中缀标记法调用函数 1 shl 2 ``` 中缀函数需要满足以下条件: * 必须是类的成员函数, 或者是 [扩展函数](extensions.html). * 必须只有单个参数. * 参数不能是 [不定数量参数](#variable-number-of-arguments-varargs) (`vararg`), 而且不能有 [默认值](#parameters-with-default-values). Note: 中缀函数调用的优先级, 低于算数运算符, 类型转换, 以及 `rangeTo` 运算符. 以下表达式是等价的: * `1 shl 2 + 3` 等价于 `1 shl (2 + 3)` * `0 until n * 2` 等价于 `0 until (n * 2)` * `xs union ys as Set<*>` 等价于 `xs union (ys as Set<*>)` 另一方面, 中缀函数调用的优先级, 高于布尔值运算符 `&&` 和 `||`, `is` 和 `in` 检查, 以及其他运算符. 以下表达式是等价的: * `a && b xor c` 等价于 `a && (b xor c)` * `a xor b in c` 等价于 `(a xor b) in c` 注意, 中缀函数的接受者和参数都需要明确指定. 使用中缀标记法调用当前接受者的方法时, 请明确使用 `this`. 这确保了语法解析不会出现歧义. ```KOTLIN class MyStringCollection { val items = mutableListOf() infix fun add(s: String) { println("Adding: $s") items += s } fun build() { add("first") // 正确: 普通函数调用 this add "second" // 正确: 带有明确接受者的中缀调用 // add "third" // 编译错误: 需要明确指定接受者 } fun printAll() = println("Items = $items") } fun main() { val myStrings = MyStringCollection() // 向列表添加 "first" 和 "second" myStrings.build() myStrings.printAll() // 输出结果为: // Adding: first // Adding: second // Items = [first, second] } ``` ## 函数的范围 你可以在文件的顶层声明 Kotlin 函数, 这意味着不需要创建类来容纳函数. 函数也可以在局部范围内, 声明为 成员函数 或 扩展函数. ### 局部函数 Kotlin 支持局部函数, 也就是在其他函数内部声明的函数. 例如, 以下代码为给定的图实现了深度优先搜索算法. 外层 `dfs()` 函数内部的局部函数 `dfs()` 用于隐藏实现细节, 并处理递归调用: ```KOTLIN class Person(val name: String) { val friends = mutableListOf() } class SocialGraph(val people: List) //sampleStart fun dfs(graph: SocialGraph) { fun dfs(current: Person, visited: MutableSet) { if (!visited.add(current)) return println("Visited ${current.name}") for (friend in current.friends) dfs(friend, visited) } dfs(graph.people[0], HashSet()) } //sampleEnd fun main() { val alice = Person("Alice") val bob = Person("Bob") val charlie = Person("Charlie") alice.friends += bob bob.friends += charlie charlie.friends += alice val network = SocialGraph(listOf(alice, bob, charlie)) dfs(network) } ``` 局部函数可以访问外层函数中的局部变量(闭包). 在上面的例子中, `visited` 函数参数可以作为一个局部变量: ```KOTLIN class Person(val name: String) { val friends = mutableListOf() } class SocialGraph(val people: List) //sampleStart fun dfs(graph: SocialGraph) { val visited = HashSet() fun dfs(current: Person) { if (!visited.add(current)) return println("Visited ${current.name}") for (friend in current.friends) dfs(friend) } dfs(graph.people[0]) } //sampleEnd fun main() { val alice = Person("Alice") val bob = Person("Bob") val charlie = Person("Charlie") alice.friends += bob bob.friends += charlie charlie.friends += alice val network = SocialGraph(listOf(alice, bob, charlie)) dfs(network) } ``` ### 成员函数 成员函数是指定义在类或对象之内的函数: ```KOTLIN class Sample { fun foo() { print("Foo") } } ``` 要调用成员函数, 请写出实例或对象名, 然后添加 `.` 并写出函数名: ```KOTLIN // 创建 Stream 类的实例, 然后调用 read() Stream().read() ``` 关于类, 以及成员覆盖, 详情请参见 [类](classes.html) 和 [继承](classes.html#inheritance). ## 泛型函数 可以在函数名称之前使用尖括号 `<>` 为函数指定泛型参数: ```KOTLIN fun singletonList(item: T): List { /*...*/ } ``` 关于泛型函数, 详情请参见 [泛型](generics.html). ## 尾递归(Tail Recursive)函数 Kotlin 支持一种称为 [尾递归(Tail Recursion)](https://en.wikipedia.org/wiki/Tail_call) 的函数式编程方式. 对于某些算法, 本来需要使用循环来实现, 你可以改用递归函数, 但同时不会存在栈溢出(stack overflow)的风险. 当一个函数标记为 `tailrec`, 并且满足某些形式上的要求, 编译器就会对代码进行优化, 消除函数的递归调用, 产生一段基于循环实现的, 快速而且高效的代码: ```KOTLIN import kotlin.math.cos import kotlin.math.abs // 任意设定的"足够好"的精度 val eps = 1E-10 tailrec fun findFixPoint(x: Double = 1.0): Double = if (abs(x - cos(x)) < eps) x else findFixPoint(cos(x)) ``` 上面的代码计算余弦函数的不动点(一个数学上的常数). 函数从 `1.0` 开始不断重复地调用 `cos()`, 直到计算结果不再变化为止, 对于示例中给定的 `eps` 精度值, 计算结果将是 `0.7390851332151611`. 上面的代码等价于下面这种传统方式编写的代码: ```KOTLIN import kotlin.math.cos import kotlin.math.abs // 任意设定的"足够好"的精度 val eps = 1E-10 private fun findFixPoint(): Double { var x = 1.0 while (true) { val y = cos(x) if (abs(x - y) < eps) return x x = cos(x) } } ``` 只有当函数在其最终操作中调用自身时, 才可以对其应用 `tailrec` 修饰符. 如果在递归调用之后还存在其他代码, 那么不能使用尾递归, 在 [try/catch/finally 代码块](exceptions.html#handle-exceptions-using-try-catch-blocks) 内, 或当函数是 [open](inheritance.html) 的情况下, 也不能使用尾递归. 参见: * [内联函数(Inline Function)](inline-functions.html) * [扩展函数](extensions.html) * [高阶函数(Higher-Order Function) 与 Lambda 表达式](lambdas.html) # 高阶函数与 Lambda 表达式 在 Kotlin 中函数是 [一级公民](https://en.wikipedia.org/wiki/First-class_function), 也就是说, 函数可以保存在变量和数据结构中, 也可以作为参数来传递给 [高阶函数](#higher-order-functions), 也可以作为 [高阶函数](#higher-order-functions) 的返回值. 你可以就像对函数之外的其他数据类型值一样, 对函数执行任意的操作. 为了实现这些功能, Kotlin 作为一种静态类型语言, 使用了一组 [函数类型](#function-types) 来表达函数, 并提供了一组专门的语言结构, 比如 [lambda 表达式](#lambda-expressions-and-anonymous-functions). ## 高阶函数(Higher-Order Function) 高阶函数(higher-order function)是一种特殊的函数, 它接受函数作为参数, 或者返回一个函数. 高阶函数的一个很好的例子就是 [函数式编程(functional programming) 中对集合的 折叠(fold)](https://en.wikipedia.org/wiki/Fold_(higher-order_function)), 这个折叠函数的参数是一个初始的累计值, 以及一个结合函数, 然后将累计值与集合中的各个元素逐个结合, 最终得到结果值: ```KOTLIN fun Collection.fold( initial: R, combine: (acc: R, nextElement: T) -> R ): R { var accumulator: R = initial for (element: T in this) { accumulator = combine(accumulator, element) } return accumulator } ``` 上面的示例代码中, `combine` 参数是 [函数类型](#function-types) `(R, T) -> R`, 所以这个参数接受一个函数, 函数又接受两个参数, 类型为 `R` 和 `T`, 返回值类型为 `R`. 这个函数在 `for` 循环内被 [调用](#invoking-a-function-type-instance), 函数的返回值被赋值给 `accumulator`. 要调用上面的 `fold` 函数, 你需要向它传递一个 [函数类型的实例](#instantiating-a-function-type) 作为参数, 在调用高阶函数时, 我们经常使用 Lambda 表达式作为这种参数 (详细介绍请参见 [后面的章节](#lambda-expressions-and-anonymous-functions)): ```KOTLIN fun main() { //sampleStart val items = listOf(1, 2, 3, 4, 5) // Lambda 表达式是大括号括起的那部分代码. items.fold(0, { // 如果 Lambda 表达式有参数, 首先声明这些参数, 后面是 '->' 符 acc: Int, i: Int -> print("acc = $acc, i = $i, ") val result = acc + i println("result = $result") // Lambda 表达式内的最后一个表达式会被看作返回值: result }) // Lambda 表达式的参数类型如果可以推断得到, 那么参数类型的声明可以省略: val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i }) // 在高阶函数调用中也可以使用函数引用: val product = items.fold(1, Int::times) //sampleEnd println("joinedToString = $joinedToString") println("product = $product") } ``` ## 函数类型(Function Type) 为了在类型和参数声明中处理函数, 比如: `val onClick: () -> Unit = ...` , Kotlin 使用函数类型(Function Type), 比如 `(Int) -> String` . 这种函数类型使用一种特殊的表示方法, 用于表示函数的签名部分 - 也就是表示函数的参数和返回值: * 所有的函数类型都带有参数类型列表, 用括号括起, 以及返回值类型: `(A, B) -> C` 表示一个函数类型, 它接受两个参数, 类型为 `A` 和 `B`, 返回值类型为 `C`. 参数类型列表可以为空, 比如 `() -> A`. [Unit 类型的返回值](functions.html#unit-returning-functions) 不能省略. * 函数类型也可以带一个额外的 接受者 类型, 以点号标记, 放在函数类型声明的前部: `A.(B) -> C` 表示一个可以对类型为 `A` 的接受者调用的函数, 参数类型为`B`, 返回值类型为 `C`. 对这种函数类型, 我们经常使用 [带接受者的函数字面值](#function-literals-with-receiver). * [挂起函数(Suspending function)](coroutines-basics.html) 是一种特殊类型的函数, 它的声明带有一个特殊的 suspend 修饰符, 比如: `suspend () -> Unit`, 或者: `suspend A.(B) -> C`. 函数类型的声明也可以指定函数参数的名称: `(x: Int, y: Int) -> Point`. 参数名称可以用来更好地说明参数含义. 为了表示函数类型是 [可以为 null 的](null-safety.html#nullable-types-and-non-nullable-types), 可以使用括号: `((Int, Int) -> Int)?`. 函数类型也可以使用括号组合在一起: `(Int) -> ((Int) -> Unit)` Note: 箭头符号的结合顺序是右侧优先, `(Int) -> (Int) -> Unit` 的含义与上面的例子一样, 而不同于: `((Int) -> (Int)) -> Unit`. 你也可以使用 [类型别名](type-aliases.html) 来给函数类型指定一个名称: ```KOTLIN typealias ClickHandler = (Button, ClickEvent) -> Unit ``` ### 创建函数类型的实例 有几种不同的方法可以创建函数类型的实例: * 使用函数字面值, 采用以下形式之一: * [Lambda 表达式](#lambda-expressions-and-anonymous-functions): `{ a, b -> a + b }`, * [匿名函数(Anonymous Function)](#anonymous-functions): `fun(s: String): Int { return s.toIntOrNull() ?: 0 }` [带接受者的函数字面值](#function-literals-with-receiver) 可以用作带接受者的函数类型的实例. * 使用已声明的元素的可调用的引用: * 顶级[函数](reflection.html#function-references), 局部[函数](reflection.html#function-references), 成员[函数](reflection.html#function-references), 或扩展[函数](reflection.html#function-references), 比如: `::isOdd`, `String::toInt`, * 顶级[属性](reflection.html#property-references), 成员[属性](reflection.html#property-references), 或扩展[属性](reflection.html#property-references), 比如: `List::size`, * [构造器](reflection.html#constructor-references), 比如: `::Regex` 以上几种形式都包括 [绑定到实例的可调用的引用](reflection.html#bound-function-and-property-references), 也就是指向具体实例的成员的引用: `foo::toString`. * 使用自定义类, 以接口的方式实现函数类型: ```KOTLIN class IntTransformer: (Int) -> Int { override operator fun invoke(x: Int): Int = TODO() } val intFunction: (Int) -> Int = IntTransformer() ``` 如果有足够的信息, 编译器可以推断出变量的函数类型: ```KOTLIN val a = { i: Int -> i + 1 } // 编译器自动推断得到的类型为 (Int) -> Int ``` 带接受者和不带接受者的函数类型的 非字面 值是可以互换的, 也就是说, 接受者可以代替第一个参数, 反过来第一个参数也可以代替接受者. 比如, 如果参数类型或变量类型为 `A.(B) -> C`, 那么可以使用 `(A, B) -> C` 函数类型的值, 反过来也是如此: ```KOTLIN fun main() { //sampleStart val repeatFun: String.(Int) -> String = { times -> this.repeat(times) } val twoParameters: (String, Int) -> String = repeatFun // OK fun runTransformation(f: (String, Int) -> String): String { return f("hello", 3) } val result = runTransformation(repeatFun) // OK //sampleEnd println("result = $result") } ``` Note: 注意, 自动推断的结果默认是不带接受者的函数类型, 即使给变量初始化赋值为一个扩展函数的引用, 也是如此. 要改变这种结果, 你需要明确指定变量类型. ### 调用一个函数类型的实例 要调用一个函数类型的值, 可以使用它的 [invoke(...) 操作符](operator-overloading.html#invoke-operator): `f.invoke(x)`, 或者直接写 `f(x)`. 如果函数类型值有接受者, 那么接受者对象实例应该作为第一个参数传递进去. 调用有接受者的函数类型值的另一种方式是, 将接受者写作函数调用的前缀, 就像调用 [扩展函数](extensions.html) 一样: `1.foo(2)`. 示例: ```KOTLIN fun main() { //sampleStart val stringPlus: (String, String) -> String = String::plus val intPlus: Int.(Int) -> Int = Int::plus println(stringPlus.invoke("<-", "->")) println(stringPlus("Hello, ", "world!")) println(intPlus.invoke(1, 1)) println(intPlus(1, 2)) println(2.intPlus(3)) // 与扩展函数类似的调用方式 //sampleEnd } ``` ### 内联函数(Inline Function) 有些时候, 使用 [内联函数](inline-functions.html) 可以为高阶函数实现更加灵活的控制流程. ## Lambda 表达式与匿名函数(Anonymous Function) Lambda 表达式和匿名函数, 都是 函数字面值(function literal), 函数字面值没有像普通函数那样声明, 而是立即作为表达式传递出去. 看看下面的示例: ```KOTLIN max(strings, { a, b -> a.length < b.length }) ``` 函数 `max` 是一个高阶函数, 因为它接受一个函数值作为第二个参数. 第二个参数是一个表达式, 本身又是另一个函数, 称为函数字面值. 这个函数字面值等价于下面这个有名称的函数: ```KOTLIN fun compare(a: String, b: String): Boolean = a.length < b.length ``` 你也可以使用 `suspend` 关键字创建一个 挂起的 Lambda 表达式. 挂起的 Lambda 表达式的函数类型是 `suspend () -> Unit`, 它可以调用其它挂起函数: ```KOTLIN val suspendingTask = suspend { doSuspendingWork() } ``` ### Lambda 表达式的语法 Lambda 表达式的完整语法形式如下: ```KOTLIN val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y } ``` * Lambda 表达式包含在大括号之内. * 在完整语法形式中, 参数声明在大括号之内, 参数类型的声明是可选的. * 函数体在 `->` 符号之后. * 如果 Lambda 表达式自动推断的返回值类型不是 `Unit`, 那么 Lambda 表达式函数体中, 最后一条(或者就是唯一一条)表达式的值, 会被当作整个 Lambda 表达式的返回值. 如果把所有可选的内容都去掉, 那么剩余的部分如下: ```KOTLIN val sum = { x: Int, y: Int -> x + y } ``` ### 函数调用时使用尾缀 Lambda 表达式 根据 Kotlin 的编码规约, 如果函数的最后一个参数是一个函数, 那么如果使用 Lambda 表达式作为这个参数的值, 可以将 Lambda 表达式写在函数调用的括号之外: ```KOTLIN val product = items.fold(1) { acc, e -> acc * e } ``` 这种语法又称为 尾缀 Lambda 表达式(Trailing Lambda). 如果 Lambda 表达式是函数调用时的唯一一个参数, 括号可以完全省略: ```KOTLIN run { println("...") } ``` ### it: 单一参数的隐含名称 很多情况下 Lambda 表达式只有唯一一个参数. 如果编译器能够识别出 Lambda 表达式没有参数定义, 那么可以不必声明参数, 并省略 `->` 符号. 这个参数会隐含地声明, 参数名为 `it`: ```KOTLIN ints.filter { it > 0 } // 这个函数字面值的类型是 '(it: Int) -> Boolean' ``` ### 从 Lambda 表达式中返回结果值 如果使用 [带标签限定的 return](returns.html#return-to-labels) 语法, 你可以在 Lambda 表达式内明确地返回一个结果值. 否则, 会隐含地返回 Lambda 表达式内最后一条表达式的值. 因此, 下面两段代码是等价的: ```KOTLIN ints.filter { val shouldFilter = it > 0 shouldFilter } ints.filter { val shouldFilter = it > 0 return@filter shouldFilter } ``` 使用这个规约, 再加上 [在括号之外传递 Lambda 表达式作为函数调用的参数](#passing-trailing-lambdas), 我们可以编写 [LINQ 风格](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/) 的程序: ```KOTLIN strings.filter { it.length == 5 }.sortedBy { it }.map { it.uppercase() } ``` ### 使用下划线代替未使用的参数 如果 Lambda 表达式的某个参数未被使用, 你可以用下划线来代替参数名: ```KOTLIN map.forEach { (_, value) -> println("$value!") } ``` ### 在 Lambda 表达式中使用解构声明 关于在 Lambda 表达式中使用解构声明, 请参见 [解构声明(destructuring declaration)](destructuring-declarations.html#destructuring-in-lambdas). ### 匿名函数(Anonymous Function) 上面讲到的 Lambda 表达式语法, 还缺少了一种功能, 就是如何指定函数的返回值类型. 大多数情况下, 不需要指定返回值类型, 因为可以自动推断得到. 但是, 如果的确需要明确指定返回值类型, 你可以可以选择另一种语法: 匿名函数(anonymous function). ```KOTLIN fun(x: Int, y: Int): Int = x + y ``` 匿名函数看起来与通常的函数声明很类似, 区别在于省略了函数名. 函数体可以是一个表达式(如上例), 也可以是多条语句组成的代码段: ```KOTLIN fun(x: Int, y: Int): Int { return x + y } ``` 参数和返回值类型的声明与通常的函数一样, 但如果参数类型可以通过上下文推断得到, 那么类型声明可以省略: ```KOTLIN ints.filter(fun(item) = item > 0) ``` 对于匿名函数, 返回值类型的自动推断方式与通常的函数一样: 如果函数体是一个表达式, 那么返回值类型可以自动推断得到, 但如果函数体是多条语句组成的代码段, 则返回值类型必须明确指定(否则被认为是 `Unit`). Note: 匿名函数当作参数传递时, 一定要放在函数调用的圆括号内. 允许将函数类型参数写在圆括号之外的语法, 仅对 Lambda 表达式有效. Lambda 表达式与匿名函数之间的另一个区别是, 它们的 [非局部返回(non-local return)](inline-functions.html#returns) 的行为不同. 不使用标签的 `return` 语句总是从 `fun` 关键字定义的函数中返回. 也就是说, Lambda 表达式内的 `return` 将会从包含这个 Lambda 表达式的函数中返回, 而匿名函数内的 `return` 只会从匿名函数本身返回. ### 闭包(Closure) Lambda 表达式, 匿名函数 (此外还有 [局部函数](functions.html#local-functions), [对象表达式](object-declarations.html#object-expressions)) 可以访问它的 闭包, 也就是, 定义在外层范围中的变量. 闭包中捕获的变量在 Lambda 表达式内是可以修改的: ```KOTLIN var sum = 0 ints.filter { it > 0 }.forEach { sum += it } print(sum) ``` ### 带有接受者的函数字面值 带接受者的 [函数类型](#function-types), 比如 `A.(B) -> C`, 可以通过一种特殊形式的函数字面值来创建它的实例, 也就是带接受者的函数字面值. 上文讲到, Kotlin 提供了一种能力, 可以指定一个 接收者对象(receiver object), 来 [调用带接受者的函数类型的实例](#invoking-a-function-type-instance). 在这个函数字面值的函数体内部, 传递给这个函数调用的接受者对象会成为一个 隐含的 `this`, 因此你可以访问接收者对象的成员, 而不必指定任何限定符, 也可以使用 [this 表达式](this-expressions.html) 来访问接受者对象. 这种行为很类似于 [扩展函数](extensions.html), 在扩展函数的函数体中, 你也可以访问接收者对象的成员. 下面的例子演示一个带接受者的函数字面值, 以及这个函数字面值的类型, 在函数体内部, 调用了接受者对象的 `plus` 方法: ```KOTLIN val sum: Int.(Int) -> Int = { other -> plus(other) } ``` 匿名函数语法允许你直接指定函数字面值的接受者类型. 如果你需要声明一个带接受者的函数类型变量, 然后在将来的某个地方使用它, 那么这种功能就很有用. ```KOTLIN val sum = fun Int.(other: Int): Int = this + other ``` 如果接受者类型可以通过上下文自动推断得到, 那么 Lambda 表达式也可以用做带接受者的函数字面值. 这种用法的一个重要例子就是 [类型安全的构建器(Type-Safe Builder)](type-safe-builders.html): ```KOTLIN class HTML { fun body() { ... } } fun html(init: HTML.() -> Unit): HTML { val html = HTML() // 创建接受者对象 html.init() // 将接受者对象传递给 Lambda 表达式 return html } html { // 带接受者的 Lambda 表达式从这里开始 body() // 调用接受者对象上的一个方法 } ``` # this 表达式 为了表示当前函数的 接收者(receiver), 你可以使用 `this` 表达式: * 在 [类](classes.html#inheritance) 的成员函数中, `this` 指向这个类的当前对象实例. * 在 [扩展函数](extensions.html) 中, 或 [带接收者的函数字面值(function literal)](lambdas.html#function-literals-with-receiver) 中, `this` 代表调用函数时, 在点号左侧传递的 接收者 参数. 如果 `this` 没有限定符, 那么它指向 包含当前代码的最内层范围. 如果想要指向其他范围内的 `this`, 需要使用 标签限定符: ## 带限定符的 this 为了访问更外层范围(比如 [类](classes.html), 或 [扩展函数](extensions.html), 或有标签的 [带接受者的函数字面值](lambdas.html#function-literals-with-receiver))内的 `this`, 你可以使用 `this@label`, 其中的 `@label` 是一个 [标签](returns.html), 代表你想要访问的 `this` 所属的范围: ```KOTLIN class A { // 隐含的标签 @A inner class B { // 隐含的标签 @B fun Int.foo() { // 隐含的标签 @foo val a = this@A // 指向 A 的 this val b = this@B // 指向 B 的 this val c = this // 指向 foo() 函数的接受者, 一个 Int 值 val c1 = this@foo // 指向 foo() 函数的接受者, 一个 Int 值 val funLit = lambda@ fun String.() { val d = this // 指向 funLit 的接受者, 一个 String 值 } val funLit2 = { s: String -> // 指向 foo() 函数的接受者, 因为包含当前代码的 Lambda 表达式没有接受者 val d1 = this } } } } ``` ## 隐含的 this 在 `this` 上调用成员函数时, 可以省略 `this.` 部分. 如果你有一个非成员函数使用了相同的名称, 那么使用时要小心, 因为某些情况下会调用到非成员函数: ```KOTLIN fun main() { fun printLine() { println("Local function") } class A { fun printLine() { println("Member function") } fun invokePrintLine(omitThis: Boolean = false) { if (omitThis) printLine() else this.printLine() } } A().invokePrintLine() // 输出结果为: Member function A().invokePrintLine(omitThis = true) // 输出结果为: Local function } ``` # 类型安全的构建器 通过将恰当命名的函数用做构建器, 结合 [带接受者的函数字面值](lambdas.html#function-literals-with-receiver), 我们可以在 Kotlin 中创建出类型安全的, 静态类型的构建器. 类型安全的构建器(Type-safe builder) 可以用来创建基于 Kotlin 的, 特定领域专用语言(Domain-Specific Language, DSL), 这些语言适合于使用半声明的方式创建复杂的层级式数据结构. 比如, 构建器的一些应用场景包括: * 使用 Kotlin 代码来生成标记式语言, 比如 [HTML](https://github.com/Kotlin/kotlinx.html) 或 XML * 为 Web 服务器配置路由: [Ktor](https://ktor.io/docs/routing.html) 我们来看看以下代码: ```KOTLIN package html fun main() { //sampleStart val result = html { head { title { +"HTML encoding with Kotlin" } } body { h1 { +"HTML encoding with Kotlin" } p { +"this format can be used as an" +"alternative markup to HTML" } // 一个元素, 指定了属性, 还指定了其中的文本内容 a(href = "http://kotlinlang.org") { +"Kotlin" } // 混合内容 p { +"This is some" b { +"mixed" } +"text. For more see the" a(href = "http://kotlinlang.org") { +"Kotlin" } +"project" } p { +"some text" ul { for (i in 1..5) li { +"${i}*2 = ${i*2}" } } } } } //sampleEnd println(result) } 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 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\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 ul(init: UL.() -> Unit) = initTag(UL(), init) fun a(href: String, init: A.() -> Unit) { val a = initTag(A(), init) a.href = href } } class Body() : BodyTag("body") class UL() : BodyTag("ul") { fun li(init: LI.() -> Unit) = initTag(LI(), init) } class B() : BodyTag("b") class LI() : BodyTag("li") 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 } ``` ``` HTML encoding with Kotlin

HTML encoding with Kotlin

this format can be used as an alternative markup to HTML

Kotlin

This is some mixed text. For more see the Kotlin project

some text

  • 1*2 = 2
  • 2*2 = 4
  • 3*2 = 6
  • 4*2 = 8
  • 5*2 = 10

``` ## 工作原理 假设你需要用 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\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 可见的接口或抽象类, 那么就不能禁止其他开发者在他们的代码中实现这些接口或扩展这些抽象类. 由于库不知道在它外部定义的错误类, 因此库不能像它自己定义的类那样一致的处理这些外部定义的类. 如果将错误类的继承阶层封闭起来, 库的作者就能够确定的知道所有可能的错误类型, 并且能够确定以后不会出现其他错误类型. 但是, 使用 封闭的 错误类层级结构, 库的作者就能够确定他们知道了所有可能的错误类型, 而且之后也不会出现其他的错误类型. 示例代码中的层级关系如下: ![封闭类和封闭接口层级结构示意图](images/sealed-classes-interfaces.svg) ### 构造器 封闭类本身永远是 [抽象(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` 的子类型. 因为, 如果 `List` 不是 不可变的(invariant), 那么下面的代码将可以通过编译, 但会在运行时导致一个异常, 那么 List 就并没有任何优于 Java 数组的地方了: ```JAVA // Java List strs = new ArrayList(); // 在编译期, Java 会在这里报告类型不匹配的错误. List objs = strs; // 如果不报告这个错误会怎么样 ? // 那么我们就能够向 String 组成的 List 添加一个 Integer 类型的元素. objs.add(1); // 然后在运行期, Java 会抛出 ClassCastException 异常: Integer cannot be cast to String String s = strs.get(0); ``` Java 禁止上面示例中的做法, 以保证运行期的类型安全. 但这个原则背后存在一些隐含的影响. 比如, 我们来看看 `Collection` 接口的 `addAll()` 方法. 这个方法的签名应该是什么样的? 你可能会根据第一直觉, 将它定义为: ```JAVA // Java interface Collection ... { void addAll(Collection items); } ``` 但是这样的话, 你将无法进行下面这种操作(尽管它是绝对安全的): ```JAVA // Java // 如果 addAll 方法使用前面那种简单的定义, 下面的代码无法编译: // Collection is not a subtype of Collection void copyAll(Collection to, Collection from) { to.addAll(from); } ``` 因此 `addAll()` 的签名定义实际上是这样的: ```JAVA // Java interface Collection ... { void addAll(Collection items); } ``` 这里的 通配符类型参数(wildcard type argument) `? extends E` 表示, 该方法接受的参数是一个集合, 集合元素的类型是 `E` 或 `E` 的子类型, 而不限于 `E` 本身. 这就意味着, 你可以安全地从集合元素中 读取 `E` (因为集合的元素是 `E` 的某个子类型的实例), 但 不能写入 到集合中去, 因为你不知道什么样的对象实例才能与这个 `E` 的未知子类型匹配. 尽管有这样的限制, 作为回报, 你得到了希望的功能: `Collection` 是 `Collection` 的子类型. 也就是说, 指定了 extends 边界 (上 边界)的通配符类型, 使得我们的类型成为一种 协变(covariant) 类型. 要理解这种模式的工作原理十分简单: 如果你只能从一个集合 取得 元素, 那么就可以使用一个 `String` 组成的集合, 并从中读取 `Object` 实例. 反过来, 如果你只能向集合 放入 元素, 那么就可以使用一个 `Object` 组成的集合, 并向其中放入 `String`: 在 Java 中有 `List`, 它可以接受 `String`, 或 `String` 的任何父类型. 上面的后一种情况称为 反向类型变异(contravariance), 对于 `List`, 你只能调用那些接受 `String` 类型参数的方法 (比如, 可以调用 `add(String)`, 或 `set(int, String)`), 如果你对 `List` 调用返回类型为 `T` 的方法时, 你得到的返回值将不会是 `String` 类型, 而是 `Object` 类型. Joshua Bloch 在他的 [Effective Java, 第 3 版](http://www.oracle.com/technetwork/java/effectivejava-136174.html) 详细解释了这个问题, (第 31 条: "为增加 API 的灵活性, 应该使用限定范围的通配符类型(bounded wildcard)"). 他将那些只能 读取 的对象称为 生产者(Producer), 将那些只能 写入 的对象称为 消费者(Consumer). 他建议: Tip: "为尽量保证灵活性, 应该对代表生产者和消费者的输入参数使用通配符类型." 他还提出了下面的记忆口诀: PECS, 表示 生产者(Producer)对应 Extends, 消费者(Consumer) 对应 Super. Note: 如果你使用一个生产者对象, 比如, `List`, 你将无法对这个对象调用 `add()` 或 `set()` 方法, 但这并不代表这个对象是 值不变的(immutable): 比如, 你完全可以调用 `clear()` 方法来删除 List 内的所有元素, 因为 `clear()` 方法不需要任何参数. 通配符类型(或者其他任何的类型变异)唯一能够确保的仅仅是 类型安全. 对象值的不变性(Immutability)是与此完全不同的另一个问题. ### 声明处的类型变异(Declaration-site variance) 假设有一个泛型接口 `Source`, 其中不存在任何接受 `T` 作为参数的方法, 仅有返回值为 `T` 的方法: ```JAVA // Java interface Source { T nextT(); } ``` 那么, 完全可以在 `Source` 类型的变量中保存一个 `Source` 类型的实例 - 因为不存在对消费者方法的调用. 但 Java 不能理解这一点, 因此仍然禁止以下代码: ```JAVA // Java void demo(Source strs) { Source objects = strs; // !!! 在 Java 中禁止这样的操作 // ... } ``` 为了解决这个问题, 你需要将对象类型声明为 `Source`, 这样其实是毫无意义的, 因为在这样修改之后, 你所能调用的方法与修改之前其实是完全一样的, 因此, 使用这样复杂的类型声明并未带来什么好处. 但编译器并不理解这一点. 在 Kotlin 中, 我们有办法将这种情况告诉编译器. 这种技术称为 声明处的类型变异(declaration site variance): 你可以对 `Source` 的 类型参数 `T` 添加注解, 来确保 `Source` 的成员函数只会 返回 (生产) `T` 类型, 而绝不会消费 `T` 类型. 为了实现这个目的, 可以对 `T` 使用 out 修饰符: ```KOTLIN interface Source { fun nextT(): T } fun demo(strs: Source) { val objects: Source = strs // 这是 OK 的, 因为 T 是一个 out 类型参数 // ... } ``` 一般规则是: 当 `C` 类的类型参数 `T` 声明为 `out` 时, 那么在 `C` 的成员函数中, `T` 类型只允许出现在 输出 位置, 这样的限制带来的回报就是, `C` 可以安全地用作 `C` 的父类型. 也就是说, 你可以将 `C` 类称为, 在类型参数 `T` 上 协变的(covariant), 或者说 `T` 是一个 协变的(covariant) 类型参数. 你可以将 `C` 类看作 `T` 类型对象的 生产者, 而不是 `T` 类型对象的 消费者. out 修饰符称为 协变注解(variance annotation), 而且, 由于这个注解出现在类型参数的声明处, 它提供了 声明处的类型变异(declaration-site variance). 这种方案与 Java 中的 使用处类型变异(use-site variance) 刚好相反, 在 Java 中, 是类型使用处的通配符产生了类型的协变. 除了 `out` 之外, Kotlin 还提供了另一种类型变异注解: `in`. 这个注解导致类型参数 `反向类型变异(contravariant)`: 也就是说这个类型将只能被消费, 而不能被生产. 反向类型变异的一个很好的例子是 `Comparable`: ```KOTLIN interface Comparable { operator fun compareTo(other: T): Int } fun demo(x: Comparable) { x.compareTo(1.0) // 1.0 类型为 Double, 是 Number 的子类型 // 因此, 你可以将 x 赋值给 Comparable 类型的变量 val y: Comparable = x // OK! } ``` in 和 out 关键字的意义看来是十分直观的(同样的关键字已经在 C# 中使用很长时间了), 因此, 前面提到的记忆口诀也没有必要了, 我们可以将它改写为更高的抽象层次: [存在主义](https://en.wikipedia.org/wiki/Existentialism) 变形法则: 消费者进, 生产者出! :-) 译注: 上面两句翻译得不够好, 待校 ## 类型投射(Type projection) ### 使用处的类型变异(Use-site variance): 类型投射(Type projection) 将声明类型参数 T 声明为 `out`, 就可以免去使用时子类化的麻烦, 这是十分方便的. 但是有些类 不能 限定为仅仅只返回 `T` 类型值! 关于这个问题, 一个很好的例子是 `Array` 类: ```KOTLIN class Array(val size: Int) { operator fun get(index: Int): T { ... } operator fun set(index: Int, value: T) { ... } } ``` 这个类对于类型参数 `T` 既不能协变, 也不能反向协变. 这就带来很大的不便. 我们来看看下面的函数: ```KOTLIN fun copy(from: Array, to: Array) { assert(from.size == to.size) for (i in from.indices) to[i] = from[i] } ``` 这个函数应该将元素从一个 Array 复制到另一个 Array. 我们来试试使用一下这个函数: ```KOTLIN val ints: Array = arrayOf(1, 2, 3) val any = Array(3) { "" } copy(ints, any) // ^ 这里发生编译错误, 期待的参数类型是 Array, 但实际类型是 Array ``` 在这里, 你又遇到了熟悉的老问题: `Array` 对于类型参数 `T` 是 不可变的, 因此 `Array` 和 `Array` 谁也不是谁的子类型. 为什么不是? 原因与以前一样, 因为 `copy` 函数内可能发生预想外的行为, 比如, 它可能会试图向 `from` 数组中写入一个 `String`, 这时如果你传入的实际参数是一个 `Int` 的数组, 就会导致一个 `ClassCastException`. 为了禁止 `copy()` 函数向 `from` 数组 写入 数据, 你可以这样: ```KOTLIN fun copy(from: Array, to: Array) { ... } ``` 这种声明称为 类型投射(type projection): 其含义是, `from` 不是一个单纯的数组, 而是一个被限制(投射)的数组. 你只能对这个数组调用那些返回值为类型参数 `T` 的方法, 在这个例子中, 只能调用 `get()` 方法. 这就是 使用处的类型变异(use-site variance) 的实现方案, 对应 Java 的 `Array`, 但略为简单一些. 你也可以使用 `in` 关键字来投射一个类型: ```KOTLIN fun fill(dest: Array, value: String) { ... } ``` `Array` 与 Java 的 `Array` 相同. 也就是说, 你可以使用 `String`, `CharSequence` 或 `Object` 的数组作为 `fill()` 函数的参数. ### 星号投射(Star-projection) 有些时候, 你可能想表示你并不知道类型参数的任何信息, 但是仍然希望能够安全地使用它. 这里所谓"安全地使用"是指, 对泛型类型定义一个类型投射, 要求这个泛型类型的所有的实体实例, 都是这个投射的子类型. 对于这个问题, Kotlin 提供了一种语法, 称为 星号投射(star-projection): * 假如类型定义为 `Foo`, 其中 `T` 是一个协变的类型参数, 上界(Upper Bound)为 `TUpper`, `Foo<*>` 等价于 `Foo`. 它表示, 当 `T` 未知时, 你可以安全地从 `Foo<*>` 中 读取 `TUpper` 类型的值. * 假如类型定义为 `Foo`, 其中 `T` 是一个反向协变的类型参数, `Foo<*>` 等价于 `Foo`. 它表示, 当 `T` 未知时, 你不能安全地向 `Foo<*>` 写入 任何东西. * 假如类型定义为 `Foo`, 其中 `T` 是一个协变的类型参数, 上界(Upper Bound)为 `TUpper`, 对于读取值的场合, `Foo<*>` 等价于 `Foo`, 对于写入值的场合, 等价于 `Foo`. 如果一个泛型类型中存在多个类型参数, 那么每个类型参数都可以单独的投射. 比如, 如果类型定义为 `interface Function`, 你可以使用以下几种星号投射: * `Function<*, String>`, 代表 `Function`. * `Function`, 代表 `Function`. * `Function<*, *>`, 代表 `Function`. Note: 星号投射与 Java 的原生类型(raw type)非常类似, 但可以安全使用. ## 泛型函数 不仅类可以有类型参数. 函数一样可以有类型参数. 类型参数放在函数名称 之前: ```KOTLIN fun singletonList(item: T): List { // ... } fun T.basicToString(): String { // 扩展函数 // ... } ``` 调用泛型函数时, 应该在函数名称 之后 指定调用端类型参数: ```KOTLIN val l = singletonList(1) ``` 如果可以通过程序上下文推断得到, 类型参数可以省略, 因此下面的例子也可以正确运行: ```KOTLIN val l = singletonList(1) ``` ## 泛型约束(Generic constraint) 对于一个给定的类型参数, 所允许使用的类型, 可以通过 泛型约束(generic constraint) 来限制. ### 上界(Upper Bound) 最常见的约束是 上界(Upper Bound), 对应于 Java 中的 extends 关键字: ```KOTLIN fun > sort(list: List) { ... } ``` 冒号之后指定的类型就是类型参数的 上界(Upper Bound): 表示对类型参数 `T`, 只允许使用 `Comparable` 的子类型. 比如: ```KOTLIN sort(listOf(1, 2, 3)) // 正确: Int 是 Comparable 的子类型 sort(listOf(HashMap())) // 错误: HashMap 不是 Comparable> 的子类型 ``` 如果没有指定, 则默认使用的上界是 `Any?`. 在定义类型参数的尖括号内, 只允许定义唯一一个上界. 如果同一个类型参数需要指定多个上界, 这时需要使用单独的 where 子句: ```KOTLIN fun copyWhenGreater(list: List, threshold: T): List where T : CharSequence, T : Comparable { return list.filter { it > threshold }.map { it.toString() } } ``` 传入的类型必须同时满足 `where` 子句中的所有条件. 在上面的示例中, `T` 类型必须 同时 实现 `CharSequence` 和 `Comparable` 接口. ## 确定不为 null 的类型 为了让与 Java 的泛型类和接口的互操作更加便利, Kotlin 允许将泛型类型参数为声明 确定不为 null. 要将泛型类型 `T` 声明为确定不为 null, 请使用 `& Any` 来声明这个类型. 例如: `T & Any`. 确定不为 null 的类型的 [上界(Upper Bound)](#upper-bounds) 必须是可以为 null 的类型. 确定不为 null 的类型的最常见的使用场景是, 你想要覆盖 override 一个包含 `@NotNull` 参数的 Java 方法. 例如, 考虑下面的 `load()` 方法: ```JAVA import org.jetbrains.annotations.*; public interface Game { public T save(T x) {} @NotNull public T load(@NotNull T x) {} } ``` 要在 Kotlin 中成功的覆盖 `load()` 方法, 你需要将 `T1` 声明为确定不为 null: ```KOTLIN interface ArcadeGame : Game { override fun save(x: T1): T1 // T1 确定不为 null override fun load(x: T1 & Any): T1 & Any } ``` 如果只使用 Kotlin, 那么你不太可能需要明确的声明确定不为 null 的类型, 因为 Kotlin 的类型推断功能会帮你解决这个问题. ## 类型擦除 对使用泛型声明的代码, Kotlin 在编译期进行类型安全性检查. 在运行期, 泛型类型的实例不保存关于其类型参数的任何信息. 我们称之为, 类型信息 被擦除 了. 比如, `Foo` 和 `Foo` 的实例, 其类型信息会被擦除, 只剩下 `Foo<*>`. ### 泛型的类型检查与类型转换 由于存在类型擦除的问题, 因此不存在一种通用的办法, 可以在运行期检查一个泛型类的实例是通过什么样的类型参数来创建的, 并且编译器禁止这样的 `is` 检查, 例如 `ints is List` 或 `list is T` (T 是类型参数). 但是, 你可以检查实例是否属于星号投射类型: ```KOTLIN if (something is List<*>) { something.forEach { println(it) } // List 中元素的类型都被识别为 `Any?` } ``` 类似的, 如果(在编译期间)已经对一个实例的类型参数进行了静态检查, 你可以对泛型之外的部分进行 `is` 检查, 或类型转换. 注意, 下面的示例中省略了尖括号: ```KOTLIN fun handleStrings(list: MutableList) { if (list is ArrayList) { // `list` 会被智能转换为 `ArrayList` } } ``` 对于不涉及类型参数的类型转换, 可以使用的相同语法, 但省略类型参数: `list as ArrayList`. 泛型函数调用的类型参数也只在编译期进行检查. 在函数内部, 类型参数不能用来进行类型检查, 而且向类型参数的类型转换 (`foo as T`) 也不做检查. 唯一的例外是使用 [实体化的类型参数(Reified type parameter)](inline-functions.html#reified-type-parameters) 的内联函数, 会将它们的实际类型参数内联到每一个调用处. 因此可以对类型参数使用类型检查和转换. 但是, 在类型检查或转换内部使用的泛型类型实例, 仍然存在上述限制. 例如, 在类型检查 `arg is T` 中, 如果 `arg` 自身是一个泛型类型的实例, 它的类型参数仍然会被擦除. ```KOTLIN //sampleStart inline fun Pair<*, *>.asPairOf(): Pair? { if (first !is A || second !is B) return null return first as A to second as B } val somePair: Pair = "items" to listOf(1, 2, 3) val stringToSomething = somePair.asPairOf() val stringToInt = somePair.asPairOf() val stringToList = somePair.asPairOf>() val stringToStringList = somePair.asPairOf>() // 这段代码能够编译, 但破坏了类型安全型! // 请展开示例代码查看详情 //sampleEnd fun main() { println("stringToSomething = " + stringToSomething) println("stringToInt = " + stringToInt) println("stringToList = " + stringToList) println("stringToStringList = " + stringToStringList) //println(stringToStringList?.second?.forEach() {it.length}) // 这里会抛出 ClassCastException 异常, 因为 list 中的元素不是字符串 } ``` ### 未检查的类型转换 将类型转换为带有实际类型参数的泛型类型, 例如 `foo as List`, 在运行期也无法进行检查. 如果不能由编译器直接推断得到类型安全, 但通过高层的程序逻辑能够保证, 那么可以使用这种未检查的类型转换. 请看下面的示例. ```KOTLIN fun readDictionary(file: File): Map = file.inputStream().use { TODO("Read a mapping of strings to arbitrary elements.") } // 我们把值为 `Int` 的 map 保存到了这个文件 val intsFile = File("ints.dictionary") // 此处会出现编译警告: Unchecked cast: `Map` to `Map` val intsDictionary: Map = readDictionary(intsFile) as Map ``` 最后一行中的类型转换会出现编译警告. 编译器无法对这个类型转换在运行期进行完整地检查, 因此不能保证 map 中的值是 `Int`. 为了避免这种未检查的类型转换, 你可以重新设计你的程序结构. 在上例中, 你可以声明 `DictionaryReader` 和 `DictionaryWriter` 接口, 然后对不同的数据类型提供类型安全的实现类. 你可以引入合理的抽象层次, 将未检查的类型转换, 从对接口的调用代码中, 移动到具体的实现类中. 正确使用 [泛型类型变异(generic variance)](#variance) 也可能有助于解决这类问题. 对于泛型函数, 使用 [实体化的类型参数(Reified type parameter)](inline-functions.html#reified-type-parameters) 可以使得 `arg as T` 之类的类型转换变成可被检查的类型转换, 除非 `arg` 的类型带有 它自己的 类型参数, 并且在运行期间被擦除了. 对类型转换语句, 或这个语句所属的声明, 添加 `@Suppress("UNCHECKED_CAST")` [注解](annotations.html), 可以屏蔽未检查的类型转换导致的编译警告: ```KOTLIN inline fun List<*>.asListOfType(): List? = if (all { it is T }) @Suppress("UNCHECKED_CAST") this as List else null ``` Note: 在 JVM 平台: [数组类型](arrays.html) (`Array`) 保持了被擦除的数组元素类型信息, 将某个类型向数组类型进行的转换, 可以进行部分地检查: 数组元素可否为空, 以及数组元素本身的类型参数仍然会被擦除. 比如, 只要 `foo` 是一个数组, 并且元素类型是任意一种 `List<*>`, 无论元素可否为 null, 那么 `foo as Array?>` 转换就会成功. ## 对类型参数的下划线操作符 可以对类型参数使用下划线操作符 `_`. 当其他类型已经明确指定时, 使用下划线操作符可以自动推断一个参数的类型: ```KOTLIN abstract class SomeClass { abstract fun execute() : T } class SomeImplementation : SomeClass() { override fun execute(): String = "Test" } class OtherImplementation : SomeClass() { override fun execute(): Int = 42 } object Runner { inline fun , T> run() : T { return S::class.java.getDeclaredConstructor().newInstance().execute() } } fun main() { // T 被推断为 String, 因为 SomeImplementation 继承自 SomeClass val s = Runner.run() assert(s == "Test") // T 被推断为 Int, 因为 OtherImplementation 继承自 SomeClass val n = Runner.run() assert(n == 42) } ``` # 异步编程(Asynchronous Programming)技术 过去几十年来, 作为开发者, 我们始终面对一个问题需要解决 - 怎么样才能让我们的应用程序不要发生阻塞. 无论我们在开发桌面应用程序, 移动应用程序, 甚至后端应用程序, 我们都希望避免让用户等待, 甚至更糟的情况, 导致瓶颈, 使得应用程序无法扩展到更大规模. 现在已经有了很多种方案来解决这个问题, 包括: * [线程(Thread)](#threading) * [回调(Callback)](#callbacks) * [Future, Promise, 以及其他](#futures-promises-and-others) * [Reactive Extension](#reactive-extensions) * [协程(Coroutine)](#coroutines) 在解释协程之前, 先让我们简单回顾一下其他解决方案. ## 线程(Thread) 要避免程序阻塞, 线程(Thread)可能是大家最熟悉的解决方案. ```KOTLIN fun postItem(item: Item) { val token = preparePost() val post = submitPost(token, item) processPost(post) } fun preparePost(): Token { // 发起请求, 随后阻塞主线程 return token } ``` 我们假设上面的代码中的 `preparePost` 是一个长时间执行的处理, 因此会阻塞 UI. 我们可以在一个独立的线程中启动它. 这样我们就可以避免 UI 阻塞. 这是非常常见的技术, 但有很多缺点: * 线程代价高昂. 线程需要 context 切换, 这个代价很高. * 线程不是无限的. 底层的操作系统限制了能够启动的线程数量. 在后端应用程序中, 这个限制可能导致显著的瓶颈. * 线程并不总是可用的. 在某些平台, 比如 JavaScript, 甚至根本不支持线程. * 线程使用困难. 调试线程, 以及避免竞争条件, 都是我们在多线程编程中遭遇的常见问题. ## 回调(Callback) 使用回调, 基本想法是将回调函数作为参数传给另一个函数, 然后在处理结束后调用这个回调函数. ```KOTLIN fun postItem(item: Item) { preparePostAsync { token -> submitPostAsync(token, item) { post -> processPost(post) } } } fun preparePostAsync(callback: (Token) -> Unit) { // 发起请求, 并立即返回 // 调度回调函数, 在之后的时刻调用它 } ``` 这个原则感觉好像是更加优雅的解决方案, 但仍然存在一系列的问题: * 回调的嵌套很困难. 通常来说, 被用作回调的函数, 经常会需要它自己的回调. 因此导致一系列的嵌套回调, 代码极难理解. 这种模式经常被称为回调地狱, 或者叫做 [诅咒金字塔(pyramid of doom)](https://en.wikipedia.org/wiki/Pyramid_of_doom_(programming)), 因为这些深度嵌套的回调造成的缩进会形成三角形. * 错误处理很复杂. 嵌套模型导致错误的处理和传播变得更加复杂. 在事件循环架构中, 比如 JavaScript, 回调是非常常见的, 但即使在这种场景, 通常人们也会改为使用其他方案, 比如 Promise 或 Reactive Extension. ## Future, Promise, 以及其他 Future 或 Promise (其他语言或平台也可能使用别的名称), 背后的理念是, 当我们发起一个调用, 我们会得到 承诺(Promise), 这个调用会在某个时间点返回一个 `Promise` 对象, 然后我们可以对它进行操作. ```KOTLIN fun postItem(item: Item) { preparePostAsync() .thenCompose { token -> submitPostAsync(token, item) } .thenAccept { post -> processPost(post) } } fun preparePostAsync(): Promise { // 发起请求, 并返回一个 promise, 它会在之后的时刻完成 return promise } ``` 这个方案要求我们的编程方式发生很多变化, 具体来说是: * 不同的编程模型. 与回调类似, 编程模型不再是自顶向下的命令模式(top-down imperative approach), 而是变为一种由链式调用构成的组合模式(compositional model). 传统的编程结构比如循环, 异常处理, 等等, 在这种模式中通常不再可用了. * 不同的 API. 通常需要学习完全不同的新 API, 比如 `thenCompose` 或 `thenAccept`, 而且这些函数在不同的平台上也可能存在差异. * 特殊的返回类型. 返回类型不再是我们需要的实际数据, 而是一个新的类型 `Promise`, 我们需要从它得到数据. * 错误处理很复杂. 错误的传播和链条通常很不直观. ## Reactive Extension [Erik Meijer](https://en.wikipedia.org/wiki/Erik_Meijer_(computer_scientist)) 将 Reactive Extension (Rx) 引入到了 C# 中. 尽管它在 .NET 平台得到了大量应用, 但并没有被主流开发者采用, 直到 Netflix 将它移植到 Java, 命名为 RxJava. 在那之后, 对很多平台有了大量的移植, 包括 JavaScript (RxJS). Rx 背后的理念是所谓 `可观察的流(observable stream)`, 我们将数据看作流(stream)(包含无限数量的数据), 而且可以观察这些流. 从实践层面来讲, Rx 只不过是 [观察者模式(Observer Pattern)](https://en.wikipedia.org/wiki/Observer_pattern), 并带有一系列的扩展, 使得我们可以对数据进行操作. 这个方案与 Future 很类似, 但 Future 可以被看作是返回一个单独的元素, Rx 则返回一个流. 但是, 与前面的方案类似, Rx 也带来了编程模型的全新的理念, 如同下面这句名言所说: ``` "任何东西都是流, 而且可以观察" ``` 这表示要用不同的方式来解决问题, 与我们以前编写同步代码相比, 编程方式发生显著的变化. 有一个优点是, 与 Future 不同, 由于 Rx 被移植到了很多平台, 因此不论编程语言是 C#, Java, JavaScript, 还是可以使用 Rx 的任何其他语言, 通常我们可以得到一致的 API 体验. 此外, Rx 还引入了比较好的错误处理方案. ## 协程(Coroutine) Kotlin 的异步编程解决方案是使用协程(Coroutine), 它的理念是可被挂起的一段计算, 也就是说, 一个函数的执行在某个时刻可以被挂起, 并在之后的某个时刻恢复运行. 协程的优点之一是, 对于开发者来说, 非阻塞代码的编写方式与编写阻塞代码基本上是一样的. 编程模型本身并没有发生变化. 以下面的代码为例: ```KOTLIN fun postItem(item: Item) { launch { val token = preparePost() val post = submitPost(token, item) processPost(post) } } suspend fun preparePost(): Token { // 发起请求, 并挂起协程 return suspendCoroutine { /* ... */ } } ``` 这段代码会启动一个长时间运行的操作, 但不会阻塞主线程. `preparePost` 是一个 `挂起函数(suspendable function)`, 因此它的前缀添加了关键字 `suspend`. 上面这段话的意思是说, 从时间序列上来看, 函数会在某个时刻开始运行, 暂停运行, 然后又恢复运行. * 函数签名完全不变. 唯一的区别是添加 `suspend` 关键字. 但返回类型仍然是我们希望返回的数据类型. * 代码的编写方式仍然与编写同步代码的方式一样, 自顶向下, 除了使用 `launch` 函数来启动协程之外(具体细节在其他教程中解释), 不需要任何特殊的语法. * 编程模型和 API 仍然保持不变. 我们可以继续使用循环, 错误处理, 等等. 不需要学习完全不用的一组新 API. * 平台独立. 无论我们的编译目标是 JVM, JavaScript 还是其他平台, 我们编写的代码是一样的. 编译器会负责协程代码与各个平台之间的调节工作. 协程不是由 Kotlin 独自发明的一个新概念. 它已经存在了几十年, 并在其他编程语言中大量使用, 比如 Go. 值得注意的是协程在 Kotlin 中的实现方式, 大多数功能交给库来实现. 实际上, 除了 `suspend` 关键字, Kotlin 语言没有添加其他关键字. 这一点与其他语言不同, 比如 C# 的语法添加了 `async` 和 `await`. 而在 Kotlin 中, 这些只是库函数. 更多详情, 请参见 [协程参考文档](coroutines-overview.html). # 协程(Coroutine) 应用程序经常需要同时执行多个任务, 例如响应用户输入, 装载数据, 或更新画面. 为了实现这些功能, 它们依赖于并发, 并发能够允许操作独立运行, 相互不会阻塞. 并发运行任务的最常见方式是使用线程, 线程是由操作系统管理的独立的执行路径. 但是, 线程相对来说比较重, 而且创建太多线程可能导致性能问题. 为了支持高效的并发, Kotlin 使用基于 协程(Coroutine) 构建的异步编程技术, 让你能够使用挂起函数(Suspending Function), 以一种自然的, 顺序的方式编写异步代码. 协程是线程的轻量替代方案. 可以挂起, 而不阻塞系统资源, 并且消耗较少的资源, 因此更适合于细粒度的并发. 大多数协程功能由 [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) 库提供, 这个库包含各种工具, 用于启动协程, 处理并发, 使用异步的流, 等等. 如果你是 Kotlin 协程的初学者, 请先阅读 [协程的基本概念](coroutines-basics.html) 向导, 然后在深入了解更复杂的内容. 这篇向导通过简单的示例, 介绍一些关键概念, 包括挂起函数, 协程构建器, 以及结构化并发: [协程的基本概念](coroutines-basics.html) Tip: 查看示例项目 [KotlinConf App](https://github.com/JetBrains/kotlinconf-app), 了解协程的具体使用. ## 协程的概念 `kotlinx.coroutines` 库提供了核心的构建代码块, 用于并发运行任务, 构建协程执行, 以及管理状态. ### 挂起函数与协程构建器 Kotlin 中的协程以挂起函数为基础, 挂起函数能够让代码暂停执行, 之后恢复执行, 而不会阻塞线程. `suspend` 关键字标记函数, 表示它能够异步的执行长时间运行的操作. 要启动新的协程, 请使用协程构建器, 例如 [.launch()](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html) 和 [.async()](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html). 这些构建器是 [CoroutineScope](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/) 上的扩展函数, `CoroutineScope` 定义协程的生存周期, 并提供协程上下文(Context). 关于这些构建器, 详情请参见 [协程的基本概念](coroutines-basics.html) 和 [组合挂起函数](coroutines-and-channels.html). ### 协程的上下文(Context)和行为 从一个 `CoroutineScope` 启动一个协程, 会创建一个上下文(Context), 控制它的执行. 构建器函数, 例如 `.launch()` 和 `.async()`, 会自动创建一组元素, 定义协程的行为: * [Job](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/) 接口, 追踪协程的生命周期, 并实现结构化并发. * [CoroutineDispatcher](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/), 控制协程在哪里运行, 例如在背景线程中, 还是在 UI 应用程序的主线程中. * [CoroutineExceptionHandler](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/), 处理未被捕获的异常. 这些元素, 以及其他可能的元素, 共同构成 [协程的上下文(Context)](coroutine-context-and-dispatchers.html), 默认从协程的父协程继承得到. 这个上下文构成一个层级结构, 实现结构化并发, 在结构化并发中, 相关的协程能够一起 [取消](cancellation-and-timeouts.html), 或者作为一个组来 [处理异常](exception-handling.html). ### 异步的数据流(Asynchronous Flow), 以及共享的可变状态 Kotlin 提供了几种方式来实现协程的通信. 请根据你想要如何在协程之间共享值, 选择以下几种方案之一: * [Flow](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/): 只有在协程主动获取值时, 才会产生值. * [Channel](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/): 允许多个协程发送和接收值, 每个值只传递给一个协程. * [SharedFlow](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-shared-flow/): 与所有活动的收集协程持续的共享每个值. 当多个协程需要访问或更新同一个数据时, 我们称为协程之间 共享可变的状态. 如果没有协调, 这可能导致竞争条件, 也就是说多个操作会以不可预测的方式相互干扰. 为了安全的管理共享的可变状态, 请使用 [StateFlow](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/#) 来封装共享的数据. 之后, 你就可以在一个协程中更新数据, 并在其他协程中获取它的最新值. 详情请参见 [异步的数据流(Asynchronous Flow)](flow.html), [通道(Channel)](channels.html), 以及 [协程(Coroutine)与通道(Channel)教程](coroutines-and-channels.html). ## 下一步做什么 * 阅读 [协程的基本概念向导](coroutines-basics.html), 了解协程, 挂起函数, 以及构建器的基础知识. * 阅读 [组合挂起函数](coroutine-context-and-dispatchers.html), 了解如何组合挂起函数, 以及构建协程管道. * 学习如何在 IntelliJ IDEA 中使用内建的工具 [调试协程](debug-coroutines-with-idea.html). * 关于数据流(Flow)的调试, 详情请参见 [使用 IntelliJ IDEA 调试 Kotlin 数据流(Flow)](debug-flow-with-idea.html) 教程. * 阅读 [使用协程进行 UI 编程向导](https://github.com/Kotlin/kotlinx.coroutines/blob/master/ui/coroutines-guide-ui.md), 学习基于协程的 UI 开发. * 阅读 [在 Android 中使用协程的最佳实践](https://developer.android.com/kotlin/coroutines/coroutines-best-practices). * 查看 [kotlinx.coroutines API 参考文档](https://kotlinlang.org/api/kotlinx.coroutines/). # 反射 反射 是语言与库中的一组功能, 允许你在运行时刻获取程序本身的信息. 函数和属性在 Kotlin 是语言中的一等公民(first-class citizen), 而且, 通过反射获取它们的信息(比如, 在运行时刻得到一个函数或属性的名称和数据类型) 也是函数式或交互式的编程方式中的基本功能. Note: Kotlin/JS 对反射只提供了有限的支持. [更多详情请参见 Kotlin/JS 中的反射功能](js-reflection.html). ## JVM 依赖项 在 JVM 平台上, Kotlin 编译器包含了使用反射功能所需要的运行时组件, 它是一个单独的 JAR 文件 `kotlin-reflect.jar`. 这样做为了对那些不使用反射功能的应用程序, 减少其运行库的大小. 在 Gradle 或 Maven 项目中, 如果需要使用反射, 需要添加 `kotlin-reflect` 的依赖项: * 在 Gradle 项目中: Kotlin: ```KOTLIN dependencies { implementation(kotlin("reflect")) } ``` Groovy: ```GROOVY dependencies { implementation "org.jetbrains.kotlin:kotlin-reflect:2.4.0" } ``` * 在 Maven 项目中: ```XML org.jetbrains.kotlin kotlin-reflect ``` 如果你没有使用 Gradle 或 Maven, 请注意将 `kotlin-reflect.jar` 添加到你的项目的 classpath 中. 对于其他支持的场景(使用命令行编译器的 IntelliJ IDEA 项目), 这个 jar 文件默认会加入到 classpath 中. 在命令行编译器中, 你可以使用 `-no-reflect` 编译选项, 从 classpath 中删除 `kotlin-reflect.jar`. ## 类引用(Class Reference) 最基本的反射功能就是获取一个 Kotlin 类的运行时引用. 要得到一个静态的已知的 Kotlin 类的引用, 可以使用 类字面值(class literal) 语法: ```KOTLIN val c = MyClass::class ``` 类引用是一个 [KClass](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-class/index.html) 类型的值. Note: 在 JVM 平台: Kotlin 的类引用与 Java 的类引用不相同. 要得到 Java 的类引用, 请使用 `KClass` 对象实例的 `.java` 属性. ### 与对象实例绑定的类引用语法 `::class` 语法同样可以用于取得某个对象实例的类的引用: ```KOTLIN val widget: Widget = ... assert(widget is GoodWidget) { "Bad widget: ${widget::class.qualifiedName}" } ``` 在这个例子中, 尽管 widget 的类型为 `Widget`, 但你会得到对象实例的确切的类的引用, 比如 `GoodWidget`, 或 `BadWidget`. ## 可调用的引用 指向函数, 属性, 构造器的引用, 可以被调用, 或用作 [函数类型](lambdas.html#function-types) 的实例. 所有可调用的引用的共同的超类是 [KCallable<out R>](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-callable/index.html), 这里的 `R` 是返回值的类型. 对于属性来说就是属性类型, 对构造器来说就是它创建出来的类的类型. ### 函数引用(Function Reference) 假设你有一个有名称的函数, 声明如下, 你可以直接调用它(`isOdd(5)`): ```KOTLIN fun isOdd(x: Int) = x % 2 != 0 ``` 另一种情况是, 你可以将它用作一个函数类型的值, 比如, 传给另一个函数作为参数. 为了实现这个功能, 可以使用 `::` 操作符: ```KOTLIN fun isOdd(x: Int) = x % 2 != 0 fun main() { //sampleStart val numbers = listOf(1, 2, 3) println(numbers.filter(::isOdd)) //sampleEnd } ``` 这里的 `::isOdd` 是一个 `(Int) -> Boolean` 函数类型的值. 函数引用的类型属于 [KFunction<out R>](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-function/index.html) 的子类之一, 具体是哪个由函数的参数个数决定. 比如, 可能是 `KFunction3`. `::` 也可以用在重载函数上, 前提是必须能够推断出对应的函数参数类型. 比如: ```KOTLIN fun main() { //sampleStart fun isOdd(x: Int) = x % 2 != 0 fun isOdd(s: String) = s == "brillig" || s == "slithy" || s == "tove" val numbers = listOf(1, 2, 3) println(numbers.filter(::isOdd)) // 指向 isOdd(x: Int) 函数 //sampleEnd } ``` 或者, 你也可以将方法引用保存到一个明确指定了类型的变量中, 通过这种方式来提供必要的函数参数类型信息: ```KOTLIN val predicate: (String) -> Boolean = ::isOdd // 指向 isOdd(x: String) 函数 ``` 如果你需要使用一个类的成员函数, 或者一个扩展函数, 就必须使用限定符: `String::toCharArray`. 即使你将一个变量初始化赋值为一个扩展函数的引用, 编译器自动推断得到的函数类型实际上是不带接受者的, 但它会带有一个额外的参数, 对应于接受者对象. 如果想要使用带接受者的函数类型, 需要明确指定函数类型: ```KOTLIN val isEmptyStringList: List.() -> Boolean = List::isEmpty ``` #### 示例: 函数组合 我们来看看下面的函数: ```KOTLIN fun compose(f: (B) -> C, g: (A) -> B): (A) -> C { return { x -> f(g(x)) } } ``` 这个函数返回一个新的函数, 由它的两个参数代表的函数组合在一起构成: `compose(f, g) = f(g(*))`. 你可以使用可以执行的函数引用来调用这个函数: ```KOTLIN fun compose(f: (B) -> C, g: (A) -> B): (A) -> C { return { x -> f(g(x)) } } fun isOdd(x: Int) = x % 2 != 0 fun main() { //sampleStart fun length(s: String) = s.length val oddLength = compose(::isOdd, ::length) val strings = listOf("a", "ab", "abc") println(strings.filter(oddLength)) //sampleEnd } ``` ### 属性引用(Property Reference) 在 Kotlin 中, 可以将属性作为一等对象来访问, 方法是使用 `::` 操作符: ```KOTLIN val x = 1 fun main() { println(::x.get()) println(::x.name) } ``` 表达式 `::x` 的计算结果是一个属性对象, 类型为 `KProperty0`. 你可以通过它的 `get()` 方法得到属性值, 或者通过它的 `name` 属性得到属性名称. 详情请参见 [KProperty 类的 API 文档](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-property/index.html). 对于值可变的属性, 比如, `var y = 1`, `::y` 返回的属性对象的类型为 [KMutableProperty0<Int>](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-mutable-property/index.html), 它有一个 `set()` 方法: ```KOTLIN var y = 1 fun main() { ::y.set(2) println(y) } ``` 所有使用单参数函数的地方都可以使用属性引用: ```KOTLIN fun main() { //sampleStart val strs = listOf("a", "bc", "def") println(strs.map(String::length)) //sampleEnd } ``` 要访问类的成员属性, 需要使用限定符, 如下: ```KOTLIN fun main() { //sampleStart class A(val p: Int) val prop = A::p println(prop.get(A(1))) //sampleEnd } ``` 对于扩展属性: ```KOTLIN val String.lastChar: Char get() = this[length - 1] fun main() { println(String::lastChar.get("abc")) } ``` ### 与 Java 反射功能的互操作性 在 Java 平台上, Kotlin 的标准库包含了针对反射类的扩展函数, 这些反射类提供了与 Java 反射对象的相互转换功能(参见包 `kotlin.reflect.jvm`). 比如, 要查找一个 Kotlin 属性的后端域变量, 或者查找充当这个属性取值函数的 Java 方法, 你可以编写下面这样的代码: ```KOTLIN import kotlin.reflect.jvm.* class A(val p: Int) fun main() { println(A::p.javaGetter) // 打印结果为: "public final int A.getP()" println(A::p.javaField) // 打印结果为: "private final int A.p" } ``` 要查找与一个 Java 类相对应的 Kotlin 类, 可以使用 `.kotlin` 扩展属性: ```KOTLIN fun getKClass(o: Any): KClass = o.javaClass.kotlin ``` ### 构造器引用(Constructor Reference) 与方法和属性一样, 也可以引用构造器. 凡是使用使用函数类型对象的地方, 你都可以使用构造器的引用, 但这个函数类型接受的参数应该与构造器相同, 返回值应该是构造器所属类的对象实例. 引用构造器使用 `::` 操作符, 再加上类名称. 我们来看看下面的函数, 它接受的参数是一个函数, 这个函数参数本身没有参数, 并返回 `Foo` 类型: ```KOTLIN class Foo fun function(factory: () -> Foo) { val x: Foo = factory() } ``` 使用 `::Foo`, 也就是 `Foo` 类的无参构造器的引用, 你可以这样调用上面的函数: ```KOTLIN function(::Foo) ``` 指向构造器的引用的类型是 [KFunction<out R>](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-function/index.html) 的子类之一, 具体是哪个由函数的参数个数决定. ### 与对象实例绑定的函数和属性引用 你可以引用某个具体的对象实例的方法: ```KOTLIN fun main() { //sampleStart val numberRegex = "\\d+".toRegex() println(numberRegex.matches("29")) val isNumber = numberRegex::matches println(isNumber("29")) //sampleEnd } ``` 上面的示例使用 `matches` 方法的引用, 而不是直接调用这个方法. 这样的引用会与方法的接受者绑定在一起. 这样的引用可以直接调用(就像上面的示例程序中那样), 也可以用在任何使用函数类型的地方: ```KOTLIN fun main() { //sampleStart val numberRegex = "\\d+".toRegex() val strings = listOf("abc", "124", "a70") println(strings.filter(numberRegex::matches)) //sampleEnd } ``` 我们来比较一下绑定到对象实例的引用, 以及未绑定到实例的引用. 绑定到对象实例的引用与它的接受者对象实例结合在一起, 因此接受者的类型不再是它的一个参数: ```KOTLIN val isNumber: (CharSequence) -> Boolean = numberRegex::matches val matches: (Regex, CharSequence) -> Boolean = Regex::matches ``` 同样, 属性的引用也可以与对象实例绑定: ```KOTLIN fun main() { //sampleStart val prop = "abc"::length println(prop.get()) //sampleEnd } ``` 你不需要指定 `this` 接收者: `this::foo` 可以简写为 `::foo`. ### 与实例绑定的构造器引用 (译注: 内部类与普通类不同, 在创建内部类实例时, 需要绑定到一个具体的外部类实例.) 通过指定一个外部类的实例, 可以得到与这个外部类实例绑定的 [内部类 (inner class)](nested-classes.html#inner-classes) 的构造器引用: ```KOTLIN class Outer { inner class Inner } val o = Outer() val boundInnerCtor = o::Inner ``` # 解构声明 有些时候, 能够将一个对象 解构(destructure) 为多个变量, 将会很方便, 比如: ```KOTLIN val (name, age) = person ``` 这种语法称为 解构声明(destructuring declaration). 一个解构声明会一次性创建多个变量. 上例中你声明了两个变量: `name` 和 `age`, 并且可以独立地使用这两个变量: ```KOTLIN println(name) println(age) ``` 解构声明在编译时将被分解为以下代码: ```KOTLIN val name = person.component1() val age = person.component2() ``` 这里的 `component1()` 和 `component2()` 函数是 Kotlin 中广泛使用的 约定原则(principle of convention) 的又一个例子 (其它例子请参见 `+` 和 `*` 操作符, `for` 循环). 任何东西都可以作为解构声明右侧的被解构值, 只要可以对它调用足够数量的组件函数(component function). 当然, 还可以存在 `component3()` 和 `component4()` 等等. Note: `componentN()` 函数需要标记为 `operator`, 才可以在解构声明中使用. 解构声明还可以使用在 `for` 循环中: ```KOTLIN for ((a, b) in collection) { ... } ``` 上面的代码将遍历集合中的所有元素, 然后对各个元素调用 `component1()` 和 `component2()` 函数, 变量 `a` 和 `b` 将得到 `component1()` 和 `component2()` 函数的返回值. ## 示例: 从一个函数返回两个值 假如你需要从一个函数返回两个值, 比如, 一个是结果对象, 另一个是某种状态值. 在 Kotlin 中有一种紧凑的方法实现这个功能, 我们可以声明一个 [数据类](data-classes.html), 然后返回这个数据类的一个实例: ```KOTLIN data class Result(val result: Int, val status: Status) fun function(...): Result { // 计算 return Result(result, status) } // 然后, 可以这样使用这个函数: val (result, status) = function(...) ``` 由于数据类会自动声明 `componentN()` 函数, 因此可以在这里使用解构声明. Note: 你也可以使用标准库中的 `Pair` 类, 让上例中的 `function()` 函数返回一个 `Pair` 实例, 但是, 给你的数据恰当地命名, 通常是一种更好的设计. ## 示例: 解构声明与 Map 遍历一个 map 的最好的方式可能就是: ```KOTLIN for ((key, value) in map) { // 使用 key 和 value 执行某种操作 } ``` 为了让上面的代码正确运行, 你应该: * 实现 `iterator()` 函数, 使得 map 成为多个值构成的序列. * 实现 `component1()` 和 `component2()` 函数, 使得 map 内的每个元素成为一对值. Kotlin 的标准库也的确实现了这些扩展函数: ```KOTLIN operator fun Map.iterator(): Iterator> = entrySet().iterator() operator fun Map.Entry.component1() = getKey() operator fun Map.Entry.component2() = getValue() ``` 因此, 你可以在对 map 的 `for` 循环中自由地使用解构声明 (也可以在对数据类集合的 `for` 循环中使用解构声明). ## 用下划线代替未使用的变量 如果在解构声明中, 你不需要其中的某个变量, 你可以用下划线来代替变量名: ```KOTLIN val (_, status) = getResult() ``` 以这种方式跳过的变量, 不会调用对应的 `componentN()` 操作符函数. ## 在 Lambda 表达式中使用解构声明 你可以在 lambda 表达式的参数中使用解构声明语法. 如果 lambda 表达式的一个参数是 `Pair` 类型 (或 `Map.Entry` 类型, 或者任何其他类型, 只要它拥有适当的 `componentN` 函数), 就可以使用几个新的参数来代替原来的参数, 只需要将新参数包含在括号内: ```KOTLIN map.mapValues { entry -> "${entry.value}!" } map.mapValues { (key, value) -> "$value!" } ``` 请注意声明两个参数, 与将一个参数解构为多个参数的区别: ```KOTLIN { a -> ... } // 这里是一个参数 { a, b -> ... } // 这里是两个参数 { (a, b) -> ... } // 这里是将一个参数解构为两个参数 { (a, b), c -> ... } // 这里是将一个参数解构为两个参数, 然后是另一个参数 ``` 如果解构后得到的某个参数未被使用到, 你可以用下划线代替它, 这样就不必为它编造一个变量名了: ```KOTLIN map.mapValues { (_, value) -> "$value!" } ``` 你可以为解构前的整个参数指定类型, 也可以为解构后的部分参数单独指定类型: ```KOTLIN map.mapValues { (_, value): Map.Entry -> "$value!" } map.mapValues { (_, value: String) -> "$value!" } ``` ## 基于名称的解构 Kotlin 支持 基于名称的解构声明, 变量按照名称与属性匹配, 而不是象 基于位置 的解构那样, 按照 `componentN()` 函数定义的位置进行匹配. Tip: 关于基于名称的解构, 详情请参见这个功能的 [KEEP](https://github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0438-name-based-destructuring.md). 在基于位置的解构中, 变量与 `componentN()` 函数的顺序相对应, 例如: ```KOTLIN data class User(val username: String, val email: String) fun main() { val user = User("alice", "alice@example.com") val (email, username) = user println(email) // 输出结果为: alice println(username) // 输出结果为: alice@example.com } ``` 在这个示例中, 由于解构依赖于 `componentN()` 函数的顺序, `email` 会得到 `username` 的值, `username` 会得到 `email` 的值. 使用基于名称的解构, 会根据属性名决定获取哪个值, 而不是由 `componentN()` 函数的位置决定: ```KOTLIN fun main() { val user = User("alice", "alice@example.com") // 以明确指定的形式使用基于名称的解构 (val mail = email, val name = username) = user println(name) // 输出结果为: alice println(mail) // 输出结果为: alice@example.com } ``` 基于名称的解构是 [实验性功能](components-stability.html#stability-levels-explained). 启用这个功能时, 它还会引入基于位置的解构的新的语法, 使用方括号. 对于元素顺序至关重要的类型, 例如 List 和其它有顺序的集合, 以及没有名称的元组, 例如 `Pair` 或 `Triple`, 请使用这个语法: ```KOTLIN val point = Pair(10, 20) // 使用基于位置的解构 val [x, y] = point ``` 你可以使用 `-Xname-based-destructuring` 编译器选项, 控制编译器如何解释解构声明. 这个选项包含以下模式: * `only-syntax`: 启用基于名称的解构的明确调用形式, 不改变既有的解构声明的行为. * `name-mismatch`: 当对数据类使用基于位置的解构时, 如果使用的变量名称与属性名称不匹配, 报告警告. * `complete`: 启用基于名称的解构的圆括号简写形式, 并且通过方括号语法, 继续支持基于位置的解构. Tip: 在启用 `complete` 模式之前, 请先在 `name-mismatch` 模式下查看报告的警告, 并解决这些警告. 这些警告表明, 在 `complete` 模式下, 编译器对哪些解构声明的解释方式会不同, 并包含了对这些声明相应的修改建议. 如果你使用 `complete` 模式, 解构语法的圆括号简写形式会将变量匹配到属性名称, 而不是依赖它的位置: ```KOTLIN val (email, username) = user ``` 要在你的项目中启用基于名称的解构, 请对你的构建配置文件添加编译器选项: Gradle: ```KOTLIN kotlin { compilerOptions { freeCompilerArgs.add("-Xname-based-destructuring=only-syntax") } } ``` Maven: ```XML org.jetbrains.kotlin kotlin-maven-plugin -Xname-based-destructuring=only-syntax ``` # 使用 Kotlin 进行后端开发 Kotlin 非常适合于开发服务器端应用程序. 使用 Kotlin 可以编写简洁高效的代码, 同时又可以完全兼容既有的 Java 技术栈. ## 入门 Kotlin 支持大规模代码库从 Java 到 Kotlin 的逐步迁移. 你可以使用 Kotlin 编写测试, 或新的产品代码, 同时保持项目的其他部分继续使用 Java. 配置你的 Java 项目来使用 Kotlin, 并使用 IntelliJ IDEA 中包含的 Java 到 Kotlin 自动转换器: [教程 - 向 Java 项目添加 Kotlin](mixing-java-kotlin-intellij.html) ## 探索框架 Kotlin 完全兼容于所有基于 Java 的框架, 因此你可以继续使用熟悉的技术栈, 同时享受 Kotlin 语法带来的好处. 除了出色的 IDE 支持外, Kotlin 还提供针对特定框架的工具, 例如在 IntelliJ IDEA Ultimate 订阅版中对 Spring 和 Ktor 的支持. ### Spring [Spring](https://spring.io) 利用 Kotlin 的语言特性, 提供了更加简洁的 API. [在线项目生成器](https://start.spring.io/#!language=kotlin) 可以帮助你快速生成新的 Kotlin 项目. [Spring Boot 和 Kotlin 入门](jvm-get-started-spring-boot.html) ### Ktor [Ktor](https://github.com/kotlin/ktor) 是 JetBrains 公司开发的框架, 使用 Kotlin 创建 Web 应用程序. 它使用协程(Coroutine)实现高度伸缩性, 并提供易用而且符合惯用法的 API. [https://ktor.io/docs/server-create-a-new-project.html](https://ktor.io/docs/server-create-a-new-project.html) ### 其他框架 以下是其他一些 Kotlin 后端框架的示例: | 框架 |描述 | ---------- | [Quarkus](https://quarkus.io/guides/kotlin) |一个开源框架, 对 Kotlin 提供一级支持. Quarkus 是为 Kubernetes 全新构建的, 利用数百种精选的库, 提供了一个整合的全栈框架. | | [Vert.x](https://vertx.io) |一个用于在 JVM 上构建反应式(Reactive) Web 应用程序的框架. Vert.x 对 Kotlin 提供了 [专门支持](https://github.com/vert-x3/vertx-lang-kotlin), 包括 [与 Kotlin 协程的集成](https://vertx.io/docs/vertx-lang-kotlin-coroutines/kotlin/). | | [kotlinx.html](https://github.com/kotlin/kotlinx.html) |一种 DSL, 可用于在 Web 应用程序中构建 HTML. 可用来替代传统的模板系统(例如 JSP 和 FreeMarker). | | [Micronaut](https://micronaut.io/) |一个现代化的, 基于 JVM 的全栈框架, 用于构建模块化的, 易于测试的微服务(Microservice)和无服务(Serverless)应用程序. 可以观看网络研讨会 [使用 Kotlin 和 Micronaut 开发微服务](https://micronaut.io/2020/12/03/webinar-micronaut-for-microservices-with-kotlin/), 并阅读详细的 [向导](https://guides.micronaut.io/latest/micronaut-kotlin-extension-fns.html), 了解如何在 Micronaut 框架中使用 [Kotlin 扩展函数](extensions.html#extension-functions). | | [http4k](https://http4k.org/) |一个尺寸很小的工具包, 用于 Kotlin HTTP 应用程序, 使用纯 Kotlin 编写. http4k 提供了 [命令行工具箱](https://toolbox.http4k.org) 来生成完整的项目模板, 以及基于 Web 的 [项目向导](https://toolbox.http4k.org/project), 可以使用选定的后端, 模块和构建工具, 启动一个可运行的 http4k 应用程序. | | [Javalin](https://javalin.io) |一个非常轻量的 Web 框架, 用于 Kotlin 和 Java, 支持 WebSocket, HTTP2 以及异步请求. | ## 发布你的应用程序 Kotlin 应用程序可以发布到任何支持 Java Web 应用程序的主机上, 包括 Amazon Web Services (AWS), Google Cloud Platform (GCP), 以及其他许多服务. * AWS 提供了专用的 [Kotlin SDK](https://docs.aws.amazon.com/sdk-for-kotlin/latest/developer-guide/home.html) 来与它的服务交互. 对于无服务(Serverless)部署, 可以参考 [适用于 Kotlin 的 AWS Lambda 代码示例](https://docs.aws.amazon.com/sdk-for-kotlin/latest/developer-guide/kotlin_lambda_code_examples.html). * Ktor 允许你将 Kotlin 应用程序发布到各种云服务提供商. 例如, 你可以按照 Ktor 教程, 学习如何部署到 [Google App Engine](https://ktor.io/docs/google-app-engine.html) 和其他服务. * Spring 应用程序也兼容大多数流行的云服务提供商. 关于如何将 Spring Boot 应用程序部署到云端, 请参见 [Spring 官方文档](https://docs.spring.io/spring-boot/how-to/deployment/cloud.html). ## 下一步 * [学习如何使用 Kotlin 和 JUnit 测试你的 Java Maven 项目](jvm-test-using-junit.html) * [探索如何使用 Ktor 构建异步的服务器应用程序](https://ktor.io/docs/server-create-a-new-project.html) # 教程 - 向 Java 项目添加 Kotlin Kotlin 与 Java 完全兼容互通, 因此你可以将 Kotlin 逐步引入到现有的 Java 项目中, 而不需要重写所有内容. 在这篇教程中, 你将学习如何: * 设置 Maven 或 Gradle 构建工具, 编译 Java 和 Kotlin 代码. * 在项目目录中组织 Java 和 Kotlin 源代码文件. * 使用 IntelliJ IDEA 将 Java 文件转换为 Kotlin. Tip: 你可以使用任何现有的 Java 项目来完成本教程, 也可以克隆我们的公开 [示例项目](https://github.com/kotlin-hands-on/kotlin-junit-sample/tree/main/complete), 其中已经设置好了 Maven 和 Gradle 构建文件. 你也可以使用我们 [已准备好的技能](https://github.com/Kotlin/kotlin-agent-skills/blob/main/skills/kotlin-tooling-java-to-kotlin/SKILL.md), 将转换工作交给你选择的 AI Agent 来完成. 请注意, AI 处理的结果并不完全可预测. ## 项目配置 要向 Java 项目添加 Kotlin, 你需要根据所使用的构建工具, 将项目配置为同时使用 Kotlin 和 Java. 项目配置确保 Kotlin 和 Java 代码都能正确编译, 并且可以无缝地相互引用. ### Maven Note: 从 IntelliJ IDEA 2025.3 开始, 当你向基于 Maven 的 Java 项目添加第一个 Kotlin 文件时, IDE 会自动更新你的 `pom.xml` 文件, 添加 Kotlin Maven plugin 和标准的依赖项. 你仍然可以手动配置, 自定义版本或构建阶段. 要在 Maven 项目中同时使用 Kotlin 和 Java, 请在 `pom.xml` 文件中应用 Kotlin Maven plugin, 并添加 Kotlin 依赖项: 1. 在 `` 部分, 添加 Kotlin 版本属性: ```XML 2.4.0 ``` 2. 在 `` 部分, 向 `` 部分添加所需的依赖项: ```XML org.junit.jupiter junit-jupiter-engine test org.junit.jupiter junit-jupiter-params test ``` 3. 在 `` 部分, 添加 Kotlin plugin: ```XML org.jetbrains.kotlin kotlin-maven-plugin ${kotlin.version} true ``` 在 Kotlin Maven plugin 中启用 `true` 有助于: * 向项目自动添加 `kotlin-stdlib` 依赖项. * 配置执行阶段, 先编译 Kotlin, 然后编译 Java. * 在 Java 代码中引用 Kotlin 代码, 或者相反. * 自动将 JVM 目标版本与 Java 编译器版本对齐. 使用带有 extensions 的 Kotlin Maven plugin 时, 不需要在 `` 部分单独配置 `maven-compiler-plugin`. 4. 在 IDE 中重新加载 Maven 项目. 5. 运行测试, 验证配置: ```BASH ./mvnw clean test ``` ### Gradle 要在 Gradle 项目中同时使用 Kotlin 和 Java, 请在 `build.gradle.kts` 文件中应用 Kotlin JVM plugin, 并添加 Kotlin 依赖项: 1. 在 `plugins {}` 代码块中, 添加 Kotlin JVM plugin: ```KOTLIN plugins { // 其他 plugin kotlin("jvm") version "2.4.0" } ``` 2. 设置 JVM 工具链版本, 与你的 Java 版本匹配: ```KOTLIN kotlin { jvmToolchain(17) } ``` 这个设置会确保 Kotlin 使用与 Java 代码相同的 JDK 版本. 3. 在 `dependencies {}` 代码块中, 添加 `kotlin("test")` 库, 这个库提供 Kotlin 测试工具并与 JUnit 集成: ```KOTLIN dependencies { // 其他依赖项 testImplementation(kotlin("test")) // 其他测试依赖项 } ``` 4. 在 IDE 中重新加载 Gradle 项目. 5. 运行测试, 验证配置: ```BASH ./gradlew clean test ``` ## 项目结构 通过这种配置, 你可以在同一个源代码目录中混合使用 Java 和 Kotlin 文件: ``` src/ ├── main/ │ ├── java/ # Java 和 Kotlin 生产代码 │ └── kotlin/ # 额外的 Kotlin 生产代码(可选) └── test/ ├── java/ # Java 和 Kotlin 测试代码 └── kotlin/ # 额外的 Kotlin 测试代码(可选) ``` 你可以手动创建这些目录, 也可以在添加第一个 Kotlin 文件时让 IntelliJ IDEA 自动创建. Kotlin 插件会自动识别 `src/main/java` 和 `src/test/java` 目录, 因此你可以将 `.kt` 和 `.java` 文件放在同一目录中. ## 将 Java 文件转换为 Kotlin Kotlin plugin 还附带了一个 Java 到 Kotlin 的转换器(J2K), 可以将 Java 文件自动转换为 Kotlin. 要对文件使用 J2K, 请在其右键菜单中, 或在 IntelliJ IDEA 的 Code 菜单中, 点击 Convert Java File to Kotlin File. ![将 Java 文件转换为 Kotlin](images/convert-java-to-kotlin.png) 虽然转换器并不能保证完全正确, 但它能很好地将大多数样板代码从 Java 转换为 Kotlin. 但是, 有时仍然需要一些手动调整. ## 探索编译器 plugin 如果你有一个更加复杂的项目, 使用了 [Spring](https://spring.io/) 或 Java Persistence API (JPA), 你可以使用 Kotlin 编译器 plugin, 这些 plugin 会自动使 Kotlin 的语言特性适应框架的要求, 减少样板代码: * [all-open](all-open-plugin.html) plugin, 在使用特定注解时会自动使类及其成员变为 `open`. 这对于 Spring 等要求类为非 final 的框架特别有用. 对于 Spring, 你可以使用专用的 [kotlin-spring](all-open-plugin.html#spring-support) plugin, 它是 `all-open` 的封装. 它会自动指定 Spring 注解. * [no-arg](no-arg-plugin.html) plugin, 为带有特定注解的类生成额外的无参数构造器. 让 JPA 能够实例化那些原本没有默认构造器的类. 你也可以使用 [kotlin-jpa](no-arg-plugin.html#jpa-support) plugin, 它是 `no-arg` 的封装. 它会自动指定 no-arg 注解. * [power-assert](power-assert.html) plugin, 为断言提供包含上下文信息的详细失败消息, 改善调试体验. 它会显示中间值, 帮助你理解测试失败的原因. ## 下一步 在 Java 项目中开始使用 Kotlin 最简单的方法, 是先添加 Kotlin 测试: [向你的 Java 项目添加第一个 Kotlin 测试](jvm-test-using-junit.html) ### 参见 * [Kotlin 与 Java 互操作的详细文档](java-to-kotlin-interop.html) * [Maven 构建配置参考](maven.html) # 教程 - 使用 Kotlin 和 JUnit 测试 Java 代码 Kotlin 与 Java 完全兼容互通, 因此你可以使用 Kotlin 为 Java 代码编写测试, 并与项目中已有的 Java 测试一起运行. 在本教程中, 你将学习如何: * 配置 Java-Kotlin 混合项目, 使用 [JUnit](https://junit.org/) 运行测试. * 添加 Kotlin 测试, 用于验证 Java 代码. * 使用 Maven 或 Gradle 运行测试. Note: 在开始之前, 请确认你已安装: * [IntelliJ IDEA](https://www.jetbrains.com/idea/download/), 或安装了 [Kotlin 扩展](https://github.com/Kotlin/kotlin-lsp/tree/main?tab=readme-ov-file#vs-code-quick-start) 的 [VS Code](https://code.visualstudio.com/Download). * Java 17 或更高版本. ## 配置项目 1. 在 IDE 中, 从版本控制系统中克隆示例项目: ```TEXT https://github.com/kotlin-hands-on/kotlin-junit-sample.git ``` 2. 进入 `initial` 模块, 查看项目结构: ```TEXT kotlin-junit-sample/ ├── initial/ │ ├── src/ │ │ ├── main/java/ # Java 源代码 │ │ └── test/java/ # Java 编写的 JUnit 测试 │ ├── pom.xml # Maven 配置 │ └── build.gradle.kts # Gradle 配置 ``` `initial` 模块包含一个简单的 Java Todo 应用程序, 带有一个测试. 3. 在同一目录中, 打开构建文件, 更新其内容, 以便支持 Kotlin: Maven: ```XML 4.0.0 org.jetbrains.kotlin kotlin-junit-complete 1.0-SNAPSHOT kotlin-junit-complete https://kotlinlang.org/docs/jvm-test-using-junit.htm UTF-8 17 1.6.0 2.4.0 org.junit junit-bom 6.0.3 pom import org.junit.jupiter junit-jupiter-api test org.junit.jupiter junit-jupiter-engine test org.junit.jupiter junit-jupiter-params test com.gitlab.klamonte jexer ${jexer.version} maven-clean-plugin 3.4.0 maven-resources-plugin 3.3.1 maven-surefire-plugin 3.3.0 maven-jar-plugin 3.4.2 maven-install-plugin 3.1.2 maven-deploy-plugin 3.1.2 maven-site-plugin 3.12.1 maven-project-info-reports-plugin 3.6.1 org.jetbrains.kotlin kotlin-maven-plugin ${kotlin.version} true ``` * 在 `` 部分, 设置 Kotlin 版本. * 在 `` 部分, 添加 JUnit Jupiter 依赖项, 以便运行测试. * 在 `` 部分, 应用 `kotlin-maven-plugin`, 并将 `` 设置为 `true`. 这个设置会自动向构建中添加相应的执行配置和 `kotlin-stdlib` 依赖项. * 使用带有 extensions 的 Kotlin Maven plugin 时, 不需要向 `` 部分添加 `maven-compiler-plugin`. Gradle: ```KOTLIN // build.gradle.kts group = "org.jetbrains.kotlin" version = "1.0-SNAPSHOT" description = "kotlin-junit-complete" java.sourceCompatibility = JavaVersion.VERSION_17 plugins { application kotlin("jvm") version "2.4.0" } kotlin { jvmToolchain(17) } application { mainClass.set("org.jetbrains.kotlin.junit.App") } repositories { mavenCentral() } dependencies { implementation("com.gitlab.klamonte:jexer:1.6.0") testImplementation(kotlin("test")) testImplementation(libs.org.junit.jupiter.junit.jupiter.api) testImplementation(libs.org.junit.jupiter.junit.jupiter.params) testRuntimeOnly(libs.org.junit.jupiter.junit.jupiter.engine) testRuntimeOnly(libs.org.junit.platform.junit.platform.launcher) } tasks.test { useJUnitPlatform() } ``` * 在 `plugins {}` 代码块中, 添加 `kotlin("jvm")` plugin. * 设置 JVM 工具链版本, 与 Java 版本匹配. * 在 `dependencies {}` 代码块中, 添加 `kotlin.test` 库, 该库提供 Kotlin 的测试工具, 与 JUnit 集成. Kotlin/JVM 支持 JUnit 的最新稳定版本, JUnit 6. 你可以在 `gradle/libs.versions.toml` 版本目录中找到它. 如果你更倾向于使用版本目录, 甚至可以在版本目录中添加 `kotlin("jvm")` plugin: ```TOML # gradle/libs.versions.toml [versions] kotlin = "2.4.0" junit = "6.0.3" [libraries] org-junit-jupiter-junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } org-junit-jupiter-junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } org-junit-jupiter-junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } org-junit-platform-junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } [plugins] kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } ``` 4. 在 IDE 中重新加载构建文件. 关于构建文件设置, 详情请参见 [项目配置](mixing-java-kotlin-intellij.html#project-configuration). ## 添加你的第一个 Kotlin 测试 `initial/src/test/java` 中的 `TodoItemTest.java` 测试已经验证了应用程序的基础功能: 包括条目创建, 默认值, 唯一 ID, 以及状态变更. 你可以通过添加 Kotlin 测试, 验证存储库级别的行为, 来扩展测试覆盖范围: 1. 进入相同的测试源代码目录 `initial/src/test/java`. 2. 在与 Java 测试相同的包中创建 `TodoRepositoryTest.kt` 文件. 3. 创建测试类, 包含字段声明和 setup 函数: ```KOTLIN package org.jetbrains.kotlin.junit import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.junit.jupiter.api.DisplayName internal class TodoRepositoryTest { lateinit var repository: TodoRepository lateinit var testItem1: TodoItem lateinit var testItem2: TodoItem @BeforeEach fun setUp() { repository = TodoRepository() testItem1 = TodoItem("Task 1", "Description 1") testItem2 = TodoItem("Task 2", "Description 2") } } ``` * JUnit 注解在 Kotlin 中的使用方式, 与在 Java 中相同. * 在 Kotlin 中, [lateinit 关键字](properties.html#late-initialized-properties-and-variables) 允许声明非 null 属性, 并在之后进行初始化. 这有助于避免在测试中使用可为 null 的类型(`TodoRepository?`). 4. 在 `TodoRepositoryTest` 类中添加一个测试, 检查存储库的初始状态及其大小: ```KOTLIN @Test @DisplayName("Should start with empty repository") fun shouldStartEmpty() { Assertions.assertEquals(0, repository.size()) Assertions.assertTrue(repository.all.isEmpty()) } ``` * 与 Java 的静态导入不同, Jupiter 的 `Assertions` 是作为类导入的, 并用作断言函数的限定符. * 你可以在 Kotlin 中以 `repository.all` 的方式将 Java 的 getter 作为属性访问, 而不必调用 `.getAll()`. 5. 再编写一个测试, 验证所有条目的复制行为: ```KOTLIN @Test @DisplayName("Should return defensive copy of items") fun shouldReturnDefensiveCopy() { repository.add(testItem1) val items1 = repository.all val items2 = repository.all Assertions.assertNotSame(items1, items2) Assertions.assertThrows( UnsupportedOperationException::class.java ) { items1.clear() } Assertions.assertEquals(1, repository.size()) } ``` * 要从 Kotlin 类获取 Java 类对象, 请使用 `::class.java`. * 你可以将复杂的断言拆分为多行, 不需要使用任何特殊的续行字符. 6. 添加一个测试, 验证通过 ID 查找条目的功能: ```KOTLIN @Test @DisplayName("Should find item by ID") fun shouldFindItemById() { repository.add(testItem1) repository.add(testItem2) val found = repository.getById(testItem1.id()) Assertions.assertTrue(found.isPresent) Assertions.assertEquals(testItem1, found.get()) } ``` Kotlin 能够与 Java 的 [Optional API](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Optional.html) 无缝协作. 它会自动将 getter 方法转换为属性, 所以 `isPresent()` 方法在这里可以作为属性访问. 7. 编写一个测试, 验证条目的删除机制: ```KOTLIN @Test @DisplayName("Should remove item by ID") fun shouldRemoveItemById() { repository.add(testItem1) repository.add(testItem2) val removed = repository.remove(testItem1.id()) Assertions.assertTrue(removed) Assertions.assertEquals(1, repository.size()) Assertions.assertTrue(repository.getById(testItem1.id()).isEmpty) Assertions.assertTrue(repository.getById(testItem2.id()).isPresent) } @Test @DisplayName("Should return false when removing non-existent item") fun shouldReturnFalseForNonExistentRemoval() { repository.add(testItem1) val removed = repository.remove("non-existent-id") Assertions.assertFalse(removed) Assertions.assertEquals(1, repository.size()) } ``` 在 Kotlin 中, 可以链式调用方法和属性访问, 例如 `repository.getById(id).isEmpty`. Tip: 你可以向 `TodoRepositoryTest` 测试类添加更多测试, 覆盖额外的功能. 请在示例项目的 [complete](https://github.com/kotlin-hands-on/kotlin-junit-sample/blob/main/complete/src/test/java/org/jetbrains/kotlin/junit/TodoRepositoryTest.kt) 模块中, 查看完整的代码. ## 运行测试 运行 Java 和 Kotlin 测试, 验证项目是否正确工作: 1. 使用侧栏图标(gutter icon)运行测试: ![运行测试](images/run-test.png)也可以从 `initial` 目录, 使用命令行运行所有的项目测试: Maven: ```BASH mvn test ``` Gradle: ```BASH ./gradlew test ``` 2. 修改某个变量值, 检查测试是否正确工作. 例如, 修改 `shouldAddItem` 测试, 期望出现一个错误的存储库大小: ```KOTLIN @Test @DisplayName("Should add item to repository") fun shouldAddItem() { repository.add(testItem1) Assertions.assertEquals(2, repository.size()) // 从 1 改为 2 Assertions.assertTrue(repository.all.contains(testItem1)) } ``` 3. 再次运行测试, 确认测试失败: ![检查测试结果. 测试失败](images/test-failed.png) Tip: 你可以在示例项目的 [complete](https://github.com/kotlin-hands-on/kotlin-junit-sample/tree/main/complete) 模块中, 找到完整配置的项目, 以及测试代码. ## 下一步做什么? 了解更多关于 [使用 Maven 测试 Kotlin 项目](jvm-test-maven.html) 的内容. # 使用 Maven 测试 Kotlin 项目 Kotlin 能够与 Maven 生态系统无缝集成, 让你使用业界标准的工具来验证你的后端应用程序. 这篇指南介绍如何使用 JUnit 创建测试, 以及使用 Maven plugin 运行单元测试和集成测试. Tip: 关于设置 Maven 项目并使用 Kotlin 和 Java 的详细指南, 请参见 [项目配置](mixing-java-kotlin-intellij.html#project-configuration). ## 使用 JUnit 创建测试 [JUnit](https://junit.org/) 是 Kotlin 后端开发的标准测试框架. Kotlin 支持多个 JUnit 版本, 但大多数现代项目应该使用 JUnit 6. 要使用 JUnit 在 Kotlin 中创建测试, 请使用来自 `kotlin.test` 或 JUnit 包的 `@Test` 注解. ### 添加依赖项 最简单的方式是使用 `kotlin-test` 库. 它提供一套通用的断言函数, 并自动引入需要的 JUnit artifact. #### JUnit 5 及更高版本 对于所有新项目, 请使用 `kotlin-test-junit5` artifact. 它提供对 JUnit 的完整支持, 包括嵌套测试和并行执行等功能. Kotlin/JVM 支持 JUnit 的最新稳定版本, JUnit 6. 按如下方式更新你的 `pom.xml` 文件: ```XML org.jetbrains.kotlin kotlin-test-junit5 2.4.0 test ``` Note: 尽管名称是 junit5, 但 `kotlin-test-junit5` 支持所有最新的 JUnit 版本, 包括 JUnit 6. #### JUnit 4 如果你希望使用旧版本的 JUnit, 例如用于旧的项目, 请使用基于 JUnit 4 的 `kotlin-test-junit` artifact: ```XML org.jetbrains.kotlin kotlin-test-junit 2.4.0 test ``` Tip: 关于使用 JUnit 进行测试的详细指南和示例项目, 请参见 [使用 Kotlin 测试 Java 代码](jvm-test-using-junit.html) 教程. ### 编写单元测试(Unit Test) 单元测试(Unit Test)用于验证代码中独立的部分, 例如单独的函数或类. 按照惯例, 单元测试的命名使用 `*Test` 后缀. 例如: ```KOTLIN import kotlin.test.Test import kotlin.test.assertEquals class OrderServiceTest { @Test fun `calculate total should sum item prices`() { val service = OrderService() val result = service.calculateTotal(listOf(10.0, 25.0)) assertEquals(35.0, result) } } ``` ### 编写集成测试(Integration Test) 集成测试(Integration Test)用于验证组件之间的交互, 例如服务与数据库之间的交互. 按照惯例, 集成测试的命名使用 `*IT` 后缀. 例如: ```KOTLIN import kotlin.test.Test import kotlin.test.assertNotNull class UserRepositoryIT { @Test fun saveFindUser() { // 示例: 与数据库或服务集成 val repository = UserRepository() repository.save(User("KotlinUser")) val user = repository.findByName("KotlinUser") assertNotNull(user) } } ``` ## 运行测试 在 Maven 项目中, 测试执行通常由两个 plugin 共同负责: Surefire 和 Failsafe, 以确保构建生命周期清晰有序. ### 使用 Surefire plugin [Surefire plugin](https://maven.apache.org/surefire/maven-surefire-plugin/) 负责处理 单元测试(Unit Test). 它运行所有符合 `*Test` 命名规范的 Kotlin 和 Java 测试. 默认情况下, 它在构建生命周期的 `test` 阶段执行, 如果测试失败则立即中止构建. ```XML org.apache.maven.plugins maven-surefire-plugin 3.5.5 ``` 要只运行单元测试, 请使用以下命令: ```BASH mvn test ``` ### 使用 Failsafe plugin [Failsafe plugin](https://maven.apache.org/surefire/maven-failsafe-plugin/) 负责处理 集成测试(Integration Test). 它运行所有符合 `*IT` 命名规范的 Kotlin 和 Java 测试. 与 Surefire 不同, Failsafe 允许构建在 `integration-test` 阶段的测试失败后继续执行, 使得 `post-integration-test` 阶段的任务(例如停止 Docker 容器)能够运行. 如果存在测试失败, 构建最终会在 `verify` 阶段报告失败. ```XML org.apache.maven.plugins maven-failsafe-plugin 3.5.5 integration-test verify ``` 要同时运行单元测试和集成测试, 请使用以下命令: ```BASH mvn verify ``` ## 探索其他测试框架 除了 JUnit 之外, 还可以使用其他流行的框架, 让 Kotlin 测试代码更加符合惯用法, 更加易读: | 库 |说明 | --------- | [AssertJ](https://github.com/assertj/assertj) |支持链式调用的流式断言库. | | [Mockito-Kotlin](https://github.com/mockito/mockito-kotlin) |Mockito 的 Kotlin 封装库, 提供辅助函数, 与 Kotlin 类型系统更好地集成. | | [MockK](https://github.com/mockk/mockk) |原生的 Kotlin mock 库, 支持 Kotlin 的特有功能, 包括协程和扩展函数. | | [Kotest](https://github.com/kotest/kotest) |面向 Kotlin 的断言库, 提供多种断言风格和丰富的匹配器支持. | | [Strikt](https://github.com/robfletcher/strikt) |面向 Kotlin 的断言库, 提供类型安全的断言以及对数据类的支持. | ## 下一步做什么? * 探索 [kotlin.test 库](https://kotlinlang.org/api/latest/kotlin.test/kotlin.test/) 的功能. * 使用 [Kotlin 的 Power-assert 编译器 plugin](power-assert.html), 改善测试的输出. # 在 Kotlin 项目中使用注解处理器 * 对以下情况, 请使用 [kapt](kapt.html): * 你有一个 Maven 项目. * 你有一个 Gradle 项目, 但需要的 Java 注解处理器还不支持 KSP. [查看支持的库列表](ksp-overview.html#supported-libraries). * 对以下情况, 请使用 [KSP](ksp-overview.html): * 你有一个 Gradle 项目, 并且需要的 Java 注解处理器支持 KSP. * 你希望创建自己的注解处理器. 注解处理器在编译期间分析你的源代码, 以生成样板代码, 验证用法, 或生成其他构件(artifact). Kotlin 支持两种方式来使用注解处理器: * [kapt 编译器 plugin](#use-kapt-with-java-annotation-processors), 工作方式是, 从 Kotlin 源代码生成桩(stub)文件, 然后在这些桩(stub)上运行 Java 注解处理器. 这个额外的桩(stub)生成步骤会使构建时间变慢, 同时意味着 kapt 无法理解 Kotlin 特有的构造, 例如 [扩展函数](extensions.html) 或 [null 安全](null-safety.html). kapt 同时支持 Maven 和 Gradle. 推荐用于所有的 Maven 项目, 以及那些使用尚未采用 KSP 的处理器库的 Gradle 项目, 例如 [MapStruct](https://mapstruct.org/). * [KSP 框架](#use-ksp-in-gradle-projects), 通过 Kotlin 优先的 API 直接读取 Kotlin 源代码, 不需要生成桩. 它能够原生的理解 Kotlin 特有的功能, 构建速度比 kapt 更快. 目前, KSP 只对 Gradle 提供官方支持. 推荐用于编写自己的处理器, 以及与支持 KSP 的库 (例如 [Dagger](https://dagger.dev/)) 配合使用. ## 配合使用 kapt 与 Java 注解处理器 [kapt](kapt.html) 让你能够在 Kotlin 项目中使用既有的 Java 注解处理器, 不需要对处理器本身做任何修改. 下面的示例演示如何使用 [MapStruct](https://mapstruct.org/) 注解处理器. MapStruct 会在编译期间生成 Java Bean 之间类型安全的 mapper 实现. 1. 在你的构建文件中, 应用 `kapt` plugin, 并将 MapStruct 添加到 `dependencies` 部分: Maven: ```XML 11 1.6.3 org.mapstruct mapstruct ${mapstruct.version} org.jetbrains.kotlin kotlin-maven-plugin ${kotlin.version} true kapt kapt src/main/kotlin src/main/java stubs org.mapstruct mapstruct-processor ${mapstruct.version} ``` * 将来自 `kotlin-maven-plugin` 的 `kapt` goal 的执行, 添加到 `compile` 执行 之前. * 使用 `aptMode` 选项, 配置 [注解处理级别](kapt.html#use-in-maven). Gradle Kotlin: ```KOTLIN plugins { kotlin("kapt") version "2.4.0" } dependencies { implementation("org.mapstruct:mapstruct:1.6.3") kapt("org.mapstruct:mapstruct-processor:1.6.3") } ``` Gradle Groovy: ```GROOVY plugins { id "org.jetbrains.kotlin.kapt" version "2.4.0" } dependencies { implementation "org.mapstruct:mapstruct:1.6.3" kapt "org.mapstruct:mapstruct-processor:1.6.3" } ``` 2. 定义你的数据类和 mapper 接口: ```KOTLIN import org.mapstruct.Mapper import org.mapstruct.factory.Mappers data class UserDto(val id: Long, val firstName: String, val lastName: String) data class UserEntity(val id: Long, val firstName: String, val lastName: String) @Mapper interface UserMapper { fun toDto(entity: UserEntity): UserDto fun toEntity(dto: UserDto): UserEntity companion object : UserMapper by Mappers.getMapper(UserMapper::class.java) } ``` 3. 构建项目. MapStruct 会在生成的源代码目录中生成 `UserMapperImpl` 类. 使用 `UserMapper` 同伴对象来调用生成的实现: ```KOTLIN fun main() { val entity = UserEntity(id = 1L, firstName = "John", lastName = "Doe") val dto = UserMapper.toDto(entity) println(dto) // 输出结果为: UserDto(id=1, firstName=John, lastName=Doe) } ``` ## 在 Gradle 项目中使用 KSP 使用 [KSP](ksp-overview.html), 你可以在 Gradle 项目中使用既有的注解处理器, 也可以创建自己的处理器, 根据源代码中的注解来生成代码. ### 配合使用 KSP 与 Java 注解处理器 对于 Gradle 项目, 请将 KSP 与兼容的注解处理器配合使用. KSP 比 kapt 更快, 并且能够原生理解 Kotlin 特有的功能. 请查看 [已支持 KSP 的库列表](ksp-overview.html#supported-libraries). 下面的示例演示如何使用 [Dagger](https://dagger.dev/), 这是一个编译期间依赖注入框架, 它根据依赖图生成连接代码. 1. 在你的 `build.gradle(.kts)` 文件中, 应用 KSP plugin, 并将 Dagger 添加到 `dependencies` 代码块: Kotlin: ```KOTLIN // build.gradle.kts plugins { kotlin("jvm") version "2.4.0" id("com.google.devtools.ksp") version "2.3.9" } dependencies { implementation("com.google.dagger:dagger:2.59.2") ksp("com.google.dagger:dagger-compiler:2.59.2") } ``` Groovy: ```GROOVY // build.gradle plugins { id 'org.jetbrains.kotlin.jvm' version '2.4.0' id 'com.google.devtools.ksp' version '2.3.9' } dependencies { implementation 'com.google.dagger:dagger:2.59.2' ksp 'com.google.dagger:dagger-compiler:2.59.2' } ``` Tip: 要查找 KSP 的最新版本, 请查看 GitHub 上的 [Releases](https://github.com/google/ksp/releases) 页面. 2. 使用 Dagger 注解, 对你的 Kotlin 类进行标注: ```KOTLIN import javax.inject.Inject import javax.inject.Singleton import dagger.Component import dagger.Module import dagger.Provides @Singleton class UserRepository @Inject constructor() { fun getUser(): String = "John Doe" } @Module class AppModule { @Provides @Singleton fun provideUserRepository(): UserRepository = UserRepository() } @Singleton @Component(modules = [AppModule::class]) interface AppComponent { fun userRepository(): UserRepository } ``` 3. 构建项目. Dagger 会在 `build/generated/ksp` 目录中生成实现类, 例如 `DaggerAppComponent`. 在你的代码中使用生成的类: ```KOTLIN fun main() { val appComponent = DaggerAppComponent.create() val userRepository = appComponent.userRepository() println("User: ${userRepository.getUser()}") // 输出结果为: User: John Doe } ``` 关于 Dagger 对 KSP 的支持, 详情请参见它的 [文档](https://dagger.dev/dev-guide/ksp.html). ### 创建你自己的注解处理器 你可以使用 KSP API 编写自己的注解处理器, 在编译期间生成代码. 一个新的处理器需要 3 个模块: * 一个 `annotation` 模块, 用于声明自定义注解. * 一个 `processor` 模块, 用于实现 `SymbolProcessor` 和 `SymbolProcessorProvider` 工厂类. `SymbolProcessor` 包含主逻辑, `SymbolProcessorProvider` 创建处理器, 并在 `META-INF/services/` 路径下注册 provider. * 一个 `app` 模块, 用于应用 KSP plugin, 依赖于处理器, 并使用注解. 关于完整的逐步说明, 请参见 [KSP 快速入门](ksp-quickstart.html#create-your-own-processor). ## 下一步做什么? * [了解 kapt 配置](kapt.html) * [开始使用 KSP](ksp-quickstart.html) * [了解如何从 kapt 迁移到 KSP](ksp-kapt-migration.html) # Spring Boot 和 Kotlin 入门 通过完成这个教程, 学习使用 Spring Boot 和 Kotlin: 本教程将会带领你使用 Spring Boot 创建一个简单的应用程序, 并添加数据库来存储信息. 完成以下 4 个步骤, 你将会学到 Kotlin 语言的很多基本功能: ![第 1 步](images/icon-1.svg) [创建 Spring Boot 项目](jvm-create-project-with-spring-boot.html) ![第 2 步](images/icon-2.svg) [向 Spring Boot 项目添加数据类](jvm-spring-boot-add-data-class.html) ![第 3 步](images/icon-3.svg) [为 Spring Boot 项目添加数据库支持](jvm-spring-boot-add-db-support.html) ![第 4 步](images/icon-4.svg) [使用 Spring Data CrudRepository 进行数据库访问](jvm-spring-boot-using-crudrepository.html) ## 下一步 首先, 使用 IntelliJ IDEA 和 Kotlin [创建一个 Spring Boot 项目](jvm-create-project-with-spring-boot.html). ### 参见 如果你想要与 AI Agent 一起工作, 请阅读我们的教程 [使用 Spring Boot 和 Claude 创建一个任务管理应用程序](spring-boot-claude.html). 请阅读我们的 Java 到 Kotlin (J2K) 的互操作和迁移向导: * [在 Kotlin 中调用 Java 代码](java-interop.html) 以及 [在 Java 中调用 Kotlin 代码](java-to-kotlin-interop.html) * [Java 和 Kotlin 中的集合(Collection)](java-to-kotlin-collections-guide.html) * [Java 和 Kotlin 中的字符串](java-to-kotlin-idioms-strings.html) ## 加入开发社区 * ![Slack](images/slack.svg) Kotlin slack: 首先 [得到邀请](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up), 然后加入 [#spring](https://kotlinlang.slack.com/archives/C0B8ZTWE4) 和 [#server](https://kotlinlang.slack.com/archives/C0B8RC352) 频道. * ![Stack Overflow](images/stackoverflow.svg) Stack Overflow: 订阅 ["kotlin"](https://stackoverflow.com/questions/tagged/kotlin), ["spring-kotlin"](https://stackoverflow.com/questions/tagged/spring-kotlin), 或 ["ktor"](https://stackoverflow.com/questions/tagged/ktor) 标签 * ![YouTube](images/youtube.svg) Kotlin YouTube channel: 订阅并观看关于 [使用 Spring 开发 Kotlin](https://www.youtube.com/playlist?list=PLlFc5cFwUnmxOJL0GSSZ1Vot4KL2Vwe7x) 的视频 # 使用 Kotlin 创建 Spring Boot 项目 这是 Spring Boot 和 Kotlin 入门 教程的第 1 部分: ![第 1 步](images/icon-1.svg) 使用 Kotlin 创建 Spring Boot 项目 ![第 2 步](images/icon-2-todo.svg) 向 Spring Boot 项目添加数据类 ![第 3 步](images/icon-3-todo.svg) 为 Spring Boot 项目添加数据库支持 ![第 4 步](images/icon-4-todo.svg) 使用 Spring Data CrudRepository 进行数据库访问 本教程的第 1 部分演示如何在 IntelliJ IDEA 中使用 Project Wizard 创建一个 Spring Boot 的 Gradle 项目. Note: 本教程不要求使用 Gradle 作为构建系统. 如果你使用 Maven, 也可以遵循相同的步骤. ## 开始之前的准备 下载并安装 [IntelliJ IDEA](https://www.jetbrains.com/idea/download/) 的最新版, 并使用 Ultimate 订阅. Tip: 如果你使用的是没有 Ultimate 订阅的 IntelliJ IDEA, 或使用其他 IDE, 你可以使用 [基于 Web 页面的项目生成器](https://start.spring.io/#!language=kotlin&type=gradle-project-kotlin) 来生成 Spring Boot 项目. ## 创建 Spring Boot 项目 使用 IntelliJ IDEA 中的 Project Wizard, 创建新的使用 Kotlin 的 Spring Boot 项目: 1. 在 IntelliJ IDEA 中, 选择 File | New | Project. 2. 在左侧面板中, 选择 Generators 中的 Spring Boot. 3. 在 New Project 窗口中, 指定以下项目和选项: * Name: demo * Language: Kotlin * Type: Gradle - Kotlin Tip: 这个选项指定构建系统和 DSL. * Package name: com.example.demo * JDK: Java JDK Note: 本教程使用 Amazon Corretto version 23. 如果你没有安装 JDK, 可以从下拉列表中下载. * Java: 17 Tip: 如果你没有安装 Java 17, 可以从 JDK 下拉列表中下载. ![创建 Spring Boot 项目](images/create-spring-boot-project.png) 4. 确认填写了所有的项目, 然后点击 Next. 5. 选择以下依赖项, 本教程将会需要它们: * Web | Spring Web * SQL | Spring Data JDBC * SQL | H2 Database ![设置 Spring Boot 项目](images/set-up-spring-boot-project.png) 6. 点击 Create, 生成并设置项目. Tip: IDE 将会生成并打开新的项目. 可能需要一些时间来下载并导入项目的依赖项. 7. 之后, 你可以在 Project view 中看到下面的项目结构: ![设置 Spring Boot 项目](images/spring-boot-project-view.png)生成的 Gradle 项目符合 Maven 的标注目录布局: * 在 `main/kotlin` 文件夹下是属于应用程序的包和类. * 应用程序的入口点是 `DemoApplication.kt` 文件的 `main()` 方法. ## 查看项目的 Gradle 构建文件 打开 `build.gradle.kts` 文件: 它是 Gradle Kotlin 构建脚本, 包含应用程序需要的依赖项目列表. Gradle 文件是用于 Spring Boot 的标准内容, 但它也包含必须的 Kotlin 依赖项, 包括 kotlin-spring Gradle plugin – `kotlin("plugin.spring")`. 下面是完整的脚本, 包括各部分和依赖项的解释: ```KOTLIN // build.gradle.kts plugins { kotlin("jvm") version "2.2.21" // 使用的 Kotlin 版本 kotlin("plugin.spring") version "2.2.21" // Kotlin Spring plugin id("org.springframework.boot") version "4.0.2" id("io.spring.dependency-management") version "1.1.7" } group = "com.example" version = "0.0.1-SNAPSHOT" java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } repositories { mavenCentral() } dependencies { implementation("org.springframework.boot:spring-boot-h2console") implementation("org.springframework.boot:spring-boot-starter-data-jdbc") implementation("org.springframework.boot:spring-boot-starter-webmvc") implementation("org.jetbrains.kotlin:kotlin-reflect") // Kotlin 反射库, 使用 Spring 时需要 implementation("tools.jackson.module:jackson-module-kotlin") // Jackson 的 Kotlin 扩展, 用于使用 JSON runtimeOnly("com.h2database:h2") testImplementation("org.springframework.boot:spring-boot-starter-data-jdbc-test") testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } kotlin { compilerOptions { freeCompilerArgs.addAll("-Xjsr305=strict", "-Xannotation-default-target=param-property") // `-Xjsr305=strict` 对 JSR-305 注解启用 strict 模式 } } tasks.withType { useJUnitPlatform() } ``` 你可以看到, Gradle 构建文件中添加了几个与 Kotlin 相关的库: 1. 在 `plugins` 代码段中, 有 2 个 Kotlin 库: * `kotlin("jvm")` plugin, 定义在项目中使用的 Kotlin 版本. * Kotlin Spring 编译器 plugin, `kotlin("plugin.spring")`, 向 Kotlin 类添加 `open` 修饰符, 使它们能够与 Spring Framework 中的功能兼容. 2. 在 `dependencies` 代码段中, 有几个 Kotlin 相关的模块: * `tools.jackson.module:jackson-module-kotlin` 模块, 支持Kotlin 类和数据类的序列化和反序列化. * `org.jetbrains.kotlin:kotlin-reflect` 是一个 Kotlin 反射库, 包括对 [反射功能](reflection.html) 的完整支持. 3. 在依赖项之后, 你可以看到 `kotlin` plugin 配置模块. 在这里你可以向编译器添加额外的参数, 来启用或禁用某些语言特性. 关于 Kotlin 编译器选项, 详情请参见 [Kotlin Gradle plugin 中的编译器选项](gradle-compiler-options.html). ## 查看生成的 Spring Boot 应用程序 打开 `DemoApplication.kt` 文件: ```KOTLIN // DemoApplication.kt package com.example.demo import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication class DemoApplication fun main(args: Array) { runApplication(*args) } ``` 声明类 – DemoApplication 类 : 在包声明和 import 语句之后, 你可以看到第一个类声明, `class DemoApplication`. : 在 Kotlin 中, 如果一个类不包含任何成员 (属性或函数), 你可以直接省略掉类的主体部分 (`{}`). @SpringBootApplication 注解 : [@SpringBootApplication 注解](https://docs.spring.io/spring-boot/reference/using/using-the-springbootapplication-annotation.html#using.using-the-springbootapplication-annotation) 在 Spring Boot 应用程序中是一个很方便的注解. 它会启用 Spring Boot 的 [自动配置](https://docs.spring.io/spring-boot/reference/using/auto-configuration.html#using.auto-configuration), [组件扫描](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/ComponentScan.html), 而且可以对 "应用程序类" 定义额外的配置.". 程序入口点 – main() : [main()](basic-syntax.html#program-entry-point) 函数是应用程序的入口点. : 它声明为在 `DemoApplication` 类之外的一个 [顶层函数](functions.html#function-scope). `main()` 函数调用 Spring 的 `runApplication(*args)` 函数, 使用 Spring Framework 来启动应用程序. 可变参数 – args: Array : 查看 `runApplication()` 函数的声明, 你会看到函数的参数标记了 [vararg 修饰符](functions.html#variable-number-of-arguments-varargs): `vararg args: String`. 这表示, 你可以向这个函数传递可变数量的字符串参数. 展开(spread)操作符 – (*args) : `args` 是 `main()` 函数的参数, 它声明为一个字符串数组. 由于存在的是字符串的数组, 而你想要将它的内容传递给函数, 请使用展开(spread)操作符 (在数组之前加上星号 `*`). ## 创建 Controller 应用程序已经可以运行了, 但我们先来更新它的逻辑. 在 Spring 应用程序中, Controller 用来处理 Web 请求. 在相同的包中, 在 `DemoApplication.kt` 文件的旁边, 创建 `MessageController.kt` 文件, 其中包含 `MessageController` 类, 如下: ```KOTLIN // MessageController.kt package com.example.demo import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController class MessageController { @GetMapping("/") fun index(@RequestParam("name") name: String) = "Hello, $name!" } ``` @RestController 注解 : 你需要告诉 Spring, `MessageController` 是一个 REST Controller, 因此你应该对它标注 `@RestController` 注解. : 这个注解表示这个类将会被组件扫描识别, 因为它和我们的 `DemoApplication` 类处在相同的包内. @GetMapping 注解 : `@GetMapping` 标注 REST Controller 的函数, 它实现了与 HTTP GET 调用对应的 endpoint: : ```KOTLIN @GetMapping("/") fun index(@RequestParam("name") name: String) = "Hello, $name!" ``` @RequestParam 注解 : 函数参数 `name` 标注了 `@RequestParam` 注解. 这个注解表示方法参数应该绑定到一个 Web 请求参数. : 因此, 如果你访问应用程序的根路径, 并提供一个请求名为 "name" 的参数, 例如 `/?name=`, 这个参数值将会被用做调用 `index()` 函数时的参数. 单表达式函数 – index() : 由于 `index()` 函数只包含一条语句, 你可以将它声明为一个 [单表达式函数](functions.html#single-expression-functions). : 意思就是说, 大括号可以省略, 函数体直接放在等号 `=` 之后. 函数返回值的类型推断 : `index()` 函数没有明确声明返回类型. 编译器会查看等号 `=` 右侧语句的结果, 以此推断返回类型. : `Hello, $name!` 表达式的类型是 `String`, 因此函数的返回类型也是 `String`. 字符串模板 – $name : `Hello, $name!` 表达式在 Kotlin 中称为 [字符串模板](strings.html#string-templates). : 字符串模板是字符串的字面值, 其中包含内嵌的表达式. : 对于字符串的拼接操作, 这是一个很方便的替代方法. ## 运行应用程序 Spring 应用程序现在可以运行了: 1. 在 `DemoApplication.kt` 文件中, 点击 `main()` 方法侧栏中的绿色 Run 图标: ![运行 Spring Boot 应用程序](images/run-spring-boot-application.png) Tip: 你也可以在终端窗口运行 `./gradlew bootRun` 命令. 这样会在你的计算机上启动本地服务器. 2. 应用程序启动后, 请打开以下 URL: ```TEXT http://localhost:8080?name=John ``` 你会看到输出的结果 "Hello, John!": ![Spring 应用程序的应答](images/spring-application-response.png) ## 下一步 本教程的下一部分中, 你将学习 Kotlin 数据类, 以及如何在你的应用程序中使用. [阅读下一章](jvm-spring-boot-add-data-class.html) # 向 Spring Boot 项目添加数据类 这是 Spring Boot 和 Kotlin 入门 教程的第 2 部分. 开始这一部分之前, 请确认你已经完成了前面的步骤: ![第 1 步](images/icon-1-done.svg) [使用 Kotlin 创建 Spring Boot 项目](jvm-create-project-with-spring-boot.html) ![第 2 步](images/icon-2.svg) 向 Spring Boot 项目添加数据类 ![第 3 步](images/icon-3-todo.svg) 为 Spring Boot 项目添加数据库支持 ![第 4 步](images/icon-4-todo.svg) 使用 Spring Data CrudRepository 进行数据库访问 在教程的这个部分, 你将会向应用程序添加更多功能, 并学会 Kotlin 语言的更多功能, 例如数据类. 我们需要修改 `MessageController` 类, 来返回 JSON 格式的应答, 其中包含一组序列化的对象. ## 更新你的应用程序 1. 在相同的包中, 在 `DemoApplication.kt` 文件旁边, 创建一个 `Message.kt` 文件 2. 在 `Message.kt` 文件中, 创建一个数据类, 包含 2 个属性: `id` 和 `text`: ```KOTLIN // Message.kt package com.example.demo data class Message(val id: String?, val text: String) ``` `Message` 类将被用来传递数据: 一组序列化后的 `Message` 对象将会组成 JSON 文档, Controller 会对浏览器请求返回这个 JSON 文档. 数据类 – Message : Kotlin 中的 [数据类](data-classes.html) 的主要目的是用来保存数据. 这样的类使用 `data` 关键字进行标记, 而且从类结构能够得到一些标准的功能和有用的函数. : 在上面的示例中, 你将 `Message` 声明为数据类, 因为它的主要目的是存储数据. val 和 var 属性 : [Kotlin 类中的属性](properties.html) 可以声明为: : * 可变属性, 使用 `var` 关键字 * 只读属性, 使用 `val` 关键字 : `Message` 类使用 `val` 关键字声明了 2 个属性, `id` 和 `text`. 编译器会为这些属性自动生成 get 函数. 在 `Message` 类的实例创建之后, 将无法对这些属性重新赋值. 可为 Null 的类型 – String? : Kotlin 提供了 [对可为 Null 的类型的内建支持](null-safety.html#nullable-types-and-non-nullable-types). 在 Kotlin 中, 类型系统会区分可以为 `null` 值的引用 (nullable references) 和不可以为 `null` 值的引用 (non-nullable references). 例如, 一个通常的 `String` 类型变量不能保存 `null` 值. 要允许使用 null 值, 你可以将一个变量声明为可为 null 的字符串, 写做 `String?`. : 这里, `Message` 类的 `id` 属性声明为可为 null 的类型. 因此, 可以对 `id` 传递 `null` 来创建一个 `Message` 类的实例: : ```KOTLIN Message(null, "Hello!") ``` 3. 在 `MessageController.kt` 文件中, 将 `index()` 函数改为 `listMessages()` 函数, 返回 `Message` 对象的 List: ```KOTLIN // MessageController.kt package com.example.demo import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/") class MessageController { @GetMapping fun listMessages() = listOf( Message("1", "Hello!"), Message("2", "Bonjour!"), Message("3", "Privet!"), ) } ``` 集合 – listOf() : Kotlin 标准库提供了基本的集合类型的实现: Set, List, 和 Map. 各个集合类型可以是 只读的, 或 可变的: : * 只读 集合包含访问集合元素的操作. * 可变 集合还包含写操作, 用于添加, 删除, 以及更新集合的元素. : Kotlin 标准库还提供了对应的工厂函数, 用来创建这些集合的实例. : 本教程中, 你使用了 [listOf()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/list-of.html) 函数来创建 `Message` 对象的 List. 这是用来创建对象的 只读 List 的工厂函数: 你不能向 List 添加或删除元素. 如果需要对 List 执行写操作, 请调用 [mutableListOf()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/mutable-list-of.html) 函数来创建一个可变的 List 实例. 尾随逗号(Trailing Comma) : [尾随逗号(Trailing Comma)](coding-conventions.html#trailing-commas) 是指在一系列元素的 最终元素 之后的逗号: : ```KOTLIN Message("3", "Privet!"), ``` : 这是 Kotlin 语法中的一个便利的功能, 而且它是可选的 – 不使用尾随逗号, 你的代码也能正常运行. : 在上面的示例中, 创建 `Message` 对象的 List 时, 在 `listOf()` 函数最后的参数之后包括了尾随逗号. `MessageController` 的应答现在是一个 JSON 文档, 其中包含 `Message` 对象的集合. Note: 如果 Jackson 库存在于类路径中, 那么 Spring 应用程序中的所有 Controller 都会默认输出 JSON 格式的应答. 由于你 [在 build.gradle.kts 文件中指定了 spring-boot-starter-webmvc 依赖项](jvm-create-project-with-spring-boot.html#explore-the-project-gradle-build-file), 你会通过 传递(transitive) 依赖项的方式得到 Jackson. 因此, 如果 endpoint 返回一个能够被序列化为 JSON 的数据结构, 应用程序就会应答一个 JSON 文档. 下面是 `DemoApplication.kt`, `MessageController.kt`, 以及 `Message.kt` 文件的完整代码: ```KOTLIN // DemoApplication.kt package com.example.demo import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication class DemoApplication fun main(args: Array) { runApplication(*args) } ``` ```KOTLIN // MessageController.kt package com.example.demo import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/") class MessageController { @GetMapping fun listMessages() = listOf( Message("1", "Hello!"), Message("2", "Bonjour!"), Message("3", "Privet!"), ) } ``` ```KOTLIN // Message.kt package com.example.demo data class Message(val id: String?, val text: String) ``` ## 运行应用程序 Spring 应用程序已经可以运行了: 1. 再次运行应用程序. 2. 应用程序启动后, 打开以下 URL: ```TEXT http://localhost:8080 ``` 你将会看到一个页面, 包含 JSON 格式的 message 集合: ![运行应用程序](images/messages-in-json-format.png) ## 下一步 本教程的下一部分中, 你将会向你的项目添加并配置一个数据库, 并发送 HTTP 请求. [阅读下一章](jvm-spring-boot-add-db-support.html) # 为 Spring Boot 项目添加数据库支持 这是 Spring Boot 和 Kotlin 入门 教程的第 3 部分. 开始这一部分之前, 请确认你已经完成了前面的步骤: ![第 1 步](images/icon-1-done.svg) [使用 Kotlin 创建 Spring Boot 项目](jvm-create-project-with-spring-boot.html) ![第 2 步](images/icon-2-done.svg) [向 Spring Boot 项目添加数据类](jvm-spring-boot-add-data-class.html) ![第 3 步](images/icon-3.svg) 为 Spring Boot 项目添加数据库支持 ![第 4 步](images/icon-4-todo.svg) 使用 Spring Data CrudRepository 进行数据库访问 在教程的这个部分, 你将会使用 Java 数据库连接 (Java Database Connectivity, JDBC) 向你的项目添加并配置一个数据库. 在 JVM 应用程序中, 你要使用 JDBC 来操作数据库. 为了方便, Spring Framework 提供了 `JdbcTemplate` 类, 简化 JDBC 的使用, 并帮助避免常见的错误. ## 添加数据库支持 在使用 Spring Framework 的应用程序中, 通常的做法是在所谓的 服务(Service) 层实现数据库访问逻辑 – 这是实现业务逻辑的地方. 在 Spring 中, 你需要使用 `@Service` 注解来标注类, 表示类属于应用程序的服务层. 在这个应用程序中, 你将会创建 `MessageService` 类来实现这个目的. 在相同的包中, 创建 `MessageService.kt` 文件, 其中包含 `MessageService` 类, 如下: ```KOTLIN // MessageService.kt package com.example.demo import org.springframework.stereotype.Service import org.springframework.jdbc.core.JdbcTemplate @Service class MessageService(private val db: JdbcTemplate) { fun findMessages(): List = db.query("select * from messages") { response, _ -> Message(response.getString("id"), response.getString("text")) } fun save(message: Message): Message { db.update( "insert into messages values ( ?, ? )", message.id, message.text ) return message } } ``` 构造器参数与依赖注入 – (private val db: JdbcTemplate) : Kotlin 中的类有一个主构造器. 还可以有一个或多个 [次级构造器](classes.html#secondary-constructors). 主构造器 是类头部的一部分, 位于类名称以及可选的类型参数之后. 在我们的例子中, 构造器是 `(val db: JdbcTemplate)`. : `val db: JdbcTemplate` 是构造器的参数: : ```KOTLIN @Service class MessageService(private val db: JdbcTemplate) ``` 尾缀 Lambda 表达式(Trailing Lambda) 与 SAM 转换 : `findMessages()` 函数调用 `JdbcTemplate` 类的 `query()` 函数. `query()` 函数接受 2 个参数: 一个 SQL 查询, 类型为字符串, 以及一个回调, 将每一行查询结果转换为对象: : ```SQL db.query("...", RowMapper { ... } ) ``` : `RowMapper` 接口只声明了一个方法, 因此可以使用 Lambda 表达式来实现它, 省略接口名称. Kotlin 编译器知道表达式需要转换成的接口, 因为你将它用作函数调用的一个参数. 这个功能称为 [Kotlin 中的SAM 转换](java-interop.html#sam-conversions): : ```SQL db.query("...", { ... } ) ``` : 在 SAM 转换之后, query 函数得到 2 个参数: 首先是一个 String, 后面是一个 Lambda 表达式. 根据 Kotlin 的习惯, 如果一个函数的最后一个参数是一个函数, 那么传递给这个参数的 Lambda 表达式可以放在括号之外. 这样的语法称为 [尾缀 Lambda 表达式(Trailing Lambda)](lambdas.html#passing-trailing-lambdas): : ```SQL db.query("...") { ... } ``` 对未使用的 Lambda 表达式参数使用下划线 : 对于带有多个参数的 Lambda 表达式, 你可以使用下划线 `_` 符号来代替你不需要使用的参数的名称. : 因此, query 函数调用的最终语法如下: : ```KOTLIN db.query("select * from messages") { response, _ -> Message(response.getString("id"), response.getString("text")) } ``` ## 更新 MessageController 类 更新 `MessageController.kt`, 使用新的 `MessageService` 类: ```KOTLIN // MessageController.kt package com.example.demo import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import java.net.URI @RestController @RequestMapping("/") class MessageController(private val service: MessageService) { @GetMapping fun listMessages() = service.findMessages() @PostMapping fun post(@RequestBody message: Message): ResponseEntity { val savedMessage = service.save(message) return ResponseEntity.created(URI("/${savedMessage.id}")).body(savedMessage) } } ``` @PostMapping 注解 : 负责处理 HTTP POST 请求的方法需要标注 `@PostMapping` 注解. 为了将 HTTP 请求 Body 部的 JSON 内容转换为对象, 你需要对方法参数使用 `@RequestBody` 注解. 由于 Jackson 库存在于应用程序的类路径中, 这个转换能够自动完成. ResponseEntity : `ResponseEntity` 表示完整的 HTTP 应答: Status Code, Header, 以及 Body. : 使用 `created()` 方法, 你可以配置应答的 Status Code (201), 并设置 "Location" Header, 表示新创建的资源的上下文路径(context path). ## 更新 MessageService 类 `Message` 类的 `id` 声明为可为 null 的字符串: ```KOTLIN data class Message(val id: String?, val text: String) ``` 但是, 将 `null` 作为 `id` 值保存到数据库是不正确的: 你需要恰当的处理这样的情况. 更新你的 `MessageService.kt` 文件中的代码, 在将 message 保存到时数据库, 如果 `id` 为 `null`, 生成新的值: ```KOTLIN // MessageService.kt package com.example.demo import org.springframework.stereotype.Service import org.springframework.jdbc.core.JdbcTemplate import java.util.UUID @Service class MessageService(private val db: JdbcTemplate) { fun findMessages(): List = db.query("select * from messages") { response, _ -> Message(response.getString("id"), response.getString("text")) } fun save(message: Message): Message { val id = message.id ?: UUID.randomUUID().toString() // 如果 id 为 null, 生成新的 id 值 db.update( "insert into messages values ( ?, ? )", id, message.text ) return message.copy(id = id) // 返回 message 的 copy, 使用新的 id 值 } } ``` Elvis 操作符 – ?: : 代码 `message.id ?: UUID.randomUUID().toString()` 使用了 [Elvis 操作符 (if-not-null-else 的缩写) ?:](null-safety.html#elvis-operator). 如果 `?:` 左侧的表达式不是 `null`, Elvis 操作符会返回这个表达式的值; 否则, 它返回右侧表达式的值. 注意, 右侧表达式只有在左侧表达式为 `null` 的情况下才会计算. 应用程序代码已经可以访问数据库了. 现在需要配置数据源. ## 配置数据库 在应用程序中配置数据库: 1. 在 `src/main/resources` 目录中创建 `schema.sql` 文件. 它将会保存数据库对象的定义: ![创建数据库 Schema](images/create-database-schema.png) 2. 更新 `src/main/resources/schema.sql` 文件, 内容如下: ```SQL -- schema.sql CREATE TABLE IF NOT EXISTS messages ( id VARCHAR(60) PRIMARY KEY, text VARCHAR NOT NULL ); ``` 它创建 `messages` 表, 包含 2 个列: `id` 和 `text`. 表结构与 `Message` 类一致. 3. 打开 `src/main/resources` 文件夹内的 `application.properties` 文件, 添加以下应用程序属性: ``` spring.application.name=demo spring.datasource.driver-class-name=org.h2.Driver spring.datasource.url=jdbc:h2:file:./data/testdb spring.datasource.username=name spring.datasource.password=password spring.sql.init.schema-locations=classpath:schema.sql spring.sql.init.mode=always ``` 这些设置会为 Spring Boot 应用程序启用数据库. 关于完整的应用程序属性列表, 请参见 [Spring 文档](https://docs.spring.io/spring-boot/appendix/application-properties/index.html). ## 通过 HTTP 请求, 向数据库添加 message 你应该使用一个 HTTP 客户端来访问前面创建的 Endpoint. 在 IntelliJ IDEA 中, 请使用内嵌的 HTTP Client: 1. 运行应用程序. 应用程序启动之后, 你可以执行 POST 请求来向数据库存储消息. 2. 在项目的根文件夹中创建 `requests.http` 文件, 并添加以下 HTTP 请求: ```HTTP_REQUEST_FULL ### Post "Hello!" POST http://localhost:8080/ Content-Type: application/json { "text": "Hello!" } ### Post "Bonjour!" POST http://localhost:8080/ Content-Type: application/json { "text": "Bonjour!" } ### Post "Privet!" POST http://localhost:8080/ Content-Type: application/json { "text": "Privet!" } ### 得到所有的 message GET http://localhost:8080/ ``` 3. 执行所有的 POST 请求. 使用请求声明侧栏中的绿色 Run 图标. 这些请求会将消息写入到数据库: ![执行 POST 请求](images/execute-post-requests.png) 4. 执行 GET 请求, 并在 Run 工具窗口查看结果: ![执行 GET 请求](images/execute-get-requests.png) ### 执行请求的其它方式 你也可以使用任何其它的 HTTP Client, 或 cURL 命令行工具. 比如, 在终端中运行以下命令, 得到同样的结果: ```BASH curl -X POST --location "http://localhost:8080" -H "Content-Type: application/json" -d "{ \"text\": \"Hello!\" }" curl -X POST --location "http://localhost:8080" -H "Content-Type: application/json" -d "{ \"text\": \"Bonjour!\" }" curl -X POST --location "http://localhost:8080" -H "Content-Type: application/json" -d "{ \"text\": \"Privet!\" }" curl -X GET --location "http://localhost:8080" ``` ## 通过 id 获取 message 为应用程序增加新的功能, 通过 id 来获取单个的 message. 1. 在 `MessageService` 类中, 添加新的函数 `findMessageById(id: String)`, 通过 id 来获取单个的 message: ```KOTLIN // MessageService.kt package com.example.demo import org.springframework.stereotype.Service import org.springframework.jdbc.core.JdbcTemplate import org.springframework.jdbc.core.query import java.util.* @Service class MessageService(private val db: JdbcTemplate) { fun findMessages(): List = db.query("select * from messages") { response, _ -> Message(response.getString("id"), response.getString("text")) } fun findMessageById(id: String): Message? = db.query("select * from messages where id = ?", id) { response, _ -> Message(response.getString("id"), response.getString("text")) }.singleOrNull() fun save(message: Message): Message { val id = message.id ?: UUID.randomUUID().toString() // 如果 id 为 null, 生成新的 id 值 db.update( "insert into messages values ( ?, ? )", id, message.text ) return message.copy(id = id) // 返回 message 的 copy, 使用新的 id 值 } } ``` vararg 参数在参数列表中的位置 : `query()` 函数接受 3 个参数: : * SQL 查询字符串, 它执行时需要一个参数 * `id`, 类型为字符串的参数 * `RowMapper` 实例, 由 Lambda 表达式实现 : `query()` 函数的第 2 个参数声明为 不定数量参数 (`vararg`). 在 Kotlin 中, 不定数量参数的位置并不要求是在参数列表的最后. singleOrNull() 函数 : [singleOrNull()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/single-or-null.html) 函数返回单个元素, 如果数组为空, 或存在相同值的多个元素, 则返回 `null`. Warning: 通过 id 来获取 message 的 `.query()` 函数是由 Spring Framework 提供的一个 [Kotlin 扩展函数](extensions.html#extension-functions), 如上面的代码所示, 它需要一个额外的 `import org.springframework.jdbc.core.query` 语句. 2. 向 `MessageController` 类添加新的 `index(...)` 函数, 参数是 `id`: ```KOTLIN // MessageController.kt package com.example.demo import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import java.net.URI @RestController @RequestMapping("/") class MessageController(private val service: MessageService) { @GetMapping fun listMessages() = ResponseEntity.ok(service.findMessages()) @PostMapping fun post(@RequestBody message: Message): ResponseEntity { val savedMessage = service.save(message) return ResponseEntity.created(URI("/${savedMessage.id}")).body(savedMessage) } @GetMapping("/{id}") fun getMessage(@PathVariable id: String): ResponseEntity = service.findMessageById(id).toResponseEntity() private fun Message?.toResponseEntity(): ResponseEntity = // 如果 message 为 null (未找到), 将应答的 Status Code 设置为 404 this?.let { ResponseEntity.ok(it) } ?: ResponseEntity.notFound().build() } ``` 从 context 路径得到值 : Spring Framework 会从 context 路径得到 message 的 `id` 值, 因为你对新函数标注了 `@GetMapping("/{id}")` 注解. 通过对函数参数标注 `@PathVariable` 注解, 你告诉 Spring Framework 使用得到的值作为函数参数. 新函数会调用 `MessageService` 来通过 id 取得单个 message. 接受者可为 null 的扩展函数 : 扩展函数可以使用可为 null 的接受者类型. 如果接受者为 `null`, 那么 `this` 也是 `null`. 因此在定义接受者可为 null 的扩展函数时, 建议在函数的 body 部之内执行 `this == null` 检查. : 你也可以使用 null 值安全的调用操作符 (`?.`) 来进行 null 值检查, 就像上面的 `toResponseEntity()` 函数那样: : ```KOTLIN this?.let { ResponseEntity.ok(it) } ``` ResponseEntity : `ResponseEntity` 表示 HTTP 应答, 包含 Status Code, Header, 以及 Body. 它是一个通用的封装, 你可以用它向客户端发送自定义的 HTTP 应答, 对应答内容进行更好的控制. 下面是应用程序的完整代码: ```KOTLIN // DemoApplication.kt package com.example.demo import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication class DemoApplication fun main(args: Array) { runApplication(*args) } ``` ```KOTLIN // Message.kt package com.example.demo data class Message(val id: String?, val text: String) ``` ```KOTLIN // MessageService.kt package com.example.demo import org.springframework.stereotype.Service import org.springframework.jdbc.core.JdbcTemplate import org.springframework.jdbc.core.query import java.util.* @Service class MessageService(private val db: JdbcTemplate) { fun findMessages(): List = db.query("select * from messages") { response, _ -> Message(response.getString("id"), response.getString("text")) } fun findMessageById(id: String): Message? = db.query("select * from messages where id = ?", id) { response, _ -> Message(response.getString("id"), response.getString("text")) }.singleOrNull() fun save(message: Message): Message { val id = message.id ?: UUID.randomUUID().toString() db.update( "insert into messages values ( ?, ? )", id, message.text ) return message.copy(id = id) } } ``` ```KOTLIN // MessageController.kt package com.example.demo import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import java.net.URI @RestController @RequestMapping("/") class MessageController(private val service: MessageService) { @GetMapping fun listMessages() = ResponseEntity.ok(service.findMessages()) @PostMapping fun post(@RequestBody message: Message): ResponseEntity { val savedMessage = service.save(message) return ResponseEntity.created(URI("/${savedMessage.id}")).body(savedMessage) } @GetMapping("/{id}") fun getMessage(@PathVariable id: String): ResponseEntity = service.findMessageById(id).toResponseEntity() private fun Message?.toResponseEntity(): ResponseEntity = this?.let { ResponseEntity.ok(it) } ?: ResponseEntity.notFound().build() } ``` ## 运行应用程序 Spring 应用程序已经可以运行了: 1. 再次运行应用程序. 2. 打开 `requests.http` 文件, 添加新的 GET 请求: ```HTTP_REQUEST_FULL ### 根据 id 得到 message GET http://localhost:8080/id ``` 3. 执行 GET 请求, 从数据库得到所有的 message. 4. 在 Run 工具窗口, 复制某个 message 的 id, 并添加到请求中, 例如: ```HTTP_REQUEST_FULL ### 根据 id 得到 message GET http://localhost:8080/f910aa7e-11ee-4215-93ed-1aeeac822707 ``` Note: 请使用你的 message 的真实 id, 不要使用上面例子中的值. 5. 执行 GET 请求, 并在 Run 工具窗口中查看结果: ![根据 id 得到 message](images/retrieve-message-by-its-id.png) ## 下一步 本教程的最后部分会向你演示, 如何使用更加流行的数据库操作方式 Spring Data. [阅读下一章](jvm-spring-boot-using-crudrepository.html) # 使用 Spring Data CrudRepository 进行数据库访问 这是 Spring Boot 和 Kotlin 入门 教程的最后部分. 开始这一部分之前, 请确认你已经完成了前面的步骤: ![第 1 步](images/icon-1-done.svg) [使用 Kotlin 创建 Spring Boot 项目](jvm-create-project-with-spring-boot.html) ![第 2 步](images/icon-2-done.svg) [向 Spring Boot 项目添加数据类](jvm-spring-boot-add-data-class.html) ![第 3 步](images/icon-3-done.svg) [为 Spring Boot 项目添加数据库支持](jvm-spring-boot-add-db-support.html) ![第 4 步](images/icon-4.svg) 使用 Spring Data CrudRepository 进行数据库访问 在这一章中, 你将会迁移服务层, 使用 [Spring Data](https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/CrudRepository.html) `CrudRepository` 进行数据库访问, 而不是原来的 `JdbcTemplate` . CrudRepository 是一个 Spring Data 接口, 可以指定类型的仓库进行通常的 [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) 操作. 它提供了一些现成的方法来操作数据库. ## 更新你的应用程序 首先, 你需要调整 `Message` 类, 来配合 `CrudRepository` API: 1. 向 `Message` 类添加 `@Table` 注解, 声明它与数据库表的映射关系. 在 `id` 属性之前添加 `@Id` 注解. Note: 这些注解也需要额外的 import. ```KOTLIN // Message.kt package com.example.demo import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table @Table("MESSAGES") data class Message(@Id val id: String?, val text: String) ``` 此外, 为了让 `Message` 类的使用更加符合 Kotlin 的编程习惯, 你可以将 `id` 属性的默认值设置为 null, 并翻转数据类的属性顺序: ```KOTLIN @Table("MESSAGES") data class Message(val text: String, @Id val id: String? = null) ``` 现在, 如果你需要创建的 `Message` 类的新实例, 你可以在参数中只指定 `text` 属性: ```KOTLIN val message = Message("Hello") // id 为 null ``` 2. 为 `CrudRepository` 声明一个接口, 它负责操作 `Message` 数据类. 创建 `MessageRepository.kt` 文件, 添加以下代码: ```KOTLIN // MessageRepository.kt package com.example.demo import org.springframework.data.repository.CrudRepository interface MessageRepository : CrudRepository ``` 3. 更新 `MessageService` 类. 它现在使用 `MessageRepository`, 而不是执行 SQL 查询: ```KOTLIN // MessageService.kt package com.example.demo import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service @Service class MessageService(private val db: MessageRepository) { fun findMessages(): List = db.findAll().toList() fun findMessageById(id: String): Message? = db.findByIdOrNull(id) fun save(message: Message): Message = db.save(message) } ``` 扩展函数 : `findByIdOrNull()` 函数是 Spring Data JDBC 中的 `CrudRepository` 接口的一个 [扩展函数](extensions.html#extension-functions). : 在上面的代码中, `Optional.toList()`, `.toList()` 是 `Optional` 的扩展函数. 使用扩展函数, 你可以向任何类添加额外的函数, 当你想要扩展某些库中的类的功能时, 这样会非常有用. CrudRepository save() 函数 : [这个函数的工作方式](https://docs.spring.io/spring-data/relational/reference/#jdbc.entity-persistence) 是假定新的对象在数据库中没有 id. 因此, 对 insertion 操作, id 需要为 null. : 如果 id 不是 null, `CrudRepository` 假定对象在数据库中已经存在, 并且这是一个 update 操作, 而不是 insert 操作. 在 insert 操作之后, `id` 会由数据库生成, 并反过来赋值给 `Message` 实例. 4. 更新 messages 表定义, 对 insert 的对象生成 id. 由于 `id` 是一个字符串, 你可以使用 `RANDOM_UUID()` 函数来生成默认的 id 值: ```SQL -- schema.sql CREATE TABLE IF NOT EXISTS messages ( id VARCHAR(60) DEFAULT RANDOM_UUID() PRIMARY KEY, text VARCHAR NOT NULL ); ``` 5. 更新 `src/main/resources` 文件夹中的 `application.properties` 文件内的数据库名称: ``` spring.application.name=demo spring.datasource.driver-class-name=org.h2.Driver spring.datasource.url=jdbc:h2:file:./data/testdb2 spring.datasource.username=name spring.datasource.password=password spring.sql.init.schema-locations=classpath:schema.sql spring.sql.init.mode=always ``` 下面是应用程序的完整代码: ```KOTLIN // DemoApplication.kt package com.example.demo import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication class DemoApplication fun main(args: Array) { runApplication(*args) } ``` ```KOTLIN // Message.kt package com.example.demo import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table @Table("MESSAGES") data class Message(val text: String, @Id val id: String? = null) ``` ```KOTLIN // MessageRepository.kt package com.example.demo import org.springframework.data.repository.CrudRepository interface MessageRepository : CrudRepository ``` ```KOTLIN // MessageService.kt package com.example.demo import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service @Service class MessageService(private val db: MessageRepository) { fun findMessages(): List = db.findAll().toList() fun findMessageById(id: String): Message? = db.findByIdOrNull(id) fun save(message: Message): Message = db.save(message) } ``` ```KOTLIN // MessageController.kt package com.example.demo import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import java.net.URI @RestController @RequestMapping("/") class MessageController(private val service: MessageService) { @GetMapping fun listMessages() = ResponseEntity.ok(service.findMessages()) @PostMapping fun post(@RequestBody message: Message): ResponseEntity { val savedMessage = service.save(message) return ResponseEntity.created(URI("/${savedMessage.id}")).body(savedMessage) } @GetMapping("/{id}") fun getMessage(@PathVariable id: String): ResponseEntity = service.findMessageById(id).toResponseEntity() private fun Message?.toResponseEntity(): ResponseEntity = // 如果 message 为 null (未找到), 将应答的 Status Code 设置为 404 this?.let { ResponseEntity.ok(it) } ?: ResponseEntity.notFound().build() } ``` ## 运行应用程序 恭喜! 应用程序可以再次运行了. 在将 `JdbcTemplate` 替换为 `CrudRepository` 之后, 功能没有改变, 因此应用程序会和以前一样运行. 现在你可以从 `requests.http` 文件 [运行 POST 和 GET HTTP 请求](jvm-spring-boot-add-db-support.html#add-messages-to-database-via-http-request), 并得到相同的结果. ## 下一步做什么 得到你个人的语言导航地图, 它可以帮助你浏览 Kotlin 的功能特性, 并追踪你学习语言的进度: [https://resources.jetbrains.com/storage/products/kotlin/docs/Kotlin_Language_Features_Map.pdf](https://resources.jetbrains.com/storage/products/kotlin/docs/Kotlin_Language_Features_Map.pdf) * 阅读 [Spring Framework](https://docs.spring.io/spring-framework/reference/) 文档. * 学习教程 [Securing a web application](https://spring.io/guides/gs/securing-web), 创建一个带有受保护资源的简单 Web 应用程序. * 完成教程 [使用 Spring Boot 和 Kotlin 创建 Web 应用程序](https://spring.io/guides/tutorials/spring-boot-kotlin). # 使用 Spring Boot 和 Claude 创建任务管理应用程序 在本教程中, 你将学习如何使用 [Claude](https://claude.com/product/overview) 创建一个用于管理任务的 Kotlin 应用程序. 本教程使用 Spring Boot 管理后端基础设施, 同时由 Claude 规划和开发应用程序. 如果你希望不借助 AI 的帮助来创建应用程序, 可以参考我们的 [使用 Kotlin 和 Spring Boot 创建 Web 应用程序](jvm-get-started-spring-boot.html) 教程. Note: 与任何 AI 工具一样, Claude 也可能会出错. 请仔细审查 Claude 的修改, 并只对你信任的代码使用它. 关于 Claude 安全策略, 详情请参见 [Claude Code 文档](https://code.claude.com/docs/en/security). ## 设置环境 Tip: 本教程通过 JetBrains AI Assistant 使用 Claude, 但你也可以在终端中通过 Claude Code 来完成教程的各个步骤. 1. 下载并安装最新版本的 [IntelliJ IDEA](https://www.jetbrains.com/idea/download/). 2. 安装 [JetBrains AI Assistant](https://plugins.jetbrains.com/plugin/22282-jetbrains-ai-assistant). 3. 通过以下任何一种方式, 激活 Claude Agent: * [使用 JetBrains AI 订阅](https://www.jetbrains.com/help/ai-assistant/activate-agents.html#activate-claude-agent-with-jbai-subscription) * [使用 API Key](https://www.jetbrains.com/help/ai-assistant/activate-agents.html#activate-claude-agent-with-api-key) * [使用 Anthropic Console](https://www.jetbrains.com/help/ai-assistant/activate-agents.html#activate-agent-with-provider-specific-method) ## 创建项目 Tip: 你也可以使用 [Spring 的基于 Web 的项目生成器](https://start.spring.io/#!language=kotlin&type=gradle-project-kotlin) 创建 Spring Boot 项目. 在 IntelliJ IDEA 中创建一个新的 Spring Boot 项目: 1. 在 IntelliJ IDEA 中, 选择 File | New | Project. 2. 在左侧面板中, 选择 New Project | Spring Boot. 3. 在 New Project 窗口中, 指定以下项目和选项: * Name: task-manager-demo * Language: Kotlin * Type: Gradle - Kotlin Tip: 这个选项指定构建系统和 DSL. * Package name: org.jetbrains.kotlin.taskmanagerdemo * JDK: jbr-21 * Java: 17 Tip: 如果你没有安装这些 Java 和 JDK 版本, 可以从下拉列表中下载. ![创建 Spring Boot 项目](images/create-spring-claude-project.png) 4. 确认所有项目都已填写, 然后点击 Next. 5. 在 Spring Boot 项目中, 选择最新稳定版本的 Spring Boot. 6. 选择 Web | Spring Web 依赖项. ![设置 Spring Boot 项目](images/spring-claude-dependency.png) 7. 点击 Create, 生成并设置项目. IDE 会生成并打开新项目. 下载和导入项目依赖项可能需要一些时间. ## 创建开发计划 在你的项目中: 1. 打开 ![AI Chat](images/toolWindowChat%4020x20.svg) AI Chat 工具窗口. 默认情况下, 会选中 Chat 模式. 请选择 Claude Agent. ![选择 Claude Agent](images/select-claude-agent.png) 2. 点击 Mode: Default ![操作模式](images/app-client.expui.general.chevronDownLarge.svg), 然后选择 Mode: Plan Mode. Claude Agent 现在准备好进行规划而不执行操作了. ![选择 Plan Mode](images/claude-plan-mode.png) Tip: 关于各种操作模式, 详情请参见 [选择操作模式](https://www.jetbrains.com/help/ai-assistant/claude-agent.html#select-operation-mode). 3. 编写一个提示词(prompt), 要求 Claude 创建一个任务管理应用程序. 提供一些你认为应该包含的内容的详细信息. 例如: ```TEXT 我想创建一个任务管理应用程序, 用于管理任务, 例如购物清单. 它应该有一个基本的 UI, 包含类别, 截止日期, 优先级, 以及状态跟踪. 在工作时使用 VCS. 逐步工作, 并在每个阶段创建提交记录, 以便我之后可以审查这些变更. ``` Tip: 关于如何设计提示词的指导, 请参见 [Claude Code 最佳实践](https://code.claude.com/docs/en/best-practices). Claude 会探索现有的项目结构, 并提出一个计划. 4. 在继续之前, 仔细审查计划. 如果你想做一些修改, 请选择 No, keep planning, 并提供你的后续意见. 5. 当你准备好继续时, 在各个 Yes ... 选项中, 选择符合你希望对 Claude 变更进行多大程度控制的那一个. ![可以开始编码](images/ready-to-code.png) Tip: 关于不同的选项, 详情请参见 [Claude Code 权限模式](https://code.claude.com/docs/en/best-practices). 6. Claude 退出 Plan Mode, 并开始工作. 等待工作完成. ## 审查提交记录 在运行应用程序之前, 请仔细审查生成的变更: 1. 打开 Git 工具窗口, 查看提交记录列表. 2. 选择一个提交记录, 并双击每个已修改的文件, 在 IntelliJ IDEA 的并排视图中查看差异. ![并排视图](images/side-by-side-viewer.png) ## 运行应用程序 如果对变更感到满意, 请运行应用程序: 1. 运行 `bootRun` Gradle 任务, 或在终端中输入以下命令: ```BASH ./gradlew bootRun ``` 2. 在浏览器中, 打开 localhost URL. 默认地址通常为: ```TEXT http://localhost:8080 ``` 现在你应该能看到 Claude 创建的基本 UI. ![运行应用程序](images/run-spring-claude-app.png) Tip: 由于是 Claude 负责设计 UI, 你的 UI 可能与本教程中的版本有所不同. ## 测试应用程序 现在应该测试应用程序了. ### 手动测试 UI 首先测试 UI 的功能. 尝试一些简单的操作: 1. 创建一个任务, 并测试表单字段. 2. 编辑一个任务, 检查更改是否被保存. 3. 更改任务的状态. 4. 删除一个任务. 5. 更改任务的类别. 如果以上任何操作不能正常工作, 请向 Claude 发送新的提示词, 要求它调查并修复问题. ### 运行单元测试 Claude 还会自动创建一些测试. 运行以下命令, 检查所有测试是否通过: ```BASH ./gradlew test ``` 或者, 在 `src/test` 目录中打开一个测试, 然后点击边栏中的运行图标 ![运行图标](images/app-client.expui.run.run.svg). 测试成功会显示 ![运行成功图标](images/app-client.expui.gutter.runSuccess.svg). 如果有任何测试不通过, 请向 Claude 发送新的提示词, 让它调查并修复问题. ## 优化改进 初始任务完成后, 你可以进行优化改进. 例如, 让我们改进 UI, 使用户能够直接在列表中编辑任务. 你可以发送类似这样的提示词: ```TEXT 下一步, 允许用户在列表中直接编辑任务. 例如, 让用户点击任务标题, 直接在列表中编辑它, 并且不必离开当前视图就能更新字段, 例如优先级, 截止日期, 或状态. 这个改变应该让应用程序感觉更快速, 使用起来更直观. ``` 与之前一样, Claude 会探索现有的项目结构, 并提出一个计划. 接受计划后, 等待 Claude 完成工作, 审查变更, 然后再次运行应用程序. ![使用 Claude 优化改进你的 Spring Boot 应用程序](images/make-refinements-claude.gif) 恭喜! 你已经使用 Claude, 直接在 IntelliJ IDEA 中规划, 构建, 测试, 并改进了一个 Kotlin Spring Boot 应用程序. ## 下一步做什么? * 了解 [Kotlin AI 技能(Skill)](kotlin-ai-skills.html) * 查看 [结合 Kotlin AI 技能使用 Junie](https://kotlinlang.org/docs/multiplatform/multiplatform-cocoapods-spm-migration-ai.html) 教程 # 概述 Kotlin 通过 Kotlin Multiplatform 提供了两种 Web 开发方案: * [基于 JavaScript (使用 Kotlin/JS 编译器)](#kotlin-js) * [基于 WebAssembly (使用 Kotlin/Wasm 编译器)](#kotlin-wasm) 两种方案都允许你在 Web 应用中共用代码, 但它们支持不同的使用场景. 它们在技术层面上也有所不同, 例如目标浏览器的支持情况. ## Kotlin/JS [Kotlin/JS](js-overview.html) 通过将你的代码, 标准库, 以及所有支持的依赖项转译为 JS, 使 Kotlin 应用程序能够在 JavaScript (JS) 环境中运行. 使用 Kotlin/JS 进行开发时, 你可以在浏览器或 Node.js 环境中运行你的应用程序. Tip: 关于配置 Kotlin/JS 目标平台, 请参见 [配置 Gradle 项目](gradle-configure-project.html#targeting-javascript) 向导. ### Kotlin/JS 的使用场景 Kotlin/JS 非常适合以下情况: * [与 JavaScript/TypeScript 代码库共用业务逻辑](#share-business-logic-with-a-javascript-typescript-codebase). * [使用 Kotlin 构建不需要共用代码的 Web 应用程序](#build-web-apps-with-kotlin-without-sharing-the-code). #### 与 JavaScript/TypeScript 代码库共用业务逻辑 如果你需要将 Kotlin 代码(例如领域逻辑或数据逻辑)与原生的 JavaScript/TypeScript 应用程序共用, Kotlin/JS 目标平台提供以下功能: * 与 JavaScript/TypeScript 的直接互操作性. * 最小的互操作开销(例如, 避免不必要的数据复制). 这使得共用代码能够平滑的集成到基于 JS 的工作流程中. #### 使用 Kotlin 构建 Web 应用程序, 不共用代码 对于 Web 应用程序完全由 Kotlin 实现, 而不需要与其他平台(iOS, Android 或 Desktop)共用代码的项目, 基于 HTML 的解决方案提供了更好的控制能力. 基于 HTML 的解决方案改善了 SEO 和可访问性. 它们还提供了更好的浏览器集成, 包括页面内搜索和页面翻译等功能. 对于基于 HTML 的解决方案, Kotlin/JS 支持多种方案: * 使用基于 Compose 的 HTML 框架, 例如 [Kobweb](https://kobweb.varabyte.com/) 或 [Kilua](https://kilua.dev/), 以 Compose 风格的架构构建 UI. * 使用带有 Kotlin 包装器的基于 React 的解决方案, 实现 [Kotlin 中的 React 组件](js-react.html). ## Kotlin/Wasm [Kotlin/Wasm](wasm-overview.html) 将 Kotlin 代码编译为 WebAssembly (Wasm), 使应用程序能够在支持 Wasm 并且满足 Kotlin 要求的环境和设备上运行. 在浏览器中, Kotlin/Wasm 让你能够使用 [Compose Multiplatform](https://kotlinlang.org/compose-multiplatform/) 构建 Web 应用程序. 在浏览器之外, 它在独立的 Wasm 虚拟机中运行, 使用 [WebAssembly System Interface (WASI)](https://wasi.dev/) 来访问平台 API. 使用 Kotlin/Wasm 进行开发时, 你可以使用以下目标平台: * `wasmJs`: 用于在浏览器或 Node.js 中运行. * `wasmWasi`: 用于在支持 WASI 的 Wasm 环境中运行, 例如 Wasmtime, WasmEdge 等. Tip: 关于配置 Kotlin/Wasm 目标平台, 请参见 [配置 Gradle 项目](gradle-configure-project.html#targeting-webassembly) 向导. ### Kotlin/Wasm 的使用场景 如果你希望在多个平台之间共用逻辑和 UI, 请使用 Kotlin/Wasm. #### 使用 Compose Multiplatform 构建跨平台应用程序 如果你希望在多个平台(包括 Web)之间共用逻辑和 UI, Kotlin/Wasm 配合 [Compose Multiplatform](https://kotlinlang.org/compose-multiplatform/) 提供了共用的 UI 层: * 确保所有平台的 UI 实现保持一致. * 使用 Wasm 提升渲染性能, 实现更流畅的 UI 更新, 例如响应式动画. * 支持最新版本的 [WebAssembly Garbage Collection (WasmGC)](https://developer.chrome.com/blog/wasmgc) 提案, 使 Kotlin/Wasm 能够在所有主流现代浏览器上运行. ## 选择你的 Web 方案 根据你的使用场景, 下表总结了推荐的目标平台: | 使用场景 |推荐的目标平台 |说明 | --------------------- | 共用业务逻辑, 但使用 Web 原生 UI |Kotlin/JS |提供与 JS 的直接互操作性, 以及最小的开销. | | 同时共用 UI 和业务逻辑 |Kotlin/Wasm |使用 [Compose Multiplatform](https://kotlinlang.org/compose-multiplatform/) 提供更好的渲染性能. | | 不需要共用的 UI |Kotlin/JS |允许使用基于 HTML 的框架(如 [Kobweb](https://kobweb.varabyte.com/), [Kilua](https://kilua.dev/), 或 [React](js-react.html)) 构建 UI, 使用现有的 JS 生态系统和工具. | Note: 如果你需要关于选择合适目标平台的指导, 请加入我们的 [Slack 社区](https://slack-chats.kotlinlang.org/c/multiplatform). 你可以在这里提问关于平台之间的差别, 性能考量, 以及特定使用场景的推荐实践. ## Web 目标平台的兼容模式 你可以为 Web 应用程序启用兼容模式, 以确保它能够在所有浏览器上直接使用. 在这种模式下, 你可以对现代浏览器使用 Wasm 构建 UI, 对较旧的浏览器则回退到 JS. 兼容模式通过对 `js` 和 `wasmJs` 两个目标平台进行交叉编译来实现. [查看关于 Web 兼容模式及其启用方法的更多信息](https://kotlinlang.org/docs/multiplatform/compose-multiplatform-create-first-app.html#compatibility-mode-for-web-targets). # Kotlin/JavaScript Kotlin/JavaScript (Kotlin/JS) 能够将你的 Kotlin 代码, Kotlin 标准库, 以及所有兼容的依赖项转译为 JavaScript. 这样, 你的 Kotlin 应用程序就能够在任何支持 JavaScript 的环境中运行. 通过 [Kotlin Multiplatform Gradle plugin](multiplatform-dsl-reference.html) (`kotlin.multiplatform`), 可以集中的配置和管理针对 JavaScript 的 Kotlin 项目. Kotlin Multiplatform Gradle plugin 提供的功能包括: 控制应用程序的打包(Bundling), 以及直接从 npm 添加 JavaScript 依赖项. 关于可用配置选项的概述, 请参见 [设置 Kotlin/JS 项目](js-project-setup.html). Tip: Kotlin/JS 目前的实现针对 [ES5](https://www.ecma-international.org/ecma-262/5.1/) 和 [ES2015](https://262.ecma-international.org/6.0/) 标准. ## Kotlin/JS 的使用场景 以下是 Kotlin/JS 的一些常见使用方式: * 在前端和 JVM 后端之间共用通用逻辑 如果你的后端使用 Kotlin 或其他 JVM 兼容语言编写, 你可以在 Web 应用程序和后端之间共用通用代码. 包括数据传输对象(DTO), 验证和认证规则, REST API 端点(Endpoint)的抽象等等. * 在 Android, iOS 和 Web 客户端之间共用通用逻辑 你可以在 Web 界面与 Android, iOS 移动应用程序之间共用业务逻辑, 同时在各个平台保持原生的用户界面. 这样可以避免重复实现共同的功能, 例如 REST API 抽象, 用户认证, 表单验证, 以及领域模型等等. * 使用 Kotlin/JS 构建前端 Web 应用程序 使用 Kotlin 开发传统的 Web 前端, 同时与现有的工具和库集成: * 如果你熟悉 Android 开发, 可以使用基于 Compose 的框架构建 Web 应用程序, 例如 [Kobweb](https://kobweb.varabyte.com/) 或 [Kilua](https://kilua.dev/). * 使用 JetBrains 提供的 [对常用 JavaScript 库的 Kotlin 封装](https://github.com/JetBrains/kotlin-wrappers), 用 Kotlin/JS 构建完全类型安全的 React 应用程序. Kotlin 封装 (`kotlin-wrappers`) 提供了对 React 和其他 JavaScript 框架的抽象和集成. 这些封装还支持补充库, 例如 [React Redux](https://react-redux.js.org/), [React Router](https://reactrouter.com/), 以及 [styled-components](https://styled-components.com/). 你还可以通过与 JavaScript 生态系统的互操作能力, 使用第三方 React 组件和组件库. * 使用 [Kotlin/JS 框架](js-frameworks.html), 这些框架与 Kotlin 生态系统集成, 支持简洁而富有表达力的代码. * 构建支持旧版浏览器的跨平台应用程序 使用 Compose Multiplatform, 你可以用 Kotlin 构建应用程序, 并在 Web 项目中复用移动端和桌面端的用户界面. 虽然 [Kotlin/Wasm](wasm-overview.html) 是这一目的的主要目标平台, 但你也可以同时面向 Kotlin/JS 目标平台, 扩展对旧版浏览器的支持. * 使用 Kotlin/JS 构建服务端和无服务器应用程序 Kotlin/JS 中的 Node.js 目标平台, 让你能够在 JavaScript 运行环境创建服务端或无服务器环境的应用程序. 这个功能提供了快速启动和低内存占用的优点. [kotlinx-nodejs](https://github.com/Kotlin/kotlinx-nodejs) 库 提供了从 Kotlin 对 [Node.js API](https://nodejs.org/docs/latest/api/) 的类型安全的访问能力. 根据你的使用场景, Kotlin/JS 项目可以使用来自 Kotlin 生态系统的兼容库, 以及来自 JavaScript 和 TypeScript 生态系统的第三方库. 要在 Kotlin 代码中使用第三方库, 你可以创建自己的类型安全封装, 或使用社区维护的封装. 此外, 你也可以使用 Kotlin/JS [动态类型](dynamic-type.html), 它允许你跳过严格类型检查和库封装, 但代价是失去了类型安全性. Kotlin/JS 还兼容于最常用的模块系统: [ESM](https://tc39.es/ecma262/#sec-modules), [CommonJS](https://nodejs.org/api/modules.html#modules-commonjs-modules), [UMD](https://github.com/umdjs/umd), 以及 [AMD](https://github.com/amdjs/amdjs-api). 这个功能让你能够 [生成和使用模块](js-modules.html), 并以有结构化的方式与 JavaScript 生态系统集成. ### 分享你的使用场景 [Kotlin/JS 的使用场景](#use-cases-for-kotlin-js) 中列举的并不是所有情况. 欢迎尝试不同的方案, 找到最适合你项目的方法. 请在 [Kotlin Slack](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up) 的 [#javascript](https://kotlinlang.slack.com/archives/C0B8L3U69) 频道, 与 Kotlin/JS 开发社区分享你的使用场景, 经验, 以及问题. ## Kotlin/JS 入门 探索 Kotlin/JS 的基础知识和初始步骤: * 如果你是 Kotlin 新手, 请先阅读 [基本语法](basic-syntax.html), 并探索 [Kotlin 观光之旅](kotlin-tour-welcome.html). * 查看 [Kotlin/JS 示例项目](#sample-projects-for-kotlin-js) 列表, 获取灵感. 这些示例包含了有用的代码片段和模式, 可以帮助你启动你的项目. * 如果你是 Kotlin/JS 新手, 请从 [设置向导](js-project-setup.html) 开始, 然后再探索更高级的主题. 想要尝试 Kotlin/JS 吗? [Kotlin/JS 入门](js-get-started.html) ## Kotlin/JS 示例项目 下表列出了一组示例项目, 演示 Kotlin/JS 的各种使用场景, 架构, 以及代码共用策略: | 项目 |说明 | ---------- | [Petclinic, 在 Spring 和 Angular 之间共用通用代码](https://github.com/Kotlin/kmp-spring-petclinic/#readme) |演示如何通过共用数据传输对象, 验证和认证规则, 以及 REST API 端点(Endpoint)的抽象, 避免在企业级应用程序中的代码重复. 代码在 [Spring Boot](https://spring.io/projects/spring-boot) 后端和 [Angular](https://angular.dev/) 前端之间共用. | | [Fullstack Conference CMS](https://github.com/Kotlin/kmp-fullstack-conference-cms/#readme) |展示了多种代码共用方式, 从最简单的到完全共用, 涵盖 [Ktor](https://ktor.io/), [Jetpack Compose](https://developer.android.com/compose) 和 [Vue.js](https://vuejs.org/) 应用程序之间的代码共用. | | [基于 Compose-HTML 的 Kobweb 框架的待办事项应用程序](https://github.com/varabyte/kobweb-templates/tree/main/examples/todo/#readme) |演示如何以 Android 开发者熟悉的方案, 创建待办事项列表应用程序. 它使用 [Kobweb 框架](https://kobweb.varabyte.com/) 构建客户端 UI 应用程序. | | [Android, iOS 和 Web 之间的简单逻辑共用](https://github.com/Kotlin/kmp-logic-sharing-simple-example/#readme) |包含一个模板, 用于构建项目, 其中包含 Kotlin 编写的通用逻辑, 这些通用逻辑可在 Android ([Jetpack Compose](https://developer.android.com/compose)), iOS ([SwiftUI](https://developer.apple.com/tutorials/swiftui/)) 和 Web ([React](https://react.dev/)) 的平台原生 UI 应用程序中使用. | | [全栈协作的待办事项列表](https://github.com/kotlin-hands-on/jvm-js-fullstack/#readme) |演示如何使用 Kotlin Multiplatform 的 JS 和 JVM 目标平台, 创建用于协作工作的待办事项列表应用程序. 后端使用 [Ktor](https://ktor.io/), 前端使用 Kotlin/JS 和 React. | ## Kotlin/JS 框架 Kotlin/JS 框架提供直接可用的组件, 路由, 状态管理, 以及其他工具, 简化了现代化 Web 应用程序的开发. [查看不同作者编写的 Kotlin/JS 可用框架](js-frameworks.html). ## 加入 Kotlin/JS 开发社区 你可以加入官方 [Kotlin Slack](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up) 的 [#javascript](https://kotlinlang.slack.com/archives/C0B8L3U69) 频道, 与社区和 Kotlin/JS 开发团队交流. ## 下一步 * [设置 Kotlin/JS 项目](js-project-setup.html) * [运行 Kotlin/JS 项目](running-kotlin-js.html) * [调试 Kotlin/JS 代码](js-debugging.html) * [在 Kotlin/JS 中运行测试](js-running-tests.html) # Kotlin/JS 入门 本教程演示如何使用 Kotlin/JavaScript (Kotlin/JS) 创建一个面向浏览器的 Web 应用程序. 要创建你的应用程序, 请选择最适合你工作流程的工具: * [IntelliJ IDEA](#create-your-application-in-intellij-idea): 从版本控制系统中克隆项目模板, 并在 IntelliJ IDEA 中使用. * [Gradle 构建系统](#create-your-application-using-gradle): 手动创建项目的构建文件, 更好地理解底层的配置工作原理. Tip: 除了针对浏览器之外, Kotlin/JS 还可以编译到其他环境. 详情请参见 [执行环境](js-project-setup.html#execution-environments). ## 在 IntelliJ IDEA 中创建应用程序 要创建 Kotlin/JS Web 应用程序, 可以使用 [IntelliJ IDEA](https://www.jetbrains.com/idea/download/). ### 设置环境 1. 下载并安装最新版本的 [IntelliJ IDEA](https://www.jetbrains.com/idea/). 2. 安装 [Kotlin Multiplatform IDE Plugin](https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform) (注意不要与 Kotlin Multiplatform Gradle Plugin 混淆). ### 创建项目 1. 在 IntelliJ IDEA 中, 选择 File | New | Project from Version Control. 2. 输入 [Kotlin/JS 模板项目](https://github.com/Kotlin/kmp-js-wizard) 的 URL: ```TEXT https://github.com/Kotlin/kmp-js-wizard ``` 3. 点击 Clone. ### 配置项目 1. 打开 `kmp-js-wizard/gradle/libs.versions.toml` 文件. 它包含项目依赖项的版本目录. 2. 确认 Kotlin 版本与 Kotlin Multiplatform Gradle Plugin 版本匹配, 创建面向 Kotlin/JS 的 Web 应用程序需要这个 Plugin: ```TOML [versions] kotlin = "2.4.0" [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } ``` 3. 同步 Gradle 文件(如果你更新了 `libs.versions.toml` 文件). 点击构建文件中出现的 Load Gradle Changes 图标. ![Load Gradle changes 按钮](images/load-gradle-changes.png)或者, 点击 Gradle 工具窗口中的刷新按钮. 关于跨平台项目的 Gradle 配置, 详情请参见 [Multiplatform Gradle DSL 参考](multiplatform-dsl-reference.html). ### 构建并运行应用程序 1. 打开 `src/jsMain/kotlin/Main.kt` 文件. * `src/jsMain/kotlin/` 目录包含你的项目针对 JavaScript 目标平台的 Kotlin main 源代码文件. * `Main.kt` 文件包含代码, 它使用 [kotlinx.browser](https://github.com/Kotlin/kotlinx-browser) API, 在浏览器页面上输出 "Hello, Kotlin/JS!". 2. 点击 `main()` 函数中的 Run 图标, 运行代码. ![运行应用程序](images/js-run-gutter.png) Web 应用程序会自动在浏览器中打开. 或者, 也可以在运行完成后, 在浏览器中打开以下 URL: ```TEXT http://localhost:8080/ ``` 你会看到这个 Web 应用程序: ![应用程序输出](images/js-output-gutter-1.png) 初次运行应用程序后, IntelliJ IDEA 会在顶部工具栏中, 创建对应的运行配置 (jsMain [js]): ![Gradle 运行配置](images/js-run-config.png) Tip: 在 Ultimate 订阅版的 IntelliJ IDEA 中, 你可以使用 [JS Debugger](https://www.jetbrains.com/help/idea/configuring-javascript-debugger.html), 直接在 IDE 中调试代码. ### 启用持续构建 Gradle 可以在你每次进行修改时, 自动重新构建项目: 1. 在运行配置列表中选择 jsMain [js], 点击 More Actions | Edit. ![Gradle 编辑运行配置](images/js-edit-run-config.png) 2. 在 Run/Debug Configurations 对话框中, 在 Run 栏内输入 `jsBrowserDevelopmentRun --continuous`. ![持续运行配置](images/js-continuous-run-config.png) 3. 点击 OK. 现在, 当你运行应用程序并进行任何修改后, Gradle 会自动对项目执行增量构建, 并在你保存(`Ctrl + S`/`Cmd + S`)或修改类文件时, 热重载浏览器. ### 修改应用程序 修改应用程序, 添加统计单词字母数的功能. #### 添加输入元素 1. 在 `src/jsMain/kotlin/Main.kt` 文件中, 通过 [扩展函数](extensions.html#extension-functions) 添加一个 [HTML input 元素](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input), 用来读取用户输入: ```KOTLIN // 替换旧代码: Element.appendMessage() 函数 fun Element.appendInput() { val input = document.createElement("input") appendChild(input) } ``` 2. 在 `main()` 中调用 `appendInput()` 函数. 它会在页面上显示一个输入元素: ```KOTLIN fun main() { // 替换旧代码: document.body!!.appendMessage(message) document.body?.appendInput() } ``` 3. [再次运行应用程序](#build-and-run-the-application). 你的应用程序现在看起来是这样的: ![带有一个输入元素的应用程序](images/js-added-input-element.png) #### 添加 input 事件处理 1. 在 `appendInput()` 函数内添加一个监听器, 用于读取输入值并响应变化: ```KOTLIN // 替换当前的 appendInput() 函数 fun Element.appendInput(onChange: (String) -> Unit = {}) { val input = document.createElement("input").apply { addEventListener("change") { event -> onChange(event.target.unsafeCast().value) } } appendChild(input) } ``` 2. 按照 IDE 的建议, 导入 `HTMLInputElement` 依赖项. ![导入依赖项](images/js-import-dependency.png) 3. 在 `main()` 中调用 `onChange` 回调. 它读取并处理输入值: ```KOTLIN fun main() { // 替换旧代码: document.body?.appendInput() document.body?.appendInput(onChange = { println(it) }) } ``` #### 添加输出元素 1. 定义一个创建段落的 [扩展函数](extensions.html#extension-functions), 添加文本元素, 用来显示输出: ```KOTLIN fun Element.appendTextContainer(): Element { return document.createElement("p").also(::appendChild) } ``` 2. 在 `main()` 中调用 `appendTextContainer()` 函数. 它创建输出元素: ```KOTLIN fun main() { // 为输出创建一个文本容器 // 替换旧代码: val message = Message(topic = "Kotlin/JS", content = "Hello!") val output = document.body?.appendTextContainer() // 读取输入值 document.body?.appendInput(onChange = { println(it) }) } ``` #### 处理输入, 统计字母数 处理输入, 删除空格, 并输出显示包含的字母数量. 在 `main()` 函数的 `appendInput()` 函数中, 添加以下代码: ```KOTLIN fun main() { // 为输出创建一个文本容器 val output = document.body?.appendTextContainer() // 读取输入值 // 替换当前的 appendInput() 函数 document.body?.appendInput(onChange = { name -> name.replace(" ", "").let { output?.textContent = "Your name contains ${it.length} letters" } }) } ``` 在上面的代码中: * [replace() 函数](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/replace.html) 删除名字中的空格. * [let{} 作用域函数](scope-functions.html#let) 在对象上下文中运行该函数. * [字符串模板](strings.html#string-templates) (`${it.length}`), 在字符串前加上美元符号 (`$`) 并将其包在花括号 (`{}`) 中, 插入单词的长度值. 其中 `it` 是 [Lambda 表达式参数](coding-conventions.html#lambda-parameters) 的默认名称. #### 运行应用程序 1. [运行应用程序](#build-and-run-the-application). 2. 输入你的名字. 3. 按下 `Enter`. 你会看到结果: ![应用程序输出](images/js-output-gutter-2.png) #### 处理输入, 统计唯一的字母数 作为额外的练习, 我们来处理输入, 计算并显示单词中唯一字母的数量: 1. 在 `src/jsMain/kotlin/Main.kt` 文件中, 为 `String` 添加 `.countDistinctCharacters()` [扩展函数](extensions.html#extension-functions): ```KOTLIN fun String.countDistinctCharacters() = lowercase().toList().distinct().count() ``` 在上面的代码中: * [.lowercase() 函数](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/lowercase.html) 将名字转换为小写. * [toList() 函数](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/to-list.html) 将输入字符串转换为字符列表. * [distinct() 函数](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/distinct.html) 只选择单词中的唯一字符. * [count() 函数](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/count.html) 统计唯一字符的数量. 2. 在 `main()` 中调用 `.countDistinctCharacters()` 函数. 它统计名字中唯一字母的数量: ```KOTLIN fun main() { // 为输出创建一个文本容器 val output = document.body?.appendTextContainer() // 读取输入值 document.body?.appendInput(onChange = { name -> name.replace(" ", "").let { // 打印唯一字母的数量 // 替换旧代码: output?.textContent = "Your name contains ${it.length} letters" output?.textContent = "Your name contains ${it.countDistinctCharacters()} unique letters" } }) } ``` 3. 按照步骤 [运行应用程序并输入你的名字](#run-the-application). 你会看到结果: ![应用程序输出](images/js-output-gutter-3.png) ## 使用 Gradle 创建应用程序 在本节中, 你可以了解如何使用 [Gradle](https://gradle.org) 手动创建 Kotlin/JS 应用程序. Gradle 是 Kotlin/JS 和 Kotlin Multiplatform 项目的默认构建系统. 它也经常在 Java, Android 和其他生态系统中使用. ### 创建项目文件 1. 确认你使用的 Gradle 版本与 Kotlin Gradle Plugin(KGP)兼容. 详情请参见 [兼容性表格](gradle-configure-project.html#apply-the-plugin). 2. 使用文件浏览器, 命令行, 或你喜欢的任何工具, 为你的项目创建一个空目录. 3. 在项目目录中, 创建一个 `build.gradle.kts` 文件, 内容如下: Kotlin: ```KOTLIN // build.gradle.kts plugins { kotlin("multiplatform") version "2.4.0" } repositories { mavenCentral() } kotlin { js { // 使用 browser(), 在浏览器中运行, 或使用 nodejs(), 在 Node.js 中运行 browser() binaries.executable() } } ``` Groovy: ```GROOVY // build.gradle plugins { id 'org.jetbrains.kotlin.multiplatform' version '2.4.0' } repositories { mavenCentral() } kotlin { js { // 使用 browser(), 在浏览器中运行, 或使用 nodejs(), 在 Node.js 中运行 browser() binaries.executable() } } ``` Note: 你可以使用不同的 [执行环境](js-project-setup.html#execution-environments), 例如 `browser()` 或 `nodejs()`. 每种环境定义了代码运行的位置, 并决定 Gradle 在项目中生成 task 名称的方式. 4. 在项目目录中, 创建一个空的 `settings.gradle.kts` 文件. 5. 在项目目录中, 创建 `src/jsMain/kotlin` 目录. 6. 在 `src/jsMain/kotlin` 目录中, 添加一个 `hello.kt` 文件, 内容如下: ```KOTLIN fun main() { println("Hello, Kotlin/JS!") } ``` 按照惯例, 所有的源代码都放在 `src/[Main|Test]/kotlin` 目录中: * `Main` 是源代码所在位置. * `Test` 是测试所在位置. * `<目标名称>` 对应目标平台(本例中为 `js`). 对于 `browser` 环境 Note: 如果你使用 `browser` 环境, 请按照接下来的步骤操作. 如果你使用 `nodejs` 环境, 请跳到 [构建并运行项目](#build-and-run-the-project) 章节. 1. 在项目目录中, 创建 `src/jsMain/resources` 目录. 2. 在 `src/jsMain/resources` 目录中, 创建一个 `index.html` 文件, 内容如下: ```HTML Application title ``` 3. 将 `<$NAME_OF_YOUR_PROJECT_DIRECTORY>` 占位符替换为你的项目目录名称. ### 构建并运行项目 要构建项目, 请在项目根目录运行以下命令: ```BASH # 用于浏览器 gradle jsBrowserDevelopmentRun # 或者 # 用于 Node.js gradle jsNodeDevelopmentRun ``` 如果你使用 `browser` 环境, 你会看到浏览器打开了 `index.html` 文件, 并在浏览器控制台中打印输出 `"Hello, Kotlin/JS!"`. 你可以使用 `Ctrl + Shift + J`/`Cmd + Option + J` 命令打开控制台. ![应用程序输出](images/js-output-gutter-4.png) 如果你使用 `nodejs` 环境, 你会看到终端打印输出 `"Hello, Kotlin/JS!"`. ![应用程序输出](images/js-output-gutter-5.png) ### 在 IDE 中打开项目 你可以在任何支持 Gradle 的 IDE 中打开你的项目. 如果你使用 IntelliJ IDEA: 1. 选择 File | Open. 2. 找到项目目录. 3. 点击 Open. IntelliJ IDEA 会自动检测它是不是 Kotlin/JS 项目. 如果在使用项目时遇到问题, IntelliJ IDEA 会在 Build 面板中显示错误信息. ## 下一步做什么? * [设置你的 Kotlin/JS 项目](js-project-setup.html). * 了解如何 [调试 Kotlin/JS 应用程序](js-debugging.html). * 了解如何 [使用 Kotlin/JS 编写和运行测试](js-running-tests.html). * 了解如何 [为实际的 Kotlin/JS 项目编写 Gradle 构建脚本](multiplatform-dsl-reference.html). * 阅读 [Gradle 构建系统](gradle.html) 的更多内容. # 设置 Kotlin/JS 工程(Project) Kotlin JavaScript 工程(Project) 使用 Gradle 进行编译. 为了方便开发者管理 Kotlin JavaScript 工程, 我们提供了 `kotlin.multiplatform` Gradle 插件, 其中包括工程配置工具, 以及对 JavaScript 开发中常见业务进行自动化处理的帮助性任务. 这个插件会在后台使用 [npm](https://www.npmjs.com/) 或 [Yarn](https://yarnpkg.com/) 包管理器下载 npm 依赖项, 并使用 [webpack](https://webpack.js.org/) 将 Kotlin 工程编译为 JavaScript bundle . 依赖项管理和配置调整大部分可以直接在 Gradle 构建脚本文件中完成, 还可以通过选项覆盖自动生成的配置, 获得完全的控制能力. 你可以在 Gradle 工程的 `build.gradle(.kts)` 文件中手动的应用 `org.jetbrains.kotlin.multiplatform` 插件: Kotlin: ```KOTLIN plugins { kotlin("multiplatform") version "2.4.0" } ``` Groovy: ```GROOVY plugins { id 'org.jetbrains.kotlin.multiplatform' version '2.4.0' } ``` 通过 Kotlin Multiplatform Gradle 插件, 你可以在编译脚本的 `kotlin` 节中管理工程的各方面设置. ```GROOVY kotlin { // ... } ``` 在 `kotlin {}` 代码段中, 你可以管理以下方面: * [目标执行环境](#execution-environments): 浏览器, 或 Node.js * [支持 ES2015 功能](#support-for-es2015-features): 类, 模块, 和生成器 * [配置输出粒度](#configure-output-granularity) * [生成 TypeScript 声明文件](#generation-of-typescript-declaration-files-d-ts) * [工程的依赖项目管理](#dependencies): Maven 或 npm * [运行配置(configuration)](#run-task) * [测试配置(configuration)](#test-task) * 对于浏览器工程的 [打包(Bundling)](#webpack-bundling) 和 [CSS 支持](#css) * [目标目录](#distribution-target-directory) 和 [模块名称](#module-name) * [工程的 package.json 文件](#package-json-customization) ## 执行环境 Kotlin/JS 工程可以运行于两种不同的执行环境: * 浏览器环境, 用于浏览器内运行的客户端脚本 * [Node.js](https://nodejs.org/), 在浏览器之外运行 JavaScript 代码, 比如, 运行服务器端脚本. 要为 Kotlin/JS 工程定义目标运行环境, 需要添加 `js {}` 代码段, 其中包含 `browser {}` 或 `nodejs {}`: ```GROOVY kotlin { js { browser { } binaries.executable() } } ``` `binaries.executable()` 指令明确的指示 Kotlin 编译器输出可执行的 `.js` 文件. 省略 `binaries.executable()` 会导致编译器只生成 Kotlin-internal 库文件, 这些库文件可以被其他项目使用, 但不能独立运行. Tip: 与创建可执行文件相比, 这样通常会更快, 而且在处理你项目中的非叶(non-leaf)模块时, 这是一种可能的优化. Kotlin Multiplatform 插件会针对选定的运行环境, 自动配置它的编译任务. 包括下载并安装应用程序运行和测试所需要的环境和依赖项目, 因此开发者可以编译, 运行, 以及测试简单的工程, 而无需再添加更多配置. 对于编译目标为 Node.js 的项目, 还有一个选项可以使用本地已安装的 Node.js. 详情请参见 [使用已安装的 Node.js](#use-pre-installed-node-js). ## 支持 ES2015 功能 Kotlin 支持 ES2015 功能, 包括: * 模块: 简化你的代码库, 提高可维护性的. * 类: 可以结合 OOP 原则, 产生更清晰, 更直观的代码. * 用于编译 [挂起函数](composing-suspending-functions.html) 的生成器: 能够改善最终 bundle 的大小, 并帮助进行调试. * [内联 JavaScript 代码](js-interop.html#inline-javascript). 你可以向你的 `build.gradle(.kts)` 文件添加 `es2015` 编译目标, 一次性启用所有支持的 ES2015 功能: ```KOTLIN tasks.withType().configureEach { compilerOptions { target = "es2015" } } ``` [关于 ES2015 (ECMAScript 2015, ES6), 更多详情请参见官方文档](https://262.ecma-international.org/6.0/). ## 配置输出粒度 你可以选择让编译器在你的项目中如何输出 `.js` 文件: * 对每个模块输出 `.js` 文件. 默认情况下, JS 编译器的编译结果是对项目的每个模块输出单独的 `.js` 文件. * 对每个项目输出 `.js` 文件. 你也可以将整个项目编译为单个 `.js` 文件, 方法是向 `gradle.properties` 文件添加以下设置: ```PROPERTIES kotlin.js.ir.output.granularity=whole-program // 默认值是 'per-module' ``` * 对每个文件输出 `.js` 文件. 你也可以设置更加细粒度的输出, 为每个 Kotlin 文件生成 1 个 JavaScript 文件 (如果 Kotlin 文件包含导出的声明, 则会生成 2 个 JavaScript 文件). 启用这个模式的方法如下: 1. 将 [编译目标](#support-for-es2015-features) 设置为 `es2015`, 在你的项目中支持 ES2015 功能. 2. 将以下内容添加到 `gradle.properties` 文件: ```PROPERTIES kotlin.js.ir.output.granularity=per-file // 默认值是 'per-module' ``` ## 生成 TypeScript 声明文件 (`d.ts`) Kotlin/JS 编译器能够从你的 Kotlin 代码生成 TypeScript 定义. 在开发混合 App(Hybrid App)时, JavaScript 工具和 IDE 可以使用这些定义实现以下功能: * 提供代码自动完成 * 支持静态分析 * 简化在 JavaScript 和 TypeScript 项目中添加 Kotlin 代码的过程 生成 TypeScript 定义对于 [共用业务逻辑的使用场景](js-overview.html#use-cases-for-kotlin-js) 尤其有用. 编译器会收集所有标注了 [@JsExport](js-to-kotlin-interop.html#jsexport-annotation) 注解的顶级声明, 并自动在一个 `.d.ts` 文件中生成 TypeScript 定义. 要生成 TypeScript 定义, 请在你的 Gradle 构建文件中明确进行配置. 请在你的 `build.gradle.kts` 文件的 [js {} 代码段](#execution-environments) 中添加 `generateTypeScriptDefinitions()` 函数: ```KOTLIN kotlin { js { binaries.executable() browser { } generateTypeScriptDefinitions() } } ``` 这些声明位于 `build/js/packages//kotlin` 目录中, 与相应的未经 webpack 处理的 JavaScript 代码在一起. ## 依赖项目 与其他 Gradle 工程一样, Kotlin/JS 工程编译脚本的 `dependencies {}` 代码段内, 支持添加传统的 Gradle [依赖项目声明](https://docs.gradle.org/current/userguide/declaring_dependencies.html): Kotlin: ```KOTLIN dependencies { implementation("org.example.myproject", "1.1.0") } ``` Groovy: ```GROOVY dependencies { implementation 'org.example.myproject:1.1.0' } ``` Kotlin Multiplatform Gradle 插件也支持在编译脚本的 `kotlin {}` 代码段中添加特定源代码集合(source set)的依赖项目声明: Kotlin: ```KOTLIN kotlin { sourceSets { val jsMain by getting { dependencies { implementation("org.example.myproject:1.1.0") } } } } ``` Groovy: ```GROOVY kotlin { sourceSets { jsMain { dependencies { implementation 'org.example.myproject:1.1.0' } } } } ``` Note: 并不是 Kotlin 编程语言中所有可用的库在 JavaScript 平台都可用: 只有那些包含针对 Kotlin/JS 的 artifact 的库才能使用. 如果你添加的库依赖于 [来自 npm 的包](#npm-dependencies), Gradle 也会自动解析这些传递性依赖项. ### Kotlin 标准库 对 [标准库](https://kotlinlang.org/api/latest/jvm/stdlib/index.html) 的依赖项会自动添加. 标准库的版本与 Kotlin Multiplatform 插件的版本相同. 对于跨平台的测试, 可以使用 [kotlin.test](https://kotlinlang.org/api/latest/kotlin.test/) API. 当你创建跨平台项目时, 你可以在 `commonTest` 中使用一个依赖项, 对所有的源代码集添加测试依赖项: Kotlin: ```KOTLIN kotlin { sourceSets { commonTest.dependencies { implementation(kotlin("test")) // 会自动引入所有的平台依赖项 } } } ``` Groovy: ```GROOVY kotlin { sourceSets { commonTest { dependencies { implementation kotlin("test") // 会自动引入所有的平台依赖项 } } } } ``` ### npm 依赖项目 在 JavaScript 的世界中, 管理依赖项目的最常见方式是 [npm](https://www.npmjs.com/). 它提供了各种 JavaScript 模块(module) 的最大的公共仓库(repository). 通过 Kotlin Multiplatform Gradle 插件, 可以在 Gradle 编译脚本中声明 npm 依赖项目, 方法和声明其他依赖项目类似. 要声明一个 npm 依赖项目, 可以在一个依赖项目声明中使用 `npm()` 函数指定依赖项目的名称和版本. 也可以使用 [npm semver 语法](https://docs.npmjs.com/about-semantic-versioning), 指定一个或多个版本范围. Kotlin: ```KOTLIN dependencies { implementation(npm("react", "> 14.0.0 <=16.9.0")) } ``` Groovy: ```GROOVY dependencies { implementation npm('react', '> 14.0.0 <=16.9.0') } ``` 默认情况下, 插件会使用 [Yarn](https://yarnpkg.com/lang/en/) 包管理器的一个单独的实例, 来下载和安装 npm 依赖项. 不需要额外配置, 默认即可工作, 但你也可以 [根据需要对其进行调整](#yarn). 你也可以通过 [npm](https://www.npmjs.com/) 包管理器直接使用 npm 依赖项. 要使用 npm 作为你的包管理器, 请在你的 `gradle.properties` 文件中, 设置以下属性: ```PROPERTIES kotlin.js.yarn=false ``` 除了标准依赖项之外, 在 Gradle DSL 中使用还可以使用 3 种其他类型的依赖项. 关于什么情况下应该选择什么类型的依赖项, 请阅读 npm 提供的官方文档: * [devDependencies](https://docs.npmjs.com/files/package.json#devdependencies), 通过 `devNpm(...)` 使用, * [optionalDependencies](https://docs.npmjs.com/files/package.json#optionaldependencies) 通过 `optionalNpm(...)` 使用, 以及 * [peerDependencies](https://docs.npmjs.com/files/package.json#peerdependencies) 如果 `peerNpm(...)` 使用. 一个 npm 依赖项目安装完成之后, 你就可以如 [在 Kotlin 中调用 JavaScript](js-interop.html) 中介绍过的那样, 在你的代码中使用它的 API. ## run 任务 Kotlin Multiplatform Gradle 插件提供了一个 `jsBrowserDevelopmentRun` 任务, 它可以运行你的纯 Kotlin/JS 工程, 无需额外的配置. 对于在浏览器内运行 Kotlin/JS 工程的情况, 这个是 `browserDevelopmentRun` 任务的一个别名(在 Kotlin 跨平台项目也可以使用). 它使用 [webpack DevServer](https://webpack.js.org/configuration/dev-server/) 来提供你的 JavaScript artifact. 如果你想要自定义 DevServer 的配置, 例如, 改变端口号, 请使用 [webpack 配置文件](#webpack-bundling). 对于在 Node.js 平台运行 Kotlin/JS 项目的情况, 请使用 `jsNodeDevelopmentRun` 任务, 它是 `nodeRun` 任务的别名. 要运行一个工程, 请执行 Gradle 编译周期(lifecycle)中标准的 `jsBrowserDevelopmentRun` 任务, 或者运行它作为别名对应的真实的任务: ```BASH ./gradlew jsBrowserDevelopmentRun ``` 如果要在修改过源代码文件后自动对你的应用程序进行重新构建, 可以使用 Gradle 的 [连续构建(continuous build)](https://docs.gradle.org/current/userguide/command_line_interface.html#sec:continuous_build) 功能: ```BASH ./gradlew jsBrowserDevelopmentRun --continuous ``` 或者 ```BASH ./gradlew jsBrowserDevelopmentRun -t ``` 工程构建成功后, `webpack-dev-server` 会自动刷新浏览器页面. ## test 任务 Kotlin Multiplatform Gradle 插件会为工程自动设置测试环境. 对于浏览器工程, 它会下载并安装测试运行器 [Karma](https://karma-runner.github.io/), 以及相关的依赖项目; 对于 Node.js 项目, 会使用 [Mocha](https://mochajs.org/) 测试框架. 插件还提供了很多有用的测试功能, 比如: * 生成源代码文件映射(Source map) * 生成测试报告(Test report) * 在控制台输出测试运行结果 为了运行浏览器中的测试, 插件会默认使用 [Headless Chrome](https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md). 你也可以选择其他浏览器来运行测试, 方法是在编译脚本的 `useKarma {}` 代码段中添加相应的设置: ```GROOVY kotlin { js { browser { testTask { useKarma { useIe() useSafari() useFirefox() useChrome() useChromeCanary() useChromeHeadless() usePhantomJS() useOpera() } } } binaries.executable() // ... } } ``` 或者你也可以在 `gradle.properties` 文件中添加测试的目标浏览器: ```PROPERTIES kotlin.js.browser.karma.browsers=firefox,safari ``` 通过这种方法, 你可以为所有的模块定义一组浏览器, 然后在某些模块的构建脚本中添加特定的浏览器. 请注意, Kotlin Multiplatform Gradle 插件不会为你自动安装这些浏览器, 而只是使用那些在它的运行环境中可用的浏览器. 比如说, 如果在一个持续集成服务器上运行 Kotlin/JS 测试, 请注意确保安装了你需要测试的浏览器. 如果想要跳过测试, 可以在 `testTask {}` 代码段中添加 `enabled = false` 设置. ```GROOVY kotlin { js { browser { testTask { enabled = false } } binaries.executable() // ... } } ``` 要运行测试, 请执行 Gradle 编译周期(lifecycle)中标准的 `check` 任务: ```BASH ./gradlew check ``` 如果要指定你的 Node.js 测试运行器使用的环境变量 (比如, 向你的测试代码传递外部信息, 或对包的解析进行微调), 可以在你的构建脚本的 `testTask {}` 代码段中使用 `environment()` 函数, 参数是键-值对: ```GROOVY kotlin { js { nodejs { testTask { environment("key", "value") } } } } ``` ## 配置 Karma Kotlin Multiplatform Gradle 插件会在构建时自动生成 Karma 配置文件, 其中包括你的 `build.gradle(.kts)` 文件中的 [kotlin.js.browser.testTask.useKarma {} 代码段](#test-task) 中的设置. 你可以在 `build/js/packages/projectName-test/karma.conf.js` 找到这个文件. 要调整 Karma 所使用的配置, 请将你的额外配置文件 放在你的项目根目录的 `karma.config.d` 目录之下. 在构建时, 这个目录下的所有 `.js` 配置文件都会被读取, 并自动合并到生成的 `karma.conf.js` 文件中. Karma 配置的详细功能请参见 Karma 的 [文档](https://karma-runner.github.io/5.0/config/configuration-file.html). ## webpack 打包(Bundling) 如果编译目标为浏览器环境, Kotlin Multiplatform Gradle 插件使用大家都熟悉的 [webpack](https://webpack.js.org/) 来打包模块. ### webpack 版本 Kotlin Multiplatform 插件使用 webpack 5. 如果你的项目通过 plugin 1.5.0 以前版本创建, 那么可以在你的项目的 `gradle.properties` 文件中添加以下设置, 临时切换回这些版本使用的 webpack 4: ```PROPERTIES kotlin.js.webpack.major.version=4 ``` ### webpack 任务 在 Gradle 编译脚本的 `kotlin.js.browser.webpackTask {}` 配置代码段中, 可以直接调整最常见的 webpack 配置: * `outputFileName` - webpack 的输出文件名称. 执行webpack 任务之后, 这个文件将生成在 `/build/dist/` 文件夹内. 默认值是工程名称. * `output.libraryTarget` - 用于 webpack 输出文件的模块系统. 详情请参见 [Kotlin/JS 工程可用的模块系统](js-modules.html). 默认值是 `umd`. ```GROOVY webpackTask { outputFileName = "mycustomfilename.js" output.libraryTarget = "commonjs2" } ``` 还可以在 `commonWebpackConfig {}` 代码段中配置 webpack 的共通设置, 用于打包(bundling), 运行, 以及测试任务. ### webpack 配置文件 Kotlin Multiplatform Gradle plugin 在构建时会自动生成一个标准的 webpack 配置文件. 位置是 `build/js/packages/projectName/webpack.config.js`. 如果还想对 webpack 配置进行进一步的调整, 请将你的额外的配置文件放在你的工程的 `webpack.config.d` 目录内. 编译你的工程时, 所有的 `.js` 配置文件都会被自动合并到 `build/js/packages/projectName/webpack.config.js` 文件内. 比如, 如果要添加一个新的 [webpack loader](https://webpack.js.org/loaders/), 请要把以下内容添加到 `webpack.config.d` 目录内的一个 `.js` 中: Note: 这种情况下, 配置对象是全局对象 `config`. 你需要在你的脚本中修改这个对象. ```GROOVY config.module.rules.push({ test: /\.extension$/, loader: 'loader-name' }); ``` 关于 webpack 的所有配置项目, 请参见它的 [文档](https://webpack.js.org/concepts/configuration/). ### 构建可执行文件 要通过 webpack 编译可执行的 JavaScript artifact, Kotlin Multiplatform Gradle 插件包含 Gradle 任务 `browserDevelopmentWebpack` 和 `browserProductionWebpack`. * `browserDevelopmentWebpack` 创建开发模式的 artifact, 文件尺寸会比较大, 但构建时间比较短. 因此, 在活跃开发阶段请使用 `browserDevelopmentWebpack` 任务. * `browserProductionWebpack` 会执行死代码消除, 生成 artifact 文件, 并对输出结果的 JavaScript 文件最小化, 构建时间更长, 但生成的可执行文件尺寸更小. 因此, 在构建你的项目用于生成目的时, 请使用 `browserProductionWebpack` 任务. 执行这两个任务可以分别得到开发模式和生产模式的 artifact 文件. 生成的文件会在 `build/dist` 目录下, 除非 [另有设置](#distribution-target-directory). ```BASH ./gradlew browserProductionWebpack ``` 注意, 只有在你的编译目标设置为生成可执行文件 (通过 `binaries.executable()`) 时, 这些任务才可用. ## CSS Kotlin Multiplatform Gradle 创建还支持 webpack 的 [CSS](https://webpack.js.org/loaders/css-loader/) 和 [style](https://webpack.js.org/loaders/style-loader/) 装载器. 虽然所有的选项都可以直接修改构建你的项目的 [webpack 配置文件](#webpack-bundling), 但最常用的方法是使用 `build.gradle(.kts)` 中直接可用的设定. 要在你的项目中打开 CSS 支持, 请在 Gradle 构建文件的 `commonWebpackConfig {}` 代码段中设置 `cssSupport.enabled` 选项. 通过 IDE 向导创建新工程时, 这个配置也会默认启用. Kotlin: ```KOTLIN browser { commonWebpackConfig { cssSupport { enabled.set(true) } } } ``` Groovy: ```GROOVY browser { commonWebpackConfig { cssSupport { it.enabled = true } } } ``` 或者, 也可以单独对 `webpackTask {}`, `runTask {}`, 和 `testTask {}` 添加 CSS 支持: Kotlin: ```KOTLIN browser { webpackTask { cssSupport { enabled.set(true) } } runTask { cssSupport { enabled.set(true) } } testTask { useKarma { // ... webpackConfig.cssSupport { enabled.set(true) } } } } ``` Groovy: ```GROOVY browser { webpackTask { cssSupport { it.enabled = true } } runTask { cssSupport { it.enabled = true } } testTask { useKarma { // ... webpackConfig.cssSupport { it.enabled = true } } } } ``` 对你的项目打开 CSS 支持, 有助于防止在未配置的项目中使用样式表时发生的常见错误, 比如 `Module parse failed: Unexpected character '@' (14:0)`. 可以使用 `cssSupport.mode` 来指定 CSS 应该如何处理. 可选的设定值如下: * `"inline"` (默认值): 样式添加到全局的 `