├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore └── README.md /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 11 | - [ ] 已仔细阅读 kotlin-web-site-cn#⁠35 及其中链接的内容 12 | - [ ] 阅读过 Kotlin 参考的大部分章节 13 | - [ ] 每一句都已经过谷歌机翻作为参考 14 | - [ ] 每一句都已经过搜狗机翻作为参考 15 | - [ ] 翻译中所用术语均与术语表中一致 16 | - [ ] 注释也已翻译,除了 `//sampleStart` 与 `//sampleEnd` 17 | - [ ] 正文与注释中的标点均已使用全角 18 | - [ ] 中文与英文/数字之间、中文与 `` ` `` 之间都以空格分隔 19 | - [ ] 英文与标点之间未留空格 20 | - [ ] 行级对照,中文句子中间断行处已使用 HTML 注释 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /.vscode/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 面向 Python 开发者的 Kotlin 教程 2 | 3 | *英文原文作者是 [Aasmund Eldhuset](https://eldhuset.net/),[可汗学院(Khan Academy)](https://www.khanacademy.org/)软件工程师。原文发布于 2018-11-29。* 4 | *本文档原文并非可汗学院官方产品的一部分,而是他们为造福编程社区而“按原样”(“as is”)提供的[内部资源](https://blog.khanacademy.org/kotlin-for-python-developers/)。如果发现任何**原文**错误,请在[原文版本库](https://github.com/Khan/kotlin-for-python-developers)提交 [issue](https://github.com/Khan/kotlin-for-python-developers/issues) 或 [pull request](https://github.com/Khan/kotlin-for-python-developers/pulls)。* 5 | *而如果发现任何**译文**错误,请在[中文版本库](https://github.com/hltj/kotlin-for-python-developers-cn)提交 [issue](https://github.com/hltj/kotlin-for-python-developers-cn/issues) 或 [pull request](https://github.com/hltj/kotlin-for-python-developers-cn/pulls)。* 6 | 7 | --- 8 | 9 | Kotlin 是一种编译型的静态类型语言,这可能会给习惯于解释型、动态类型的 Python 用户带来一些初始障碍。本文档旨在解释 Kotlin 的大部分语法、概念以及与 Python 中相应概念的比较。 10 | 11 | Kotlin 可以为多个不同平台编译。在本文档中,假定目标平台是 Java 虚拟机,它提供了一些附加功能——尤其是会将代码编译为 Java 字节码,进而能够与 Java 库的庞大生态系统互操作。 12 | 13 | 即使你不了解 Python,这篇文档应该也是对 Kotlin 的有用介绍,尤其是如果你已习惯于其他动态类型语言。但是如果你有 Java 背景,最好直接去看优秀的[官方网文档](https://www.kotlincn.net/docs/home.html)(本文档也从中汲取了很多灵感)。一定程度上讲,你可以按照 Java 代码的方式编写,并在所尝试的内容不起作用时查找资料——一些 IDE 甚至可以自动将 Java 代码转换为 Kotlin 代码。 14 | 15 | 16 | ## 目录 17 | 18 | * [Hello World](#hello-world) 19 | * [编译与运行](#编译与运行) 20 | * [声明变量](#声明变量) 21 | * [只读变量](#只读变量) 22 | * [常量](#常量) 23 | * [显式指定类型](#显式指定类型) 24 | * [作用域与命名](#作用域与命名) 25 | * [原生数据类型及其表示范围](#原生数据类型及其表示范围) 26 | * [整型](#整型) 27 | * [浮点数与其他类型](#浮点数与其他类型) 28 | * [字符串](#字符串) 29 | * [条件式](#条件式) 30 | * [`if`/`else`](#ifelse) 31 | * [比较](#比较) 32 | * [`when`](#when) 33 | * [集合](#集合) 34 | * [循环](#循环) 35 | * [`for`](#for) 36 | * [`while`](#while) 37 | * [`continue` 与 `break`](#continue-与-break) 38 | * [函数](#函数) 39 | * [声明](#声明) 40 | * [调用](#调用) 41 | * [返回](#返回) 42 | * [重载](#重载) 43 | * [Vararg 与可选/命名参数](#vararg-与可选命名参数) 44 | * [类](#类) 45 | * [声明与实例化](#声明与实例化) 46 | * [继承的内置函数](#继承的内置函数) 47 | * [属性](#属性) 48 | * [构造函数与初始化块](#构造函数与初始化块) 49 | * [Setter 与 getter](#setter-与-getter) 50 | * [成员函数](#成员函数) 51 | * [Lateinit](#lateinit) 52 | * [中缀函数](#中缀函数) 53 | * [操作符](#操作符) 54 | * [枚举类](#枚举类) 55 | * [数据类](#数据类) 56 | * [异常](#异常) 57 | * [抛出与捕获](#抛出与捕获) 58 | * [Nothing](#nothing) 59 | * [空安全](#空安全) 60 | * [使用空值](#使用空值) 61 | * [安全调用操作符](#安全调用操作符) 62 | * [Elvis 操作符](#elvis-操作符) 63 | * [非空断言操作符](#非空断言操作符) 64 | * [函数式编程](#函数式编程) 65 | * [函数类型](#函数类型) 66 | * [函数字面值:lambda 表达式与匿名函数](#函数字面值lambda-表达式与匿名函数) 67 | * [集合推导](#集合推导) 68 | * [接收者](#接收者) 69 | * [内联函数](#内联函数) 70 | * [不错的工具函数](#不错的工具函数) 71 | * [`run()`、`let()` 与 `with()`](#runlet-与-with) 72 | * [`apply()` 与 `also()`](#apply-与-also) 73 | * [`takeIf()` 与 `takeUnless()`](#takeif-与-takeunless) 74 | * [包与导入](#包与导入) 75 | * [包](#包) 76 | * [导入](#导入) 77 | * [可见性修饰符](#可见性修饰符) 78 | * [继承](#继承) 79 | * [子类化](#子类化) 80 | * [覆盖](#覆盖) 81 | * [接口](#接口) 82 | * [抽象类](#抽象类) 83 | * [多态](#多态) 84 | * [类型转换与类型检测](#类型转换与类型检测) 85 | * [委托](#委托) 86 | * [属性委托](#属性委托) 87 | * [密封类](#密封类) 88 | * [对象与伴生对象](#对象与伴生对象) 89 | * [对象声明](#对象声明) 90 | * [伴生对象](#伴生对象) 91 | * [对象表达式](#对象表达式) 92 | * [泛型](#泛型) 93 | * [泛型类型参数](#泛型类型参数) 94 | * [约束](#约束) 95 | * [型变](#型变) 96 | * [简介](#简介) 97 | * [声明处协变与逆变](#声明处协变与逆变) 98 | * [型变方向](#型变方向) 99 | * [类型投影(使用处协变与逆变)](#类型投影使用处协变与逆变) 100 | * [具体化的类型参数](#具体化的类型参数) 101 | * [扩展函数/属性](#扩展函数属性) 102 | * [成员引用与反射](#成员引用与反射) 103 | * [属性引用](#属性引用) 104 | * [函数引用](#函数引用) 105 | * [由类引用获取成员引用](#由类引用获取成员引用) 106 | * [Java 风格反射](#java-风格反射) 107 | * [注解](#注解) 108 | * [文件 I/O](#文件-io) 109 | * [作用域内资源用法](#作用域内资源用法) 110 | * [编写文档](#编写文档) 111 | 112 | 113 | ## Hello World 114 | 115 | 开门见山,将以下信息键入到一个扩展名为 `.kt` 的文件中: 116 | 117 | ```kotlin 118 | fun main(args: Array) { 119 | println("Hello World!") 120 | } 121 | ``` 122 | 123 | Kotlin 文件的顶层只能有导入与声明。因此“运行”单个文件只有在其中包含*入口点*时才有意义,该*入口点*必须是名为 `main` 的函数,该函数有一个名为 `args`、类型为“字符串数组”的参数。`args` 会包含调用程序的命令行参数,类似于 Python 中的 `sys.argv`;如果程序并不需要接受命令行参数并且使用的是 Kotlin 1.3,那么可以省略该参数: 124 | 125 | ```kotlin 126 | fun main() { 127 | println("Hello World!") 128 | } 129 | ``` 130 | 131 | 函数体由花括号分隔——缩进在 Kotlin 中通常不重要[^1],但是为了肉眼可读性理应正确缩进代码。 132 | 133 | [^1]: 译者注:这里是指语法,实际开发中应该遵循 [Kotlin 的编码规范](https://www.kotlincn.net/docs/reference/coding-conventions.html)。 134 | 135 | 注释以 `//` 开始一直到行尾。块注释以 `/*` 开头、以 `*/` 结尾。 136 | 137 | 与 Python 类似,Kotlin 中语句也能以分号结尾,但是并不鼓励这么用。Kotlin 中没有续行符;如果行合并是使代码正确解析的唯一方式,那么该行会自动与一到多个后续行相连。在实际使用中,这意味着如果当前行的左圆括号未配对(与 Python 类似),或者当前行以“悬空操作符”结尾(与 Python 不同),或者后续行如果不与当前行相连就无法解析(与 Python 不同)。请注意,这几乎[与 JavaScript 相反](https://stackoverflow.com/questions/2846283/what-are-the-rules-for-javascripts-automatic-semicolon-insertion-asi#2846298),在 JavaScript 中只要代码仍能解析,它通常会继续连接尽量多的行。因此,以下代码在 Kotlin 与 Python 中是两个表达式(因为 `+` 可以是一元操作符,所以第二行能够独立解析),但是在 JavaScript 中是一个表达式: 138 | 139 | ```kotlin 140 | 1 + 2 141 | + 3 142 | ``` 143 | 144 | 这段代码在 Kotlin(因为第一行不能独立解析)与 JavaScript 中都是一个表达式,而在 Python 中不能解析: 145 | 146 | ```kotlin 147 | 1 + 2 + 148 | 3 149 | ``` 150 | 151 | 以下这段代码也一样,`+` 与 `.` 之间的区别在于 `+` 可以是一元操作符,但 `.` 不可以,因此解析第二行的唯一方式是将其与前一行相连: 152 | 153 | ```kotlin 154 | x.foo() 155 | .bar() 156 | ``` 157 | 158 | 这段代码在三门语言中都是一个表达式: 159 | 160 | ```kotlin 161 | (1 + 2 162 | + 3) 163 | ``` 164 | 165 | 如果将一行拆分为两行后各自作为独立行语法上都有效(即使导致与 Kotlin 语法没有直接关系的编译错误),就不要拆分该行。以下代码实际上并不会返回 `foo()` 的结果——它返回一个称为 `Unit` 的特殊值(稍后会介绍它),并且永远不会调用 `foo()`。 166 | 167 | ```kotlin 168 | return // 空 return 语句 169 | foo() // 独立的,不可达语句 170 | ``` 171 | 172 | 173 | ## 编译与运行 174 | 175 | 作者强烈建议使用支持 Kotlin 的 IDE,因为静态类型让 IDE 能够进行可靠的导航与代码补全。我推荐 [IntelliJ IDEA](https://www.jetbrains.com/idea/),它与 Kotlin 都是同一家公司出品的。其社区版免费;参见[引入指引](https://www.kotlincn.net/docs/getting-started.html)(其中预置了 Kotlin,可以在 IDE 中运行程序)。 176 | 177 | 如果你坚持使用普通编辑器与命令行,请参见[这些指引](https://www.kotlincn.net/docs/command-line.html)。 简而言之,在运行之前需要*编译* Kotlin 代码。假设你的 Kotlin 文件名为 `program.kt`: 178 | 179 | ```bash 180 | kotlinc program.kt -include-runtime -d program.jar 181 | ``` 182 | 183 | 默认情况下,Kotlin 编译为 Java[^2](因此可以使用整个 Java 标准库并且与 Java 库交互也易如反掌),于是现在有了一个 Java 归档文件(`program.jar`),其中包含了支持 Kotlin 特性所必需的 Java 库(多亏了 `-include-runtime`),之后就可以使用开箱即用的 Java 来运行了: 184 | 185 | [^2]: 译者注:实际上是 Java 字节码 186 | 187 | ```bash 188 | java -jar program.jar 189 | ``` 190 | 191 | 192 | ## 声明变量 193 | 194 | 每个变量都必需*声明*。任何尝试使用尚未声明的变量都会是语法错误;因此可以防止意外赋值给拼错的变量。声明还决定了允许在变量中存储哪种数据。 195 | 196 | 局部变量通常在声明时同时初始化,对于这种情况,变量的类型*推断*为初始化所使用的表达式的类型: 197 | 198 | ```kotlin 199 | var number = 42 200 | var message = "Hello" 201 | ``` 202 | 203 | 现在有一个局部变量 `number`,其值为 42、其类型为 `Int`(因为这是字面值 `42` 的类型),还有一个局部变量 `message`,其值为 `Hello`、其类型为 `String`。变量的后续用法必须只使用其变量名而不带 `var`: 204 | 205 | ```kotlin 206 | number = 10 207 | number += 7 208 | println(number) 209 | println(message + " there") 210 | ``` 211 | 212 | 然而不能改变变量的类型:`number` 只能引用 `Int` 值,而 `message` 只能引用 `String` 值,因此 `number = "Test"` 与 `message = 3` 都是非法的、都会产生语法错误。 213 | 214 | 215 | ### 只读变量 216 | 217 | 在变量的生存期内,通常仅需要引用一个对象。然后,可以使用 `val`(用于“值”)声明它: 218 | 219 | ```kotlin 220 | val message = "Hello" 221 | val number = 42 222 | ``` 223 | 224 | 关键字 `var` 声明了一个 _可变_ 变量,而 `val` 声明了一个 _只读_ 或者说 _只赋值一次_ 的变量——因此这两种都称为 _变量_。 225 | 226 | 请注意,只读变量本身不是常量:可以使用变量的值进行初始化(因此,在编译期不需要知道其值),如果在构造函数中声明了该变量,并反复调用(例如函数或循环),那么每次调用时可以采用不同的值。同样,尽管只读变量在作用域内可能无法重新赋值,但它仍可以引用自身是可变的对象(例如列表)。 227 | 228 | 229 | ### 常量 230 | 231 | 如果有一个真正常量的值,并且该值是在编译期已知的字符串或原生类型(请参见下文),那么可以声明一个实际常量。只能在文件的顶层或[对象声明](#对象声明)内(但不能在类声明内)执行此操作: 232 | 233 | ```kotlin 234 | const val x = 2 235 | ``` 236 | 237 | 238 | ### 显式指定类型 239 | 240 | 如果确实需要,可以在同一行上初始化并指定类型。如果要处理类层次结构(稍后会详细介绍),并且希望变量类型是值的类的基类型,那么这是非常有用的: 241 | 242 | ```kotlin 243 | val characters: CharSequence = "abc" 244 | ``` 245 | 246 | 在本文档中,有时会不必要地指定类型,以突出显示表达式产生的类型。(此外,良好的 IDE 可以显示结果类型。) 247 | 248 | 为了完整起见:也可以(但不鼓励)拆分声明与初始赋值,甚至可以根据某些条件在多个位置进行初始化。只能在编译器可以证明每个可能的执行路径都已将其初始化的点读取变量。如果以这种方式创建只读变量,那么还必须确保每个可能的执行路径都 _刚好只_ 赋值一次。 249 | 250 | ```kotlin 251 | val x: String 252 | x = 3 253 | ``` 254 | 255 | 256 | ### 作用域与命名 257 | 258 | 变量仅存在于其中声明了它的 _作用域_(花括号括起来的代码块;稍后会详细介绍)内——因此,在循环内声明的变量仅存在于该循环内。无法在循环后检测其最终值。可以在嵌套作用域内重新声明变量——因此,如果函数有一个参数 `x`,在该函数内创建一个循环并在该循环内声明一个 `x`,那么该循环内的 `x` 与函数内的 `x` 不同。 259 | 260 | 变量名称应使用 `lowerCamelCase`(小写驼峰命名)而不是 `snake_case`(下划线命名)。 261 | 262 | 通常,标识符可以由字母、数字与下划线组成,并且不能以数字开头。但是,如果正在编写这样的代码:根据标识符自动生成 JSON,并且希望 JSON 键是不符合这些规则或与关键字冲突的字符串,可以将其括在反引号中:`` `I can't believe this is not an error!` `` 是有效的标识符。 263 | 264 | 265 | ## 原生数据类型及其表示范围 266 | 267 | _原生数据类型_ 是 Kotlin 中最基本的类型。所有其他类型均由这些类型及其数组组成。它们的表现(在内存与 CPU 时间方面都)非常高效,因为它们映射到可由 CPU 直接操作的小字节组。 268 | 269 | 270 | ### 整型 271 | 272 | 与 Python 中任意大的整数相反,Kotlin 中的整数类型具有 _大小限制_。该限制取决于类型,而类型决定了该数字在内存中占用多少比特: 273 | 274 | 类型 | 比特数 | 最小值 | 最大值 275 | -----|------|-----------|---------- 276 | `Long` | 64 | -9223372036854775808 | 9223372036854775807 277 | `Int` | 32 | -2147483648 | 2147483647 278 | `Short` | 16 | -32768 | 32767 279 | `Byte` | 8 | -128 | 127 280 | 281 | 由于 Kotlin 继承了 Java 的不良设计决策,因此字节数为 -128 至 127。为了获得介于 0 与 255 之间的传统字节值,如果该值是正数,那么将其保持原样;如果它是负数,那么将其加上 256(因此,-128 实际上是 128,而 -1 是真正的 255)。请参见[扩展函数](#扩展函数属性)部分,以获取解决方案。 282 | 283 | 如果整数字面的值适合 `Int`,那么其类型为 `Int`,否则为 `Long`。为清晰起见,`Long` 字面量应加 `L` 后缀,这也使得可以将“小”值设为 `Long`。`Short` 或 `Byte` 没有字面后缀,因此此类值需要显式类型声明或使用显式转换函数。 284 | 285 | ```kotlin 286 | val anInt = 3 287 | val anotherInt = 2147483647 288 | val aLong = 2147483648 289 | val aBetterLong = 2147483649L 290 | val aSmallLong = 3L 291 | val aShort: Short = 32767 292 | val anotherShort = 1024.toShort() 293 | val aByte: Byte = 65 294 | val anotherByte = (-32).toByte() 295 | ``` 296 | 297 | 请注意,将整数除以整数会产生整数(类似于 Python 2,但与 Python 3不同)。如果需要浮点结果,那么至少一个操作数需要为浮点数(并且请记住,就像在大多数语言中一样,浮点运算通常是不精确的): 298 | 299 | ```kotlin 300 | println(7 / 3) // 输出 2 301 | println(7 / 3.0) // 输出 2.3333333333333335 302 | val x = 3 303 | println(7 / x) // 输出 2 304 | println(7 / x.toDouble()) // 输出 2.3333333333333335 305 | ``` 306 | 307 | 每当对相同类型的两个整数使用算术运算符时(或使用例如 `-` 之类的一元运算符时),_如果结果不适合操作数的类型,那么不会自动进行“升级”!_ 试试这个: 308 | 309 | ```kotlin 310 | val mostPositive = 2147483647 311 | val mostNegative = -2147483648 312 | println(mostPositive + 1) 313 | println(-mostNegative) 314 | ``` 315 | 316 | 这两个命令都输出 `-2147483648`,因为仅存储了“真实”结果的低 32 比特。 317 | 318 | 当对两个不同类型的整数使用算术运算符时,结果将“升级”为最大类型。请注意,结果仍有可能溢出。 319 | 320 | 简而言之:_请仔细考虑整数的声明,并绝对肯定该值永远不会大于该类型的限制!_ 如果需要无限制大小的整数,请使用非原始类型 `BigInteger`。 321 | 322 | 323 | ### 浮点数与其他类型 324 | 325 | 类型 | 比特数 | 注释 326 | -----|------|------ 327 | `Double` | 64 | 16~17 位有效数字(与 Python 中的 `float` 相同) 328 | `Float` | 32 | 6~7 位有效数字 329 | `Char` | 16 | UTF-16 代码单元(请参见[字符串](#字符串)——在大多数情况下,这是一个 Unicode 字符,但也可能只是 Unicode 字符的一半) 330 | `Boolean` | 8 | `true` 或 `false` 331 | 332 | 浮点数的作用与 Python 中的相似,但根据所需的位数,分为两种类型。如果需要更高的精度,或者需要处理货币金额(或必须具有精确结果的其他情况),请使用非原始类型 `BigDecimal`。 333 | 334 | 335 | ## 字符串 336 | 337 | 保证 Unicode 正确性在 Python 2 中可能很繁琐,因为“默认”字符串类型 `str` 实际上只是一个字节数组,而 `unicode` 实际上是一系列 _代码单元_(参见下文)——代码单元是 16 位还是 32 位宽取决于 Python 发行版本的构建方式。在 Kotlin 中,没有这种混乱:`String` 是声明字符串字面值(只能用双引号引起来)时得到的,它是 UTF-16 代码单元的不可变序列。`ByteArray` 是固定大小(但可变的)字节数组(并且 `String` 明确 *不能* 用作字节数组)。 338 | 339 | UTF-16 _代码单元_ 是一个 16 位无符号整数值,代表一个 Unicode _代码点_(字符代码),或者必须与另一个代码单元结合形成一个代码单元。如果觉得这没有意义,那么强烈推荐阅读[由 Joel Spolsky 撰写的关于 Unicode 及其编码的出色文章](https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/)。对于大多数西方脚本(包括英语),所有代码点都位于一个代码单元内,因此很容易将代码单元视为字符——但是一旦代码遇到非西方脚本,就会误入歧途。单个 UTF-16 代码单元可以用单引号表示,并具有 `Char` 类型: 340 | 341 | ```kotlin 342 | val c = 'x' // Char 343 | val message = "Hello" // String 344 | val m = message[0] // Char 345 | ``` 346 | 347 | 因此,单引号不能用于声明字符串字面值。 348 | 349 | 给定字符串 `s`,可以通过调用 `s.toByteArray()` 获得带有字符串 UTF-8 编码的 `ByteArray`,或者可以指定其他编码,例如 `s.toByteArray(Charsets.US_ASCII)` ——就像 Python 中的 `encode()` 一样。给定一个字节数组 `b`,其中包含一个 UTF-8 编码的字符串,那么可以通过调用 `String(b)` 获得 `String`。如果使用其他编码,请使用例如 `String(b, Charsets.US_ASCII)`,就像 Python 中的 `decode()` 一样。也可以调用例如 `b.toString(Charsets.US_ASCII)`,但 _不要_ 在没有参数的情况下调用 `b.toString()`(这只会输出对字节数组的内部引用)。 350 | 351 | 可以使用 `$` 进行字符串插值,并对表达式使用花括号: 352 | 353 | ```kotlin 354 | val name = "Anne" 355 | val yearOfBirth = 1985 356 | val yearNow = 2018 357 | val message = "$name is ${yearNow - yearOfBirth} years old" 358 | ``` 359 | 360 | 如果要使用文本 `$`,那么需要对其进行转义:`\$`。转义通常以与 Python 中相同的方式工作,并具有一组类似的标准转义序列。 361 | 362 | 363 | 364 | ## 条件式 365 | 366 | 367 | ### `if`/`else` 368 | 369 | `if`/`else` 的工作方式与 Python 相同,但是使用 `else if` 而不是 `elif`,条件用小括号括起来,而主体用花括号括起来: 370 | 371 | ```kotlin 372 | val age = 42 373 | if (age < 10) { 374 | println("You're too young to watch this movie") 375 | } else if (age < 13) { 376 | println("You can watch this movie with a parent") 377 | } else { 378 | println("You can watch this movie") 379 | } 380 | ``` 381 | 382 | 如果主体只有一条语句,那么可以省略主体周围的花括号。除非主体与条件在同一行,否则不建议这样做,因为这样做很容易犯错误,尤其是习惯使用 Python 的人: 383 | 384 | ```kotlin 385 | if (age < 10) 386 | println("You're too young to watch this movie") 387 | println("You should go home") // 错误——这不是 if 主体的一部分! 388 | ``` 389 | 390 | 没有花括号,只有第一行是主体的一部分。Kotlin 的缩进仅对易读性有意义,因此第二条输出在 if 之外,并且总是会被执行。 391 | 392 | if/else 语句也是一个表达式,这意味着在 Kotlin 中,三元运算符(在 Python 中像是 `result = true_body if condition else false_body`)看起来就像这样: 393 | 394 | ```kotlin 395 | val result = if (condition) trueBody else falseBody 396 | ``` 397 | 398 | 使用 if/else 作为表达式时,`else` 部分是必需的(但也可以有 `else if` 部分)。如果最后要求值的主体包含多行,那么返回最后一行的结果作为 `if`/`else` 的结果。 399 | 400 | 401 | ### 比较 402 | 403 | 结构相等性比较是使用 `==` 或 `!=` 进行的,但取决于每个类来定义含义是什么,就像在 Python 中一样,可以通过[覆盖](#覆盖) [`equals()`](#继承的内置函数)(将在左侧操作数上调用,以右侧操作数为参数)与 `hashCode()`。大多数内置集合类型对这些运算符和函数执行深度相等检测。检测两个变量是否引用同一对象(与 Python 中的 `is` 相同)——用 `===` 或 `!==` 进行。 404 | 405 | 布尔表达式由 `&&` 表示逻辑“与”,`||` 表示逻辑“或”,而 `!` 表示逻辑“非”。与 Python 中一样,`&&` 与 `||` 是短路的:它们仅在需要求值时才检测右侧。请注意,关键字 `and` 与 `or` 也存在,但是它们仅对整数值执行 _逐位_ 操作,并且不会短路。 406 | 407 | 没有自动转换为布尔值的方法,因此也没有真值(truthy)与假值(falsy)的概念:必须使用 `==` 或 `!=` 显式进行是否为零、为空容器或为 null 的检测。 大多数集合类型都有 `isEmpty()` 与 `isNotEmpty()` 函数。 408 | 409 | 410 | ### `when` 411 | 412 | The [`when` expression](https://kotlinlang.org/docs/control-flow.html#when-expression) has similarities with pattern matching introduced in Python 3.10. 它可以用非常紧凑的方式将一个表达式与多种表达式进行比较(但这不是完整的函数式编程风格的模式匹配器)。例如: 413 | 414 | ```kotlin 415 | val x = 42 416 | when (x) { 417 | 0 -> println("zero") 418 | in 1..9 -> println("single digit") 419 | else -> println("multiple digits") 420 | } 421 | ``` 422 | 423 | 424 | ## 集合 425 | 426 | Kotlin 中的数组具有恒定的长度,因此通常使用列表,这些列表类似于 Python 中的列表。在 Python 中称为 _字典_ 而在 Kotlin 中称为 _map_(不要与 `map()` 函数混淆)。`List`、`Map` 与 `Set` 都是由许多不同的类实现的 _接口_。在大多数情况下,将使用标准的基于数组的列表或基于哈希的 Map 或 Set,并且可以轻松地进行如下操作: 427 | 428 | ```kotlin 429 | val strings = listOf("Anne", "Karen", "Peter") // List 430 | val map = mapOf("a" to 1, "b" to 2, "c" to 3) // Map 431 | val set = setOf("a", "b", "c") // Set 432 | ``` 433 | 434 | (请注意,`to` 是一个[中缀函数](#中缀函数),它创建一个包含键和值的 `Pair`,并以此构建 Map。)结果集合是不可变的——既不能更改其大小,也不能替换其元素——但是,元素本身仍可能是可变对象。对于可变集合,请执行以下操作: 435 | 436 | ```kotlin 437 | val strings = mutableListOf("Anne", "Karen", "Peter") 438 | val map = mutableMapOf("a" to 1, "b" to 2, "c" to 3) 439 | val set = mutableSetOf("a", "b", "c") 440 | ``` 441 | 442 | 可以使用 `c.size` 获得集合 `c` 的大小/长度(字符串对象除外,由于 Java 的遗留问题,必须使用 `s.length` 代替)。 443 | 444 | 不幸的是,如果想要一个空集合,那么需要显式声明结果集合类型,或者将元素类型提供给构造该集合的函数: 445 | 446 | ```kotlin 447 | val noInts: List = listOf() 448 | val noStrings = listOf() 449 | val emptyMap = mapOf() 450 | ``` 451 | 452 | 尖括号内的类型称为 _泛型参数_,将在后面介绍。简而言之,这是使一个类与另一个类绑定的有用技术(例如,将容器类与其元素类绑定)且适用于许多不同的类。 453 | 454 | Coming from Python, you might be used to creating lists that contain elements of different types. This is discouraged in Kotlin, except when dealing with [polymorphic types](#polymorphism). In many cases, such as when returning multiple values from a function, the differently-typed values represent different kinds of information; it would then be better to create a [data class](#data-classes) with named properties of the appropriate types, or to use the per-element-typed `Pair` or `Triple` instead. However, if you really need to, you can put anything inside `listOf()` and the other collection creation functions. Kotlin will then infer the "lowest common denominator" supertype of the types of the given values, and you'll get a list of that element type. If the values have nothing in common, the element type will be `Any`, or `Any?` if one or more of the values are `null`: 455 | 456 | ```kotlin 457 | val mixed = listOf("a", 2, 3.14) // List 458 | val mixedWithNull = listOf("a", 2, 3.14, null) // List 459 | ``` 460 | 461 | If you need a collection with a more general type than the values you are initializing it with, you can specify the type like this: `listOf(1, 2, 3)`. 462 | 463 | 464 | 465 | ## 循环 466 | 467 | 468 | ### `for` 469 | 470 | Kotlin 的循环类似于 Python 的循环。`for` 用于遍历任何 _可遍历对象_(任何具有提供 `Iterator` 对象的 `iterator()` 函数的对象)或者本身就是迭代器的对象: 471 | 472 | ```kotlin 473 | val names = listOf("Anne", "Peter", "Jeff") 474 | for (name in names) { 475 | println(name) 476 | } 477 | ``` 478 | 479 | 请注意,`for` 循环始终隐式声明一个新的只读变量(在本示例中为 `name`)——如果外部作用域已经包含一个具有相同名称的变量,那么该变量将被不相关的循环变量遮盖。出于同样的原因,循环变量的最终值在循环后不可访问。 480 | 481 | In every iteration, the type of the loop variable is the same as the element type of the iterable, even if you are iterating over a mixed-type list. With `for (x in listOf("a", 2, 3.14))`, the type of `x` will always be `Any`, and you'll need to cast it in order to perform any operations that depend on knowing the "real" type. This is one of the reasons that mixed-type lists are usually only useful with [polymorphic types](#polymorphism), where the common supertype defines operations that are applicable to all the subtypes. In the example below, `Number` is a supertype of both `Int` and `Double`; it defines `toDouble()`, which converts the number to a `Double`, which can be multiplied. It would _not_ work to simply write `x * 2`. 482 | 483 | ```kotlin 484 | for (x in listOf(2, 3.14)) { 485 | println(x.toDouble() * 2) 486 | } 487 | ``` 488 | 489 | 可以使用 `..` 操作符创建区间——但要注意,与 Python 的 `range()` 不同,它 _包含_ 其端点: 490 | 491 | ```kotlin 492 | for (x in 0..10) println(x) // 输出 0 到 10(含10) 493 | ``` 494 | 495 | 如果要排除最后一个值,请使用 `until`: 496 | 497 | ```kotlin 498 | for (x in 0 until 10) println(x) // 输出 0 到 9 499 | ``` 500 | 501 | 可以使用 `step` 控制增量: 502 | 503 | ```kotlin 504 | for (x in 0 until 10 step 2) println(x) // 输出 0, 2, 4, 6, 8 505 | ``` 506 | 507 | step 值必须为正。如果需要递减计数,请使用内置的 `downTo`: 508 | 509 | ```kotlin 510 | for (x in 10 downTo 0 step 2) println(x) // 输出 10, 8, 6, 4, 2, 0 511 | ``` 512 | 513 | 以上例子中所有 `in` 右边的表达式都可以在循环外部使用,以生成 _区间_(一种可遍历的类型——这类似于 Python 2 中的 `xrange()` 或 Python 3 中的 `range()`),可以稍后进行遍历或转换为列表: 514 | 515 | ```kotlin 516 | val numbers = (0..9).toList() 517 | ``` 518 | 519 | 如果在遍历时需要了解当前元素的索引,可以使用 `withIndex()`,它对应于 `enumerate()`。它产生一系列具有两个属性(索引与值)以及两个特殊命名的访问器函数的对象序列,分别称为 `component1()` 与 `component2()`。Kotlin 允许将这样的对象解构为声明: 520 | 521 | ```kotlin 522 | for ((index, value) in names.withIndex()) { 523 | println("$index: $value") 524 | } 525 | ``` 526 | 527 | 可以通过几种不同的方式遍历 Map,具体取决于是想要键、要值还是两个都要: 528 | 529 | ```kotlin 530 | // 遍历条目为包含键与值作为属性的对象 531 | for (entry in map) { 532 | println("${entry.key}: ${entry.value}") 533 | } 534 | 535 | // 遍历条目,将键值分开为单独的对象 536 | for ((key, value) in map) { 537 | println("$key: $value") 538 | } 539 | 540 | // 遍历键 541 | for (key in map.keys) { 542 | println(key) 543 | } 544 | 545 | // 遍历值 546 | for (value in map.values) { 547 | println(value) 548 | } 549 | ``` 550 | 551 | 552 | ### `while` 553 | 554 | `while` 循环与 Python 类似(但请记住,条件必须是实际的布尔表达式,因为没有真值(truthy)与假值(falsy)的概念)。 555 | 556 | ```kotlin 557 | var x = 0 558 | while (x < 10) { 559 | println(x) 560 | x++ // 等同于 x += 1 561 | } 562 | ``` 563 | 564 | 循环变量(如果有)必须在 `while` 循环外声明,因此可以在以后检测,此时它们将包含使循环条件为假的值。 565 | 566 | 567 | ### `continue` 与 `break` 568 | 569 | 普通的 `continue` 或 `break` 与 Python 中的工作方式相同:`continue` 跳到最里面的包含循环的下一个迭代,而 `break` 停止循环。但是,也可以用 _标签_ 循环并在 `continue` 或 `break` 语句中引用该标签,以指示要影响哪个循环。标签是标识符,后跟 `@`,例如:`outer@`(可能后跟一个空格)。例如,生成质数: 570 | 571 | ```kotlin 572 | outer@ for (n in 2..100) { 573 | for (d in 2 until n) { 574 | if (n % d == 0) continue@outer 575 | } 576 | println("$n is prime") 577 | } 578 | ``` 579 | 580 | 请注意,`continue`/`break` 与 `@` 之间必须没有空格。 581 | 582 | 583 | ## 函数 584 | 585 | 586 | ### 声明 587 | 588 | 函数使用 `fun` 关键字声明。对于参数,不仅必须声明其名称,还必须声明其类型,并且必须声明函数返回值的类型。函数的主体通常是一个 _代码块_,用花括号括起来: 589 | 590 | ```kotlin 591 | fun happyBirthday(name: String, age: Int): String { 592 | return "Happy ${age}th birthday, $name!" 593 | } 594 | ``` 595 | 596 | 在这里,`name` 必须是一个字符串,`age` 必须是一个整数,并且该函数必须返回一个字符串。但是,也可以创建一个单行函数,其中主体只是要返回其结果的表达式。在这种情况下,将推断返回类型,并使用等号表示它是一个单行代码: 597 | 598 | ```kotlin 599 | fun square(number: Int) = number * number 600 | ``` 601 | 602 | (请注意,没有 `**` 运算符;应通过 `Math.pow()` 进行非平方幂运算。) 603 | 604 | 函数名称应使用 `lowerCamelCase`(小写驼峰命名)而不是 `snake_case`(下划线命名)。 605 | 606 | 607 | ### 调用 608 | 609 | 函数的调用方式与 Python 相同: 610 | 611 | ```kotlin 612 | val greeting = happyBirthday("Anne", 32) 613 | ``` 614 | 615 | 如果不需要返回值,那么无需赋值给任何变量。 616 | 617 | 618 | ### 返回 619 | 620 | 与 Python 相反,在函数末尾省略 `return` 不会隐式返回 null;如果要返回 null,那么必须使用 `return null`。如果一个函数不需要任何返回值,那么该函数应该声明返回类型为 `Unit`(或者根本不声明返回类型,在这种情况下,返回类型默认为 `Unit`)。在这样的函数中,可能根本没有 `return` 语句,或只有 `return`。`Unit` 既是一个单例对象(在 Python 中也恰好是 `None`),也是该对象的类型,它表示“此函数不会返回任何信息”(而不是“此函数可能返回信息,但这次没有返回信息”——这或多或少是返回 null 的语义)。 621 | 622 | 623 | ### 重载 624 | 625 | 在 Python 中,函数名称在模块或类中必须唯一。而在 Kotlin 中,可以 _重载_ 函数:可以有多个具有相同名称的函数声明。重载的函数必须通过其参数列表相互区分。(参数列表的类型与返回类型一起被称为函数的 _签名_,但是返回类型不能用于消除重载函数的歧义。)例如,可以在同一个文件中同时声明这两个函数: 626 | 627 | ```kotlin 628 | fun square(number: Int) = number * number 629 | fun square(number: Double) = number * number 630 | ``` 631 | 632 | 在调用时,要使用的函数取决于参数的类型: 633 | 634 | ```kotlin 635 | square(4) // 调用第一个函数;结果为 16 (Int) 636 | square(3.14) // 调用第二个函数;结果为 9.8596 (Double) 637 | ``` 638 | 639 | 尽管此示例恰好使用相同的表达式,但这不是必须的——如果需要,重载的函数可以做完全不同的事情(尽管可以使行为截然不同的函数互相重载,但是代码可能会造成混乱)。 640 | 641 | 642 | ### Vararg 与可选/命名参数 643 | 644 | 函数可以接受任意数量的参数,类似于 Python 中的 `*args`,但它们必须都属于同一类型。与 Python 不同的是,可以在可变参数之后声明其他位置参数,但最多可以有一个可变参数。如果其类型为 `X` 并且 `X` 是基本类型,那么参数的类型为 `XArray`,否则为 `Array`。 645 | 646 | ```kotlin 647 | fun countAndPrintArgs(vararg numbers: Int) { 648 | println(numbers.size) 649 | for (number in numbers) println(number) 650 | } 651 | ``` 652 | 653 | Kotlin 中没有 `**kwargs`,但是可以定义具有默认值的可选参数,并且在调用函数时可以选择命名部分或所有参数(无论它们是否具有默认值)。具有默认值的参数仍必须明确指定其类型。像在 Python 中一样,已命名的参数可以在调用时随意重新排序: 654 | 655 | ```kotlin 656 | fun foo(decimal: Double, integer: Int, text: String = "Hello") { ... } 657 | 658 | foo(3.14, text = "Bye", integer = 42) 659 | foo(integer = 12, decimal = 3.4) 660 | ``` 661 | 662 | 663 | 在 Python 中,默认值的表达式只在函数定义时计算一次。这导致了这个经典的陷阱,开发人员希望每次调用没有 `numbers` 值的函数时都得到一个新的空列表,但是每次都使用相同的列表: 664 | 665 | ```python 666 | def tricky(x, numbers=[]): # Bug:每次调用都会看到相同的列表! 667 | numbers.append(x) 668 | print numbers 669 | ``` 670 | 671 | 在 Kotlin 中,每次调用函数时,都会计算默认值的表达式。因此,只要使用在每次求值时生成新列表的表达式,就可以避免上述陷阱 672 | 673 | ```kotlin 674 | fun tricky(x: Int, numbers: MutableList = mutableListOf()) { 675 | numbers.add(x) 676 | println(numbers) 677 | } 678 | ``` 679 | 680 | 因此,不应该将带有副作用的函数用作默认值初始化程序,因为副作用将在每次调用时发生。如果仅引用变量而不是调用函数,那么每次调用该函数时都会读取相同的变量:`numbers: MutableList = myMutableList`。如果变量是不可变的,那么每个调用将看到相同的值(但如果该值本身是可变的,那么在两次调用之间可能会更改),如果变量是可变的,那么每个调用将看到该变量的当前值。不用说,这些情况很容易引起混淆,因此默认值初始化器应该是一个常数或一个函数调用,该调用总是产生具有相同值的新对象。 681 | 682 | 可以使用包含所有可变参数的一个数组(而不是列表或任何其他可迭代对象)来调用可变参数函数,使用 `*` 运算符(与 Python 相同的语法)将数组 _展开_: 683 | 684 | ```kotlin 685 | val numbers = listOf(1, 2, 3) 686 | countAndPrintArgs(*numbers.toIntArray()) 687 | ``` 688 | 689 | Kotlin 继承了 Java 烦躁的数组系统,因此原始类型具有自己的数组类型与转换函数,而其他任何类型都使用通用 `Array` 类型,可以使用 `.toTypedArray()` 转换为该类型。 690 | 691 | 但是,不能将 Map 展开到函数调用中,然后期望将 Map 中的值传递给以键命名的参数——必须在编译时知道参数的名称。如果需要运行时定义的参数名称,那么函数必须采用 Map 或采用 `vararg kwargs: Pair`(其中 `X` 是参数类型的“最低公分母”,在最坏的情况下 `Any?`——做好必须强制转换参数值的准备,并注意将失去类型安全性)。可以调用这样的函数:`foo("bar" to 42, "test" to "hello")`,因为 `to` 是创建 `Pair` 的[中缀函数](#中缀函数)。 692 | 693 | 694 | ## 类 695 | 696 | Kotlin 的对象模型与 Python 的对象模型有很大的不同。最重要的是,类 _不能_ 在运行时动态修改!(对此有一些有限的例外,但是通常不应该这样做。但是,_可以_ 使用称为 _反射_ 的特性在运行时动态 _探查_ 类与对象——这可能很有用,但应谨慎使用。)必须直接在类主体中声明或作为[_扩展函数_](#扩展函数属性)声明类中可能需要的属性与函数,因此应该仔细考虑类设计。 697 | 698 | 699 | ### 声明与实例化 700 | 701 | 用 `class` 关键字声明类。没有任何属性或函数的基本类如下所示: 702 | 703 | ```kotlin 704 | class Empty 705 | ``` 706 | 707 | 然后,可以按照类似于 Python 的方式创建该类的实例,就好像该类是一个函数(但这只是语法糖——与 Python 不同,Kotlin 中的类并不是真正的函数): 708 | 709 | ```kotlin 710 | val object = Empty() 711 | ``` 712 | 713 | 就像在 Python 中一样,类名应使用 `UpperCamelCase`(大写驼峰命名)。 714 | 715 | 716 | ### 继承的内置函数 717 | 718 | 每个未明确声明父类的类都从 `Any` 继承,Any 是类层次结构的根(类似于 Python 中的 `object`)——有关[继承](#继承)的更多信息见下文。通过 `Any`,每个类自动具有以下函数: 719 | 720 | * `toString()` 返回对象的字符串表示形式,类似于 Python 中的 `__str__()`(默认实现相当有趣,因为它仅返回类名与类似于对象 ID 的名称) 721 | * `equals(x)` 检测此对象是否与任何类的某个其他对象 `x` 相同(默认情况下,它仅检测该对象是否与 `x` 是 _相同的_ 对象——类似 Python 中的 `is`——但可以被子类覆盖以进行属性值的自定义比较) 722 | * `hashCode()` 返回一个整数,哈希表可以使用该整数并用于简化复杂的相等比较(根据 `equals()` 相等的对象必须具有相同的哈希码,因此,如果两个对象的哈希码不同,那么这些对象不能相等) 723 | 724 | 725 | ### 属性 726 | 727 | 空类不是很有趣,所以来创建一个具有一些 _属性_ 的类: 728 | 729 | ```kotlin 730 | class Person { 731 | var name = "Anne" 732 | var age = 32 733 | } 734 | ``` 735 | 736 | 请注意,必须明确指定属性的类型。与 Python 相反,在类内部直接声明属性不会创建类级别的属性,而是创建实例级别的属性:`Person` 的每个实例都有 _它自己_ 的 `name` 和 `age`。它们的值将在每个实例中分别以 `"Anne"` 与 `32` 生成,但是每个实例中的值可以独立于其他实例进行修改: 737 | 738 | ```kotlin 739 | val a = Person() 740 | val b = Person() 741 | println("${a.age} ${b.age}") // 输出 "32 32" 742 | a.age = 42 743 | println("${a.age} ${b.age}") // 输出 "42 32" 744 | ``` 745 | 746 | 公平地说,在 Python 中会得到相同的输出,但是机制会有所不同:两个实例生成时自身都没有任何属性(`age` 与 `name` 将是类的属性),并且 第一次输出将访问类属性;只有赋值会导致 `age` 属性出现在 `a` 上。在 Kotlin 中,此示例中没有类属性,并且每个实例都从这两个属性生成。如果需要类级别的属性,请参见[伴生对象](#伴生对象)一节。 747 | 748 | 由于对象的属性集必须严格限制为在对象类的编译时声明的属性集,因此无法在运行时将新属性添加到对象或类中。所以,例如 `a.nationality = "Norwegian"` 将无法通过编译。 749 | 750 | 属性名称应使用 `lowerCamelCase`(小写驼峰命名)而不是 `snake_case`(下划线命名)。 751 | 752 | 753 | ### 构造函数与初始化块 754 | 755 | 没有合理默认值的属性应作为构造函数参数。像 Python 的 `__init__()` 一样,只要创建对象的实例,Kotlin 构造函数和初始化程序块就会自动运行(请注意,没有与 `__new__()` 相对应的函数)。Kotlin 类可以具有一个 _主构造函数_,其参数在类名之后提供。在类主体中初始化属性时,主构造函数参数是可用的,在可选 _初始化块_ 中也是可用的,可选初始化块可以包含复杂的初始化逻辑(可以在没有初始值的情况下声明属性,在这种情况下,必须在 `init` 中对其进行初始化)。另外,经常需要使用 `val` 而不是 `var`,以使构造后属性不可变。 756 | 757 | ```kotlin 758 | class Person(firstName: String, lastName: String, yearOfBirth: Int) { 759 | val fullName = "$firstName $lastName" 760 | var age: Int 761 | 762 | init { 763 | age = 2018 - yearOfBirth 764 | } 765 | } 766 | ``` 767 | 768 | 如果想要对构造函数参数值进行的所有操作就是声明指定名称的属性,那么可以在主构造函数参数列表中声明该属性(下面的单行代码足以声明属性,声明构造函数参数以及使用参数初始化属性): 769 | 770 | ```kotlin 771 | class Person(val name: String, var age: Int) 772 | ``` 773 | 774 | 如果需要多种方法来初始化类,那么可以创建 _次构造函数_,每个构造函数看起来都像一个名称为 `constructor` 的函数。每个次构造函数都必须使用 `this` 关键字来调用另一个(主或次)构造函数,就好像它是一个函数一样(以便每个实例构造最终都调用该主构造函数)。 775 | 776 | ```kotlin 777 | class Person(val name: String, var age: Int) { 778 | constructor(name: String) : this(name, 0) 779 | constructor(yearOfBirth: Int, name: String) 780 | : this(name, 2018 - yearOfBirth) 781 | } 782 | ``` 783 | 784 | (如果需要做的事情比主构造函数还要多,那么次构造函数也可以使用花括号括起来。)这些构造函数通过其参数类型彼此区分开,就像在普通函数重载中一样。这就是必须在最后一个次构造函数中翻转参数顺序的原因——否则,它与主构造函数将无法区分(参数名称不是函数签名的一部分,并且对重载解析没有任何影响)。在以下示例中,可以通过三种不同的方式创建一个 `Person`: 785 | 786 | ```kotlin 787 | val a = Person("Jaime", 35) 788 | val b = Person("Jack") // age = 0 789 | val c = Person(1995, "Lynne") // age = 23 790 | ``` 791 | 792 | 请注意,如果一个类具有主构造函数,那么无法在不提供任何参数的情况下创建其实例(除非其中一个次构造函数是无参数的)。 793 | 794 | 795 | ### Setter 与 Getter 796 | 797 | 属性实际上是一个 _幕后字段_(对象内部为隐藏变量的种类)与两个访问器函数:一个用于获取变量的值,另一个用于设置值。可以重写一个或两个访问器(未被重写的访问器会自动获得默认行为,即直接返回或设置幕后字段)。在访问器内部,可以使用 `field` 引用幕后字段。Setter 访问器必须采用参数 `value`,这是赋值给属性的值。一个 Getter 主体可以是一个以 `=` 开头的单行表达式,也可以是一个花括号括起来的更复杂的主体,而 Setter 主体通常包括一个赋值,因此必须括在花括号中。如果要验证年龄是否为负数: 798 | 799 | ```kotlin 800 | class Person(age: Int) { 801 | var age = 0 802 | set(value) { 803 | if (value < 0) throw IllegalArgumentException( 804 | "Age cannot be negative") 805 | field = value 806 | } 807 | 808 | init { 809 | this.age = age 810 | } 811 | } 812 | ``` 813 | 814 | 烦人的是,初始化未调用 Setter 逻辑,而是直接设置了幕后字段——这就是为什么在此示例中必须使用初始化块来验证新创建的 Person 也不会得到负年龄的原因。请注意在初始化程序块中使用 `this.age` 以便区分同名属性和构造函数参数。 815 | 816 | 如果由于某种原因想要在幕后字段中存储与赋值给该属性值不同的值,那么可以自由地这样做,但是可能会希望使用 Getter 将调用代码返回给它们期望的结果:如果在 Setter 中声明 `field = value * 2`,那么在初始化块中声明 `this.age = age * 2`,那么还应该有 `get() = field / 2`。 817 | 818 | 还可以创建实际上没有幕后字段的属性,而只需引用另一个属性: 819 | 820 | ```kotlin 821 | val isNewborn 822 | get() = age == 0 823 | ``` 824 | 825 | 请注意,尽管由于使用 `val` 声明了它是一个只读属性(在这种情况下,可能没有提供 Setter),但它的值仍然可以更改,因为它是从可变属性中读取的——只是无法给该属性赋值。另外,请注意,属性类型是根据 Getter 的返回值推断出来的。 826 | 827 | 访问器前面的缩进是由于约定;像 Kotlin 的其他地方一样,它没有语法意义。编译器可以知道哪些访问器属于哪些属性,因为访问器的唯一合法位置是在属性声明之后(并且最多可以有一个 Getter 与一个 Setter)——因此无法拆分属性声明与访问器声明。然而,访问器的顺序并不重要。 828 | 829 | 830 | ### 成员函数 831 | 832 | 在类内部声明的函数称为该类的 _成员函数_。像在 Python 中一样,成员函数的每次调用都必须在类的实例上执行,并且该实例将在函数执行期间可用——但与 Python 不同的是,函数签名并未声明:没有明确的 `self` 参数。相反,每个成员函数都可以使用关键字 `this` 引用当前实例,而无需声明它。与 Python 不同,只要与名称相同的参数或局部变量没有名称冲突,`this` 就可以省略。如果在具有 `name` 属性的 `Person` 类中执行此操作: 833 | 834 | ```kotlin 835 | fun present() { 836 | println("Hello, I'm $name!") 837 | } 838 | ``` 839 | 840 | 然后可以这样做: 841 | 842 | ```kotlin 843 | val p = Person("Claire") 844 | p.present() // 输出 "Hello, I'm Claire!" 845 | ``` 846 | 847 | 可能已经写过 `${this.name}`,但这是多余的,通常不建议使用。可以使用 `=` 声明单行函数: 848 | 849 | ```kotlin 850 | fun greet(other: Person) = println("Hello, ${other.name}, I'm $name!") 851 | ``` 852 | 853 | 除了将实例自动传递到 `this` 之外,成员函数通常的作用类似于普通函数。 854 | 855 | 由于对象的成员函数集被限制为恰好是在编译时在对象的类与基类中声明的成员函数集,在运行时无法向对象或类添加新的成员函数。所以,例如 `p.leave = fun() { println("Bye!") }` 或其他任何形式都无法通过编译。 856 | 857 | 成员函数名应该使用 `lowerCamelCase`(小写驼峰命名)而不是 `snake_case`(下划线命名)。 858 | 859 | 860 | ### Lateinit 861 | 862 | Kotlin 要求在实例构建过程中初始化每个成员属性。有时,类的使用方式使构造函数没有足够的信息来初始化所有属性(例如,在生成构建器类或使用基于属性的依赖注入时)。为了不必使这些属性可为空,可以使用 _后期初始化的属性_: 863 | 864 | ```kotlin 865 | lateinit var name: String 866 | ``` 867 | 868 | Kotlin 将允许声明该属性而无需初始化它,并且可以在构造后的某个时候(直接或通过函数)设置属性值。类本身及其用户都有责任注意在设置属性之前不要读取该属性,并且 Kotlin 允许编写读取 `name` 的代码,就像它是一个普通的,不可为空的属性一样。但是,编译器无法强制正确使用,因此,如果在设置属性之前先读取该属性,将在运行时抛出 `UninitializedPropertyAccessException`。 869 | 870 | 在声明了 Lateinit 属性的类中,可以检测它是否已初始化: 871 | 872 | ```kotlin 873 | if (::name.isInitialized) println(name) 874 | ``` 875 | 876 | `lateinit` 只能与 `var` 一起使用,而不能与 `val` 一起使用,并且类型必须是非基本且不可空的。 877 | 878 | 879 | ### 中缀函数 880 | 881 | 可以指定单个参数成员函数或[扩展函数](#扩展函数属性)以用作中缀运算符,这在设计 DSL 时很有用。左操作数将变为 `this`,而右操作数将变为参数。如果在具有 `name` 属性的 `Person` 类中执行此操作: 882 | 883 | ```kotlin 884 | infix fun marry(spouse: Person) { 885 | println("$name and ${spouse.name} are getting married!") 886 | } 887 | ``` 888 | 889 | 现在,可以执行此操作(但是仍然可以按正常方式调用该函数): 890 | 891 | ```kotlin 892 | val lisa = Person("Lisa") 893 | val anne = Person("Anne") 894 | lisa marry anne // 输出 "Lisa and Anne are getting married!" 895 | ``` 896 | 897 | 所有中缀函数具有相同的[优先级](https://www.kotlincn.net/docs/reference/grammar.html#precedence)(与所有内置中缀函数共享,例如位运算 `and`、`or`、`inv` 等):低于算术运算符与 `..` 区间运算符,但高于 Elvis 运算符 `?:`、比较、逻辑运算符和赋值。 898 | 899 | 900 | ### 操作符 901 | 902 | Kotlin 语法可识别的大多数操作符都有预定义的文本名称,可在类中实现,就像使用 Python 的双下划线操作符名称一样。例如,二进制 `+` 操作符称为 `plus`。与中缀实例类似,如果在具有 `name` 属性的 `Person` 类中执行此操作: 903 | 904 | ```kotlin 905 | operator fun plus(spouse: Person) { 906 | println("$name and ${spouse.name} are getting married!") 907 | } 908 | ``` 909 | 910 | 使用中缀实例中的 `lisa` 与 `anne`,现在可以执行以下操作: 911 | 912 | ```kotlin 913 | lisa + anne // 输出 "Lisa and Anne are getting married!" 914 | ``` 915 | 916 | 一个特别有趣的操作符是函数调用括号对,其函数名称为 `invoke`——如果实现此功能,那么可以像使用函数一样调用类的实例。甚至可以重载它以提供不同的函数签名。 917 | 918 | `operator` 也可以用于某些其他预定义功能,以创建精美的效果,例如[属性委托](#属性委托)。 919 | 920 | 由于可用的操作符被硬编码到正式的 Kotlin 语法中,因此无法发明新的操作符,并且重写操作符不会影响其[优先级](https://www.kotlincn.net/docs/reference/grammar.html#precedence)。 921 | 922 | 923 | ### 枚举类 924 | 925 | 每当想要一个只能包含有限数量的值的变量,而每个值的唯一特征是与所有其他值都不同时,那么可以创建一个 _枚举类_: 926 | 927 | ```kotlin 928 | enum class ContentKind { 929 | TOPIC, 930 | ARTICLE, 931 | EXERCISE, 932 | VIDEO, 933 | } 934 | ``` 935 | 936 | 该类有四个实例,名为 `ContentKind.TOPIC` 等。可以使用 `==` 与 `!=` 将该类的实例相互比较,并且可以通过 `ContentKind.values()` 获得所有允许的值。如果需要,还可以为每个实例提供更多信息: 937 | 938 | ```kotlin 939 | enum class ContentKind(val kind: String) { 940 | TOPIC("Topic"), 941 | ARTICLE("Article"), 942 | EXERCISE("Exercise"), 943 | VIDEO("Video"), 944 | ; 945 | 946 | override fun toString(): String { 947 | return kind 948 | } 949 | } 950 | ``` 951 | 952 | 通常会强制执行空安全,因此与 Java 不同,`ContentKind` 类型的变量不能为 null。 953 | 954 | 955 | ### 数据类 956 | 957 | 通常——尤其是想要从函数的复杂返回类型或 Map 的复杂键——将需要一个糙快猛的类,该类仅包含一些属性,但对于相等性仍可比较,并且可用作 Map 键。如果创建 _数据类_,那么将自动实现以下函数:`toString()`(将产生包含所有属性名称和值的字符串)、`equals()`(将按属性进行 `equals()`)、`hashCode()`(将散列各个属性并组合散列)以及使 Kotlin 将类的实例解构为声明所需的函数(`component1()`、`component2()` 等): 958 | 959 | ```kotlin 960 | data class ContentDescriptor(val kind: ContentKind, val id: String) { 961 | override fun toString(): String { 962 | return kind.toString() + ":" + id 963 | } 964 | } 965 | ``` 966 | 967 | 968 | ## 异常 969 | 970 | 971 | ### 抛出与捕获 972 | 973 | 异常几乎像在 Python 中一样工作。使用 `throw` 将 _抛出_ 一个异常: 974 | 975 | ```kotlin 976 | throw IllegalArgumentException("Value must be positive") 977 | ``` 978 | 979 | 然后使用 `try`/`catch` 来 _捕获_ 异常(对应 Python 的 `try`/`except`): 980 | 981 | ```kotlin 982 | fun divideOrZero(numerator: Int, denominator: Int): Int { 983 | try { 984 | return numerator / denominator 985 | } catch (e: ArithmeticException) { 986 | return 0 987 | } 988 | } 989 | ``` 990 | 991 | 依次尝试 `catch` 代码块,直到找到与抛出的异常匹配的异常类型(无需精准匹配;抛出的异常的类可以是已声明异常的子类),并且最多包含一个 `catch` 代码块将被执行。如果没有找到匹配项,那么异常会从 `try`/`catch` 中冒出。 992 | 993 | 无论结果如何,都将在最后执行 `finally` 代码块(如果有的话):在 try 代码块成功执行之后,或者在 catch 代码块执行之后(即使 catch 块引发了另一个异常),或者找不到匹配的捕获。 994 | 995 | 与 Python 不同,`try`/`catch` 是一个表达式:`try` 代码块(如果成功)或所选的 `catch` 代码块的最后一个表达式将成为结果值(`finally` 不会影响结果),因此可以将上面的函数体重构为: 996 | 997 | ```kotlin 998 | return try { 999 | numerator / denominator 1000 | } catch (e: ArithmeticException) { 1001 | 0 1002 | } 1003 | ``` 1004 | 1005 | 基本异常类是 `Throwable`(但是扩展其子类 `Exception` 更为常见),并且有大量内置的异常类。如果找不到满足需求的异常类,那么可以通过从现有异常类继承来创建自己的异常类。 1006 | 1007 | 请注意,除了与 Java 代码进行交互时,在 Kotlin 中不建议使用异常。与其在自己的代码中引发异常,不如考虑使用特殊的返回类型,例如 [Arrow 库](https://arrow-kt.io/)中的 [Either](https://arrow-kt.io/docs/apidocs/arrow-core-data/arrow.core/-either/)。 1008 | 1009 | 1010 | ### Nothing 1011 | 1012 | `throw` 也是一个表达式,其返回类型是特殊类 `Nothing`,它没有任何实例。编译器知道类型为 `Nothing` 的表达式永远不会正常返回,因此即使通常需要使用其他类型(例如在 [Elvis 操作符](#elvis-操作符)之后)的情况下,也通常会接受其使用。如果创建一个始终抛出异常的函数,或者开始一个无限循环,那么可以将其返回类型声明为 `Nothing`,以使编译器意识到这一点。一个有趣的例子是内置函数 `TODO`,可以在任何表达式中调用它(可能提供一个字符串参数),它会引发 `NotImplementedError`。 1013 | 1014 | 可为空版本 `Nothing?` 在当使用 null 初始化某些内容且没有其他类型信息时,编译器将使用它。在 `val x = null` 中,`x` 的类型将为 `Nothing?`。此类型没有“从不正常返回”的语义;相反,编译器知道该值将始终为 null。 1015 | 1016 | 1017 | ## 空安全 1018 | 1019 | 1020 | ### 使用空值 1021 | 1022 | A variable that doesn't refer to anything refers to `null` (or you can say that the variable "is null"). As opposed to `None` in Python, `null` is not an object - it's just a keyword that is used to make a variable refer to nothing or to check if it does (that check must be performed with `==` or `!=`). Because nulls are a frequent source of programming errors, Kotlin encourages avoiding them as much as possible - a variable cannot actually be null unless it's been declared to allow for null, which you do by suffixing the type name with `?`. For example: 1023 | 1024 | ```kotlin 1025 | fun test(a: String, b: String?) { 1026 | } 1027 | ``` 1028 | 1029 | The compiler will allow this function to be called as e.g. `test("a", "b")` or `test("a", null)`, but not as `test(null, "b")` or `test(null, null)`. Calling `test(a, b)` is only allowed if the compiler can prove that `a` cannot possibly be null. Inside of `test`, the compiler will not allow you to do anything with `b` that would result in an exception if `b` should happen to be null - so you can do `a.length`, but not `b.length`. However, once you're inside a conditional where you have checked that `b` is not null, you can do it: 1030 | 1031 | ```kotlin 1032 | if (b != null) { 1033 | println(b.length) 1034 | } 1035 | ``` 1036 | 1037 | Or: 1038 | 1039 | ```kotlin 1040 | if (b == null) { 1041 | // Can't use members of b in here 1042 | } else { 1043 | println(b.length) 1044 | } 1045 | ``` 1046 | 1047 | Making frequent null checks is annoying, so if you have to allow for the possibility of nulls, there are several very useful operators in Kotlin to ease working with values that might be null, as described below. 1048 | 1049 | 1050 | ### 安全调用操作符 1051 | 1052 | `x?.y` evaluates `x`, and if it is not null, it evaluates `x.y` (without reevaluating `x`), whose result becomes the result of the expression - otherwise, you get null. This also works for functions, and it can be chained - for example, `x?.y()?.z?.w()` will return null if any of `x`, `x.y()`, or `x.y().z` produce null; otherwise, it will return the result of `x.y().z.w()`. 1053 | 1054 | 1055 | ### Elvis 操作符 1056 | 1057 | `x ?: y` evaluates `x`, which becomes the result of the expression unless it's null, in which case you'll get `y` instead (which ought to be of a non-nullable type). This is also known as the "Elvis operator". You can even use it to perform an early return in case of null: 1058 | 1059 | ```kotlin 1060 | val z = x ?: return y 1061 | ``` 1062 | 1063 | This will assign `x` to `z` if `x` is non-null, but if it is null, the entire function that contains this expression will stop and return `y` (this works because `return` is also an expression, and if it is evaluated, it evaluates its argument and then makes the containing function return the result). 1064 | 1065 | 1066 | ### 非空断言操作符 1067 | 1068 | Sometimes, you're in a situation where you have a value `x` that you know is not null, but the compiler doesn't realize it. This can legitimately happen when you're interacting with Java code, but if it happens because your code's logic is more complicated than the compiler's ability to reason about it, you should probably restructure your code. If you can't convince the compiler, you can resort to saying `x!!` to form an expression that produces the value of `x`, but whose type is non-nullable: 1069 | 1070 | ```kotlin 1071 | val x: String? = javaFunctionThatYouKnowReturnsNonNull() 1072 | val y: String = x!! 1073 | ``` 1074 | 1075 | It can of course be done as a single expression: `val x = javaFunctionThatYouKnowReturnsNonNull()!!`. 1076 | 1077 | `!!` will will raise a `NullPointerException` if the value actually is null. So you could also use it if you really need to call a particular function and would rather have an exception if there's no object to call it on (`maybeNull!!.importantFunction()`), although a better solution (because an NPE isn't very informational) is this: 1078 | 1079 | ```kotlin 1080 | val y: String = x ?: throw SpecificException("Useful message") 1081 | y.importantFunction() 1082 | ``` 1083 | 1084 | The above could also be a oneliner - and note that the compiler knows that because the `throw` will prevent `y` from coming into existence if `x` is null, `y` must be non-null if we reach the line below. Contrast this with `x?.importantFunction()`, which is a no-op if `x` is null. 1085 | 1086 | 1087 | ## 函数式编程 1088 | 1089 | 1090 | ### 函数类型 1091 | 1092 | 像在 Python 中一样,Kotlin 中的函数是一等值——它们可以赋值给变量并作为参数传递。函数的类型是 _function type_,用括号括起来的参数类型列表和返回类型的箭头指示。参考以下函数: 1093 | 1094 | ```kotlin 1095 | fun safeDivide(numerator: Int, denominator: Int) = 1096 | if (denominator == 0) 0.0 else numerator.toDouble() / denominator 1097 | ``` 1098 | 1099 | 它带有两个 `Int` 参数并返回 `Double`,因此其类型为 `(Int, Int) -> Double`。可以通过在函数名称前加上 `::` 来引用函数本身,并且可以将其赋值给变量(通常会推断出其类型,但为了演示将显示类型签名): 1100 | 1101 | ```kotlin 1102 | val f: (Int, Int) -> Double = ::safeDivide 1103 | ``` 1104 | 1105 | 当具有函数类型的变量或参数(有时称为 _函数引用_)时,可以像调用普通函数一样对其进行调用,这将导致引用的函数被调用: 1106 | 1107 | ```kotlin 1108 | val quotient = f(3, 0) 1109 | ``` 1110 | 1111 | 类有可能像执行接口一样实现函数类型。然后,它必须提供一个具有给定签名的称为 `invoke` 的运算符函数,然后可以将该类的实例赋值给该函数类型的变量: 1112 | 1113 | ```kotlin 1114 | class Divider : (Int, Int) -> Double { 1115 | override fun invoke(numerator: Int, denominator: Int): Double = ... 1116 | } 1117 | ``` 1118 | 1119 | 1120 | ### 函数字面值:lambda 表达式与匿名函数 1121 | 1122 | 像在 Python 中一样,可以编写 _lambda 表达式_:使用非常紧凑的语法声明并编写匿名函数,它计算可调用函数对象的值。在 Kotlin 中,lambdas 可以包含多个语句,这使得它们对于比 Python 的单表达式 lambdas 在处理[更复杂的任务](#接收者)时更有用。最后一个语句必须是一个表达式,它的结果将成为 lambda 的返回值(除非 `Unit` 是 lambda 表达式所赋值的变量或参数的返回类型,在这种情况下,lambda 没有返回值)。一个 lambda 表达式包含在花括号中,它首先列出了它的参数名和可能的类型(除非可以从上下文中推断出类型): 1123 | 1124 | ```kotlin 1125 | val safeDivide = { numerator: Int, denominator: Int -> 1126 | if (denominator == 0) 0.0 else numerator.toDouble() / denominator 1127 | } 1128 | ``` 1129 | 1130 | `safeDivide` 的类型是 `(Int, Int) -> Double`。请注意,与函数类型声明不同,lambda 表达式的参数列表不得包含在括号中。 1131 | 1132 | 请注意,Kotlin 中花括号的其他用法(例如在函数和类定义中以及在 `if`、`else`、`for`、`while` 语句之后)不是 lambda 表达式(因此,`if` 是有条件地执行 lambda 函数的函数的情况 _并非_ 如此)。 1133 | 1134 | Lambda 表达式的返回类型是根据其中的最后一个表达式的类型(或从 Lambda 表达式所赋值给的变量或参数的函数类型)推断出来的。如果将 lambda 表达式作为函数参数(通常使用)传递或赋值给具有声明类型的变量,那么 Kotlin 也可以推断参数类型,只需要指定其名称即可: 1135 | 1136 | ```kotlin 1137 | val safeDivide: (Int, Int) -> Double = { numerator, denominator -> 1138 | if (denominator == 0) 0.0 else numerator.toDouble() / denominator 1139 | } 1140 | ``` 1141 | 1142 | 或: 1143 | 1144 | ```kotlin 1145 | fun callAndPrint(function: (Int, Int) -> Double) { 1146 | println(function(2, 0)) 1147 | } 1148 | 1149 | callAndPrint({ numerator, denominator -> 1150 | if (denominator == 0) 0.0 else numerator.toDouble() / denominator 1151 | }) 1152 | ``` 1153 | 1154 | 无参数 lambda 不需要箭头。单参数 lambda 可以选择省略参数名称和箭头,在这种情况下,该参数可通过 `it` 调用: 1155 | 1156 | ```kotlin 1157 | val square: (Double) -> Double = { it * it } 1158 | ``` 1159 | 1160 | 如果函数的最后一个参数的类型是函数类型,并且想提供 lambda 表达式,那么可以将 lambda 表达式放在参数括号之外。如果 lambda 表达式是唯一的参数,那么可以完全省略括号。这对于[构建 DSL](#接收者) 非常有用。 1161 | 1162 | ```kotlin 1163 | fun callWithPi(function: (Double) -> Double) { 1164 | println(function(3.14)) 1165 | } 1166 | 1167 | callWithPi { it * it } 1168 | ``` 1169 | 1170 | 如果想更清楚地了解创建函数的事实,可以创建一个 _匿名函数_,该函数仍然是表达式而不是声明: 1171 | 1172 | ```kotlin 1173 | callWithPi(fun(x: Double): Double { return x * x }) 1174 | ``` 1175 | 1176 | 或: 1177 | 1178 | ```kotlin 1179 | callWithPi(fun(x: Double) = x * x) 1180 | ``` 1181 | 1182 | Lambda 表达式和匿名函数统称为 _函数字面值_。 1183 | 1184 | 1185 | ### 集合推导 1186 | 1187 | Kotlin 可以非常接近 Python 的 `list`、`dict`、`set` 理解的紧凑性。假设 `people` 是具有 `name` 属性的 `Person` 对象的集合: 1188 | 1189 | ```kotlin 1190 | val shortGreetings = people 1191 | .filter { it.name.length < 10 } 1192 | .map { "Hello, ${it.name}!" } 1193 | ``` 1194 | 1195 | 相当于 1196 | 1197 | ```python 1198 | short_greetings = [ 1199 | f"Hello, {p.name}" # In Python 2, this would be: "Hello, %s!" % p.name 1200 | for p in people 1201 | if len(p.name) < 10 1202 | ] 1203 | ``` 1204 | 1205 | 在某些方面,这更易于阅读,因为操作是按照它们应用于值的顺序指定的。结果将是一个不变的 `List`,其中 `T` 是使用的转换(在这种情况下为 `String`)生成的任何类型。如果需要可变列表,请在最后调用 `toMutableList()`。如果需要 Set,请在最后调用 `toSet()` 或 `toMutableSet()`。如果要将 Set 转换为 Map,请调用 `associateBy()`,它需要两个 lambda,用于指定如何从每个元素提取键和值:`people.associateBy({it.ssn}, {it.name})`(如果希望整个元素作为值,那么可以省略第二个 lambda;如果希望结果可变,那么可以在最后调用 `toMutableMap()`)。 1206 | 1207 | 这些转换也可以应用于 `Sequence`,它与 Python 的生成器类似,并且允许进行惰性求值。如果有一个庞大的列表,并且想要延迟处理它,那么可以在其上调用 `asSequence()`。 1208 | 1209 | [`kotlin.collections` 包](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/)中提供了大量函数式编程风格的操作。 1210 | 1211 | 1212 | ### 接收者 1213 | 1214 | 成员函数或[扩展函数](#扩展函数属性)的签名始于 _接收者_:可以在其上调用函数的类型。例如,`toString()` 的签名是 `Any.() -> String`——可以在任何非空对象(接收者)上调用它,它不带任何参数,并且返回 `String`。可以使用这样的签名来编写 lambda 函数——这被称为 _带有接收者的函数字面值_,对于构建DSL非常有用。 1215 | 1216 | 带接收者的函数文字可能最容易被认为是 lambda 表达式形式的扩展函数。该声明看起来像一个普通的 lambda 表达式。使其成为接收者的是上下文——必须将其传递给以接收者作为参数的函数,或者将其赋值给类型为接收者的函数类型的变量或属性。将函数与接收者一起使用的唯一方法是在接收者类的实例上调用它,就像它是成员函数或扩展函数一样。例如: 1217 | 1218 | ```kotlin 1219 | class Car(val horsepowers: Int) 1220 | 1221 | val boast: Car.() -> String = { "I'm a car with $horsepowers HP!"} 1222 | 1223 | val car = Car(120) 1224 | println(car.boast()) 1225 | ``` 1226 | 1227 | 在带有接收者的 lambda 表达式中,可以使用 `this` 来引用接收者对象(在本例中为 `car`)。像往常一样,如果没有命名冲突,那么可以省略 `this`,这就是为什么可以简单地说 `$horsepowers` 而不是 `${this.horsepowers}` 的原因。因此请注意,在 Kotlin 中,`this` 取决于上下文可能具有不同的含义:如果在内部(可能嵌套的)lambda 表达式与接收者一起使用,它指的是最内部包含接收者的 lambda 表达式的接收者对象。如果需要“突破”函数文字并获取“原始”`this`(正在其中执行的成员函数的实例),请在 `this@` 之后提及包含的类名——如果在函数字面量内,而接收方在 Car 的成员函数内,请使用 `this@Car`。 1228 | 1229 | 与其他函数字面值一样,如果函数采用一个参数(调用该参数的接收方对象除外),那么除非声明另一个名称,否则单个参数将隐式称为 `it`。如果使用多个参数,那么必须声明其名称。 1230 | 1231 | 这是一个用于构建树形结构的小型 DSL 示例: 1232 | 1233 | ```kotlin 1234 | class TreeNode(val name: String) { 1235 | val children = mutableListOf() 1236 | 1237 | fun node(name: String, initialize: (TreeNode.() -> Unit)? = null) { 1238 | val child = TreeNode(name) 1239 | children.add(child) 1240 | if (initialize != null) { 1241 | child.initialize() 1242 | } 1243 | } 1244 | } 1245 | 1246 | fun tree(name: String, initialize: (TreeNode.() -> Unit)? = null): TreeNode { 1247 | val root = TreeNode(name) 1248 | if (initialize != null) { 1249 | root.initialize() 1250 | } 1251 | return root 1252 | } 1253 | 1254 | val t = tree("root") { 1255 | node("math") { 1256 | node("algebra") 1257 | node("trigonometry") 1258 | } 1259 | node("science") { 1260 | node("physics") 1261 | } 1262 | } 1263 | ``` 1264 | 1265 | 在 `tree("root")` 之后的块是带有接收者的第一个函数字面值,它将作为 `initialize` 参数传递给 `tree()`。根据 `tree()` 的参数列表,接收者的类型为 `TreeNode`,因此,`tree()` 可以在 `root` 上调用 `initialize()`。然后,`root` 在该 lambda 表达式的范围内变为 `this`,因此,当调用 `node("math")` 时,它隐式地表示为 `this.node("math")`,其中 `this` 与 `root` 所指的是相同的 `TreeNode`。下一个块传递给 `TreeNode.node()`,并在 `root` 节点的第一个子节点上调用,即 `math`,在其内部,`this` 将引用 `math`。 1266 | 1267 | 如果想在 Python 中表达相同的内容,它将看起来像这样,而 lambda 函数只能包含一个表达式将会受阻,所以需要显式的函数定义来处理除单行之外的所有内容 1268 | 1269 | ```python 1270 | class TreeNode: 1271 | def __init__(self, name): 1272 | self.name = name 1273 | self.children = [] 1274 | 1275 | def node(self, name, initialize=None): 1276 | child = TreeNode(name) 1277 | self.children.append(child) 1278 | if initialize: 1279 | initialize(child) 1280 | 1281 | def tree(name, initialize=None): 1282 | root = TreeNode(name) 1283 | if initialize: 1284 | initialize(root) 1285 | return root 1286 | 1287 | def init_root(root): 1288 | root.node("math", init_math) 1289 | root.node("science", 1290 | lambda science: science.node("physics")) 1291 | 1292 | def init_math(math): 1293 | math.node("algebra") 1294 | math.node("trigonometry") 1295 | 1296 | t = tree("root", init_root) 1297 | ``` 1298 | 1299 | 官方文档还有一个非常酷的示例,其中包含[用于构造 HTML 文档的 DSL](https://www.kotlincn.net/docs/type-safe-builders.html)。 1300 | 1301 | 1302 | ### 内联函数 1303 | 1304 | Lambda 函数有一些运行时开销:它们实际上是对象,因此必须实例化,并且(与其他函数一样)调用它们也需要一点时间。如果在函数上使用 `inline` 关键字,那么会告诉编译器内联函数和其 lambda 参数(如果有的话)——也就是说,编译器会将函数的代码(及其 lambda 参数)复制到每个调用处,这样就消除了 lambda 实例化以及函数和 lambda 调用的开销。这将无条件地发生,这与 C 和 C++ 中的 `inline` 更多地是对编译器的提示不同。这将导致已编译代码的大小增加,但是对于某些较小但经常调用的函数可能值得这样做。 1305 | 1306 | ```kotlin 1307 | inline fun time(action: () -> Unit): Long { 1308 | val start = Instant.now().toEpochMilli() 1309 | action() 1310 | return Instant.now().toEpochMilli() - start 1311 | } 1312 | ``` 1313 | 1314 | 现在,如果这样做: 1315 | 1316 | ```kotlin 1317 | val t = time { println("Lots of code") } 1318 | println(t) 1319 | ``` 1320 | 1321 | 编译器将生成类似以下内容的代码(除了 `start` 不会与任何其他同名标识符冲突): 1322 | 1323 | ```kotlin 1324 | val start = Instant.now().toEpochMilli() 1325 | println("Lots of code") 1326 | val t = Instant.now().toEpochMilli() - start 1327 | println(t) 1328 | ``` 1329 | 1330 | 在内联函数定义中,可以在任何函数类型的参数前面使用 `noinline` 来防止将要传递给它的 lambda 内联。 1331 | 1332 | 1333 | ### 不错的工具函数 1334 | 1335 | 1336 | #### `run()`、`let()` 与 `with()` 1337 | 1338 | 如果想在可能为空的东西上调用函数,`?.` 很好。但是,如果要调用一个采用非空参数的函数,但要为该参数传递的值可能为空怎么办?尝试 `run()`,它是 `Any?` 上的扩展函数,该函数以带有接收者的 lambda 作为参数,并在其调用的值上调用它,而用 `?.` 来调用 `run()` 仅当该对象非空时才会调用: 1339 | 1340 | ```kotlin 1341 | val result = maybeNull?.run { functionThatCanNotHandleNull(this) } 1342 | ``` 1343 | 1344 | 如果 `maybeNull` 为空,那么不会调用该函数,而 `result` 为空。否则,它将是 `functionThatCanNotHandleNull(this)` 的返回值,其中 `this` 是指 `maybeNull`。可以使用 `?.` 链接 `run()` 调用——如果前一个结果不为空,那么每个 `run()` 都会调用: 1345 | 1346 | ```kotlin 1347 | val result = maybeNull 1348 | ?.run { firstFunction(this) } 1349 | ?.run { secondFunction(this) } 1350 | ``` 1351 | 1352 | 第一个 `this` 是指 `maybeNull`,第二个是 `firstFunction()` 的结果,`result` 将是 `secondFunction()` 的结果(如果 `maybeNull` 或任何中间结果为空,那么返回空)。 1353 | 1354 | `run()` 的语法变体是 `let()`,它以普通函数类型而不是带有接收者的函数类型作为参数,因此可能为空的表达式将称为 `it` 而不是 `this`。 。 1355 | 1356 | 如果有一个需要多次使用的表达式,但是不必为它提供一个变量名并进行空检测,`run()` 与 `let()` 都非常有用: 1357 | 1358 | ```kotlin 1359 | val result = someExpression?.let { 1360 | firstFunction(it) 1361 | it.memberFunction() + it.memberProperty 1362 | } 1363 | ``` 1364 | 1365 | 还有一个版本是 `with()`,也可以使用它来避免为表达式提供变量名,但前提是知道其结果不为空: 1366 | 1367 | ```kotlin 1368 | val result = with(someExpression) { 1369 | firstFunction(this) 1370 | memberFunction() + memberProperty 1371 | } 1372 | ``` 1373 | 1374 | 在最后一行,在 `memberFunction()` 与 `memberProperty` 之前都有一个隐含的`this.`(如果这些存在于 `someExpression` 类型)。返回值是最后一个表达式的值。 1375 | 1376 | 1377 | #### `apply()` 与 `also()` 1378 | 1379 | 如果不关心函数的返回值,但是想进行一个或多个涉及空值的调用,然后继续使用该值,请尝试 `apply()`,它返回被调用的值。如果要使用所讨论对象的许多成员,这特别有用: 1380 | 1381 | ```kotlin 1382 | maybeNull?.apply { 1383 | firstFunction(this) 1384 | secondFunction(this) 1385 | memberPropertyA = memberPropertyB + memberFunctionA() 1386 | }?.memberFunctionB() 1387 | ``` 1388 | 1389 | 在 `apply` 块中,`this` 是指 `maybeNull`。在 `memberPropertyA`,`memberPropertyB` 与 `memberFunctionA` 之前有一个隐含的 `this`(除非这些在 `maybeNull` 上不存在,在这种情况下将在包含的作用域中查找它们)。此后,也可以在 `maybeNull` 上调用 `memberFunctionB()`。 1390 | 1391 | 如果发现 `this` 语法令人困惑,那么可以改用 `also`,它以普通的 lambda 作为参数: 1392 | 1393 | ```kotlin 1394 | maybeNull?.also { 1395 | firstFunction(it) 1396 | secondFunction(it) 1397 | it.memberPropertyA = it.memberPropertyB + it.memberFunctionA() 1398 | }?.memberFunctionB() 1399 | ``` 1400 | 1401 | 1402 | #### `takeIf()` 与 `takeUnless()` 1403 | 1404 | 如果仅在满足特定条件时才使用值,请尝试 `takeIf()`,如果满足给定谓词,那么返回它被调用的值,否则返回空值。还有 `takeUnless()`,其逻辑正好相反。可以在其后接一个 `?.`,以仅在满足谓词的情况下对该值执行运算。下面,计算某些表达式的平方,但前提是表达式的值至少为 42: 1405 | 1406 | ```kotlin 1407 | val result = someExpression.takeIf { it >= 42 } ?.let { it * it } 1408 | ``` 1409 | 1410 | 1411 | ## 包与导入 1412 | 1413 | 1414 | ### 包 1415 | 1416 | 每个 Kotlin 文件都应属于一个 _包_。这有点类似于 Python 中的模块,但是文件需要显式声明它们属于哪个包,并且每当任何文件声明自己属于该包时,就隐式地存在一个包(与使用 `__init__.py` 显式定义一个模块相反)。并使该目录中的所有文件隐式属于该模块)。软件包声明必须放在文件顶部: 1417 | 1418 | ```kotlin 1419 | package content.exercises 1420 | ``` 1421 | 1422 | 如果文件没有声明包,那么它属于无名 _默认包_。应该避免这种情况,因为在命名冲突的情况下,这将使引用该文件中的符号变得困难(不能显式导入空包)。 1423 | 1424 | 程序包名称通常与目录结构相对应——请注意,源文件名 _不_ 应该是程序包名称的一部分(因此,如果遵循此名称,那么文件级符号名在整个目录中必须唯一,而不仅仅是在文件中)。但是,这种对应关系不是必需的,因此,如果要与 Java 代码进行互操作,并且所有包名称都必须以相同的前缀开头,例如:`org.khanacademy`,可能会发现不需要将所有代码都放在 `org/khanacademy` 中(这是 Java 会强迫执行的操作)而感到宽慰,——相反,可以从例如名为 `content` 的目录开始。并且其中的文件可以声明它们属于软件包 `org.khanacademy.content`。但是,如果有一个同时包含 Kotlin 与 Java 代码的项目,那么约定也将 Java 风格的软件包目录也用于 Kotlin 代码。 1425 | 1426 | 尽管这些点表明程序包彼此嵌套,但从语言角度来看实际上并非如此。虽然最好组织代码以使诸如 `content.exercises` 与 `content.articles` 之类的 `content` 的“子包”都包含与内容相关的代码,但是从语言的角度来看,这三个包是无关的。但是,如果使用 _模块_(由构建系统定义),通常所有“子包”都放在同一个模块中,在这种情况下,带有 [`internal` 可见性](#可见性修饰符) 的符号在各个子包中都是可见的。 1427 | 1428 | 程序包名称通常只包含小写字母(没有下划线)和分隔点。 1429 | 1430 | 1431 | ### 导入 1432 | 1433 | 为了使用包中的内容,只需在使用符号的地方使用包名称来完全限定符号的名称即可: 1434 | 1435 | ```kotlin 1436 | val exercise = content.exercises.Exercise() 1437 | ``` 1438 | 1439 | 这很快变得很笨拙,因此通常将 _导入_ 所需的符号。可以导入特定的符号: 1440 | 1441 | ```kotlin 1442 | import content.exercises.Exercise 1443 | ``` 1444 | 1445 | 或一个完整的包装,它将带入该包中的所有符号: 1446 | 1447 | ```kotlin 1448 | import content.exercises.* 1449 | ``` 1450 | 1451 | 无论使用哪个版本的导入,现在都可以执行以下操作: 1452 | 1453 | ```kotlin 1454 | val exercise = Exercise() 1455 | ``` 1456 | 1457 | 如果存在命名冲突,通常应该只导入其中一个符号,并完全限定另一个符号的用法。如果两者都被大量使用,那么可以在导入时重命名符号: 1458 | 1459 | ```kotlin 1460 | import content.exercises.Exercise as Ex 1461 | ``` 1462 | 1463 | 在 Kotlin 中,导入是一个编译期概念——导入内容实际上不会导致任何代码运行(与 Python 不同,在 Python 中,文件中的所有顶层语句都在导入时执行)。因此,允许循环导入,但是它们可能会在代码中提示设计问题。但是,在执行期间,将在首次引用类(或其任何属性或函数)时加载类,并且类加载会导致初始化[伴生对象](#伴生对象)——如果具有循环依赖项,那么可能导致运行时异常。 1464 | 1465 | 每个文件都隐式导入其自己的程序包以及许多内置的 Kotlin 与 Java 程序包。 1466 | 1467 | 1468 | ## 可见性修饰符 1469 | 1470 | Kotlin 允许通过 _可见性修饰符_(可以放置在符号声明上)强制执行符号可见性(Python 仅通过下划线约定来实现)。如果不提供可见性修饰符,那么会获得默认的 _可见性_ 级别,该级别为 _public_ 。 1471 | 1472 | 可见性修饰符的含义取决于它是应用于顶层声明还是应用于类内的声明。对于顶层声明: 1473 | 1474 | * `public`(或省略):此符号在整个代码库中可见 1475 | * `internal`:此符号仅在与声明该符号的文件属于同一 _模块_(由 IDE 或构建工具定义的源代码分组)的文件内部可见 1476 | * `private`:此符号仅在声明该符号的文件内部可见 1477 | 1478 | 例如,可以使用 `private` 来定义一个文件中的多个函数所需的属性或辅助函数,或者定义其中一个私有函数返回的复杂类型,而无需将其泄漏给代码库的其余部分: 1479 | 1480 | ```kotlin 1481 | private class ReturnType(val a: Int, val b: Double, val c: String) 1482 | 1483 | private fun secretHelper(x: Int) = x * x 1484 | 1485 | private const val secretValue = 3.14 1486 | ``` 1487 | 1488 | 对于在类内部声明的符号: 1489 | 1490 | * `public`(或省略):任何可以看到包含该符号的类的代码都可以看到该符号 1491 | * `internal`:该符号仅对在这样的文件内的代码可见,该文件与声明该符号的文件属于同一模块,并且还可以看到文件中包含的类 1492 | * `protected`:此符号仅在包含类及其所有子类中可见,无论它们在何处声明(因此,如果类是公有的且[开放](#子类化)的,那么任何人都可以对其进行子类化,从而查看并使用受保护的成员)。如果使用过 Java:这也 _不_ 会授予其余包的访问权限。 1493 | * `private`:此符号仅在包含的类中可见 1494 | 1495 | 构造函数也可以具有可见性修饰符。如果要在主要构造函数上放置一个(如果有许多次要构造函数,它们全部调用了不想公开的复杂主要构造函数,那么可能要这样做),需要包括 `constructor` 关键字:`class Person private constructor(val name: String)`。 1496 | 1497 | 可见性修饰符不能放在局部变量上,因为它们的可见性始终限于包含块。 1498 | 1499 | 属性的类型以及用于参数的类型和函数的返回类型,必须与属性/函数本身“至少一样可见”。例如,公有函数不能将私有类型作为参数。 1500 | 1501 | 可见性级别仅影响 _符号_ 的 _词法可见性_ ——也就是说,编译器允许键入符号。它不会影响 _实例_ 的使用位置:例如,公有顶层函数很可能会返回私有类的实例,只要返回类型没有提及私有类名称,而是它的公有基类即可。私有类(可能是 `Any`)或私有类实现的公有接口。当对某个子类进行[子类化](#子类化)时,子类也继承了它的私有成员,但是在该子类中不能直接访问它——但是,如果调用碰巧访问了私有成员的继承的公有函数,那也没关系。 1502 | 1503 | 1504 | ## 继承 1505 | 1506 | 1507 | ### 子类化 1508 | 1509 | Kotlin 支持单父类继承——因此,每个类(根类 `Any` 除外)都只有一个父类,称为 _超类_。Kotlin 需要仔细考虑类的设计,以确保对其进行 _子类化_ 实际上是安全的,因此,默认情况下类是 _关闭的_,除非明确声明该类为 _开放类_ 或 _抽象类_,否则无法继承。然后,可以通过声明一个新类来从该类中子类化,该新类在冒号后提及其父类: 1510 | 1511 | ```kotlin 1512 | open class MotorVehicle 1513 | class Car : MotorVehicle() 1514 | ``` 1515 | 1516 | 没有声明超类的类隐式地继承自 `Any`。子类必须调用基类的构造函数之一,并传递其自身构造函数的参数或常量值: 1517 | 1518 | ```kotlin 1519 | open class MotorVehicle(val maxSpeed: Double, val horsepowers: Int) 1520 | class Car( 1521 | val seatCount: Int, 1522 | maxSpeed: Double 1523 | ) : MotorVehicle(maxSpeed, 100) 1524 | ``` 1525 | 1526 | 子类 _继承_ 其超类中存在的所有成员——既直接在超类中定义的成员,也包括超类本身已继承的成员。在此示例中,`Car` 包含以下成员: 1527 | 1528 | * `seatCount`,这是 `Car` 的属性 1529 | * `maxSpeed` 与 `horsepowers`,继承自 `MotorVehicle` 1530 | * `toString()`、`equals()`、与 `hashCode()`,继承自 `Any` 1531 | 1532 | 请注意,术语“子类”和“超类”可以跨越多个继承级别——`Car` 是 `Any` 的子类,而 `Any` 是所有东西的超类。如果想要限制在一个继承级别,将说“直接子类”或“直接超类”。 1533 | 1534 | 请注意,不用在 `Car` 中的 `maxSpeed` 前面使用 `val`——这样做会在 `Car` 中引入一个独特的属性,从而 _覆盖_ 了从 `MotorVehicle` 继承的属性。如所写,它只是一个构造函数参数,将其传递给超级构造函数。 1535 | 1536 | `private` 成员(以及其他模块中超类的 `internal` 成员)也被继承,但不能直接访问:如果超类包含由公共函数 `bar()` 引用的私有属性 `foo`,那么子类的实例将包含 `foo`;不能直接使用它,但是可以调用 `bar()`。 1537 | 1538 | 构造子类的实例时,首先构造超类“part”(通过超类构造函数)。这意味着在执行打开类的构造函数期间,可能正在构造的对象是子类的实例,在这种情况下,子类特定的属性尚未初始化。因此,从构造函数中调用开放函数是有风险的:它可能在子类中被覆盖,并且如果它正在访问子类特定的属性,那么这些属性将不会被初始化。 1539 | 1540 | 1541 | ### 覆盖 1542 | 1543 | 如果成员函数或属性被声明为 `open`,那么子类可以通过提供新的实现 _覆盖_ 它。假设 `MotorVehicle` 声明了此函数: 1544 | 1545 | ```kotlin 1546 | open fun drive() = 1547 | "$horsepowers HP motor vehicle driving at $maxSpeed MPH" 1548 | ``` 1549 | 1550 | 如果 `Car` 不执行任何操作,它将按原样继承此函数,并且将返回一条消息,其中包含汽车的功率和最大速度。如果想要特定于汽车的消息,`Car` 可以通过使用 `override` 关键字重新声明该函数来覆盖该函数: 1551 | 1552 | ```kotlin 1553 | override fun drive() = 1554 | "$seatCount-seat car driving at $maxSpeed MPH" 1555 | ``` 1556 | 1557 | 覆盖的函数名必须与被覆盖的函数名完全匹配,但覆盖函数中的返回类型可以是被覆盖函数的返回类型的子类型。 1558 | 1559 | 如果覆盖函数想要做的是对被覆盖函数所做的扩展,那么可以通过 `super`(在其他代码之前、之后或之间)调用被覆盖函数: 1560 | 1561 | ```kotlin 1562 | override fun drive() = 1563 | super.drive() + " with $seatCount seats" 1564 | ``` 1565 | 1566 | 1567 | ### 接口 1568 | 1569 | 单继承规则经常变得过于局限,因为经常会发现类层次结构不同分支中的类之间存在共性。这些共同点可以在 _接口_ 中表达。 1570 | 1571 | 接口本质上是类可以选择签署的契约;如果确实如此,那么该类必须提供接口属性与函数的实现。但是,接口可以提供(但通常不提供)部分或全部属性与函数的默认实现。如果属性或函数具有默认实现,那么该类可以选择覆盖它,但这不是必须的。这是一个没有任何默认实现的接口: 1572 | 1573 | ```kotlin 1574 | interface Driveable { 1575 | val maxSpeed: Double 1576 | fun drive(): String 1577 | } 1578 | ``` 1579 | 1580 | 可以选择让 `MotorVehicle` 实现该接口,因为它具有所需的成员——但现在需要用 `override` 标记这些成员,并且由于覆盖的函数是隐式开放的,因此可以删除 `open`: 1581 | 1582 | ```kotlin 1583 | open class MotorVehicle( 1584 | override val maxSpeed: Double, 1585 | val wheelCount: Int 1586 | ) : Driveable { 1587 | override fun drive() = "Wroom!" 1588 | } 1589 | ``` 1590 | 1591 | 如果要引入另一个类 `Bicycle`,该类既不应该是 `MotorVehicle` 的子类也不可以是其超类,只要在 `Bicycle` 中声明 `maxSpeed` 与 `drive`,仍然可以使其实现 `Driveable`。 1592 | 1593 | 实现接口的类的子类(在本例中为 `Car`)也被视为正在实现该接口。 1594 | 1595 | 在接口内部声明的符号通常应该是 public。唯一的其他合法可见性修饰符是 `private`,只有在提供了函数体时才能使用——可以由实现该接口的每个类调用该函数,而不能由其他任何类调用。 1596 | 1597 | 至于为什么要创建一个接口,除了提醒类实现某些成员外,请参见[多态](#多态)一节。 1598 | 1599 | 1600 | ### 抽象类 1601 | 1602 | 某些超类作为相关类的分组机制和提供共享函数非常有用,但它们是如此笼统,以至于它们本身并没有用。`MotorVehicle` 似乎符合此描述。应该将此类声明为 _抽象类_,以防止直接实例化该类: 1603 | 1604 | ```kotlin 1605 | abstract class MotorVehicle(val maxSpeed: Double, val wheelCount: Int) 1606 | ``` 1607 | 1608 | 现在,不能如此声明:`val mv = MotorVehicle(100, 4)`。 1609 | 1610 | 抽象类是隐式开放的,因为如果它们没有任何具体的子类,它们将无用。 1611 | 1612 | 当抽象类实现一个或多个接口时,不必提供其接口成员的定义(但如果需要,可以提供)。它仍必须使用 `abstract override` _声明_ 此类成员,并且不为函数或属性提供任何主体: 1613 | 1614 | ```kotlin 1615 | abstract override val foo: String 1616 | abstract override fun bar(): Int 1617 | ``` 1618 | 1619 | 通过将工作分担到子类上,抽象是“逃避”必须实现接口成员的唯一方法——如果子类想要具体化,那么必须实现所有“缺失”成员。 1620 | 1621 | 1622 | ### 多态 1623 | 1624 | 多态是一种以通用方式处理具有相似特征的对象的能力。在 Python 中,这是通过 _[鸭子类型](https://zh.wikipedia.org/wiki/%E9%B8%AD%E5%AD%90%E7%B1%BB%E5%9E%8B)_ 实现的:如果 `x` 指向某个对象,那么只要该对象碰巧具有函数 `quack()`,就可以调用 `x.quack()`——关于该对象,不需要知道(或者更确切地说,假设)其他任何内容。这非常灵活,但是也很冒险:如果 `x` 是一个参数,那么函数的每个调用者都必须知道传递给它的对象必须具有 `quack()`,并且如果有人弄错了,程序就会在运行时崩溃。 1625 | 1626 | 在 Kotlin 中,多态性是通过类层次结构来实现的,这样就不可能遇到缺少属性或函数的情况。基本规则是,当且仅当 `B` 是 `A` 的子类型时,声明类型为 `A` 的变量/属性/参数才可以引用 `B` 类的实例。这意味着,`A` 必须是一个类,而 `B` 必须是 `A` 的子类,或者 `A` 必须是一个接口,而 `B` 必须是实现该接口的类,或者是该类的子类。使用上一部分中的类和接口,可以定义以下函数: 1627 | 1628 | ```kotlin 1629 | fun boast(mv: MotorVehicle) = 1630 | "My ${mv.wheelCount} wheel vehicle can drive at ${mv.maxSpeed} MPH!" 1631 | 1632 | fun ride(d: Driveable) = 1633 | "I'm riding my ${d.drive()}" 1634 | ``` 1635 | 1636 | 并这样调用它们: 1637 | 1638 | ```kotlin 1639 | val car = Car(4, 120) 1640 | boast(car) 1641 | ride(car) 1642 | ``` 1643 | 1644 | 可以将 `Car` 传递给 `boast()`,因为 `Car` 是 `MotorVehicle` 的子类。可以将 `Car` 传递给 `ride()`,因为 `Car` 实现了 `Driveable`(由于是 `MotorVehicle` 的子类)。在 `boast()` 内部,即使处在已知它确实是 `Car` 的情况下,也只能访问声明的参数类型为 `MotorVehicle` 的成员(因为可能会有其他调用而并非通过 `Car`)。在 `ride()` 内部,仅允许访问声明的参数类型 `Driveable` 的成员。这样可以确保每个成员查找都是安全的——编译器仅允许传递保证具有必需成员的对象。缺点是有时会迫使声明“不必要的”接口或包装器类,以使函数接受不同类的实例。 1645 | 1646 | 使用集合和函数,多态性变得更加复杂——请参见[泛型](#泛型)部分。 1647 | 1648 | 1649 | [//]: TODO (Overload resolution rules) 1650 | 1651 | 1652 | ### 类型转换与类型检测 1653 | 1654 | 当将接口或开放类作为参数时,通常在运行时不知道参数的实际类型,因为它可能是子类的实例,也可能是实现该接口的任何类的实例。可以检测确切的类型,但是像在 Python 中一样,通常应避免使用它,而应设计类层次结构,以便可以通过适当地覆盖函数或属性来执行所需的操作。 1655 | 1656 | 如果没有很好的解决方法,并且需要根据某种事物的类型采取特殊的操作或访问仅在某些类中存在的函数/属性,那么可以使用 `is` 检测对象的真实类型是否为特定的类或其子类(或接口的实现者)。当将它用作 `if` 中的条件时,编译器将允许对 `if` 主体内的对象执行特定于类型的操作: 1657 | 1658 | ```kotlin 1659 | fun foo(x: Any) { 1660 | if (x is Person) { 1661 | println("${x.name}") // 这不会在 if 之外编译 1662 | } 1663 | } 1664 | ``` 1665 | 1666 | 如果要检测 _不_ 是类型的实例,请使用 `!is`。请注意,`null` 绝不是任何非空类型的实例,但它始终是任何可为空类型的“实例”(即使从技术上讲它不是实例,但它没有任何实例)。 1667 | 1668 | 编译器不会执行无法成功执行的检测,因为变量的声明类型是要检测的类的类层次结构的不相关分支上的类——如果声明的类型为 `x` 是 `MotorVehicle`,不能检测 `x` 是否是 `Person`。如果 `is` 的右侧是接口,那么 Kotlin 将允许左侧的类型为任何接口或开放类,因为它的某些子类可以实现该接口。 1669 | 1670 | 如果代码对于编译器来说太聪明了,并且知道在没有 `is` 的帮助下,`x` 是 `Person` 的实例,但是编译器却不是,那么可以使用 `as` _转换(cast)_ 的值: 1671 | 1672 | ```kotlin 1673 | val p = x as Person 1674 | ``` 1675 | 1676 | 如果对象实际上不是 `Person` 或其任何子类的实例,那么将引发 `ClassCastException`(类强制转换异常)。如果不确定 `x` 是什么,如果它不是 `Person`,但是乐于获得 null,那么可以使用 `as?`,如果强制转换失败,它将返回 null。请注意,结果类型为 `Person?`: 1677 | 1678 | ```kotlin 1679 | val p = x as? Person 1680 | ``` 1681 | 1682 | 也可以使用 `as` 强制转换为可为空的类型。这个和之前的 `as?` 转换之间的区别是,如果 `x` 是除 `Person` 之外的其他类型的非空实例,那么此转换将失败: 1683 | 1684 | ```kotlin 1685 | val p = x as Person? 1686 | ``` 1687 | 1688 | 1689 | ### 委托 1690 | 1691 | 如果发现要通过类的属性之一实现了要实现类的接口,那么可以通过 `by` 将该接口的实现 _委托_ 给该属性的实现: 1692 | 1693 | ```kotlin 1694 | interface PowerSource { 1695 | val horsepowers: Int 1696 | } 1697 | 1698 | class Engine(override val horsepowers: Int) : PowerSource 1699 | 1700 | open class MotorVehicle(val engine: Engine): PowerSource by engine 1701 | ``` 1702 | 1703 | 通过在 `engine` 上调用相同的成员,这将自动在 `MotorVehicle` 中实现 `PowerSource` 的所有接口成员。这仅适用于在构造函数中声明的属性。 1704 | 1705 | 1706 | ### 属性委托 1707 | 1708 | 假设正在编写一个简单的 ORM。数据库库将一行表示为类 `Entity` 的实例,并具有诸如 `getString("name")` 与 `getLong("age")` 之类的函数,用于从给定列中获取键入的值。可以这样创建一个类型化的包装类: 1709 | 1710 | ```kotlin 1711 | abstract class DbModel(val entity: Entity) 1712 | 1713 | class Person(val entity: Entity) : DbModel(entity) { 1714 | val name = entity.getString("name") 1715 | val age = entity.getLong("age") 1716 | } 1717 | ``` 1718 | 1719 | 这很容易,但是也许要进行延迟加载,这样就不会花时间来提取不会使用的字段(特别是如果其中一些包含大量数据,而这种格式解析起来会很费时),也许希望支持默认值。虽然可以在 `get()` 块中实现该逻辑,但需要在每个属性中重复该逻辑。另外,可以在一个单独的 `StringProperty` 类中实现逻辑(请注意,这个简单的示例不是线程安全的): 1720 | 1721 | ```kotlin 1722 | class StringProperty( 1723 | private val model: DbModel, 1724 | private val fieldName: String, 1725 | private val defaultValue: String? = null 1726 | ) { 1727 | private var _value: String? = defaultValue 1728 | private var loaded = false 1729 | val value: String? 1730 | get() { 1731 | // 警告:这不是线程安全的! 1732 | if (loaded) return _value 1733 | if (model.entity.contains(fieldName)) { 1734 | _value = model.entity.getString(fieldName) 1735 | } 1736 | loaded = true 1737 | return _value 1738 | } 1739 | } 1740 | 1741 | // 在 Person 里 1742 | val name = StringProperty(this, "name", "Unknown Name") 1743 | ``` 1744 | 1745 | 不幸的是,使用此属性会要求每次要使用该属性时都键入 `p.name.value`。可以执行以下操作,但这也不好,因为它引入了额外的属性: 1746 | 1747 | ```kotlin 1748 | // 在 Person 里 1749 | private val _name = StringProperty(this, "name", "Unknown Name") 1750 | val name get() = _name.value 1751 | ``` 1752 | 1753 | 该解决方案是一个委托的属性,它允许指定获取和设置属性的行为(与在 Python 中实现 `__getattribute__()` 与 `__setattribute__()` 类似,但一次只设置一个属性)。 1754 | 1755 | ```kotlin 1756 | class DelegatedStringProperty( 1757 | private val fieldName: String, 1758 | private val defaultValue: String? = null 1759 | ) { 1760 | private var _value: String? = null 1761 | private var loaded = false 1762 | operator fun getValue(thisRef: DbModel, property: KProperty<*>): String? { 1763 | if (loaded) return _value 1764 | if (thisRef.entity.contains(fieldName)) { 1765 | _value = thisRef.entity.getString(fieldName) 1766 | } 1767 | loaded = true 1768 | return _value 1769 | } 1770 | } 1771 | ``` 1772 | 1773 | 可以像这样使用委派的属性在 `Person` 中声明属性——请注意,使用 `by` 代替 `=`: 1774 | 1775 | ```kotlin 1776 | val name by DelegatedStringProperty(this, "name", "Unknown Name") 1777 | ``` 1778 | 1779 | 现在,只要有人读 `p.name`、`getValue()` 都将以 `p` 作为 `thisRef` 调用,并将 `name` 属性的元数据作为 `property` 调用。由于 `thisRef` 是 `DbModel`,因此只能在 `DbModel` 及其子类内部使用此委托属性。 1780 | 1781 | 一个好用的内置委托属性 `lazy`,它是惰性加载模式的适当线程安全实现。首次访问该属性时,将仅对提供的 ​​lambda 表达式求值一次。 1782 | 1783 | ```kotlin 1784 | val name: String? by lazy { 1785 | if (thisRef.entity.contains(fieldName)) { 1786 | thisRef.entity.getString(fieldName) 1787 | } else null 1788 | } 1789 | ``` 1790 | 1791 | 1792 | ### 密封类 1793 | 1794 | 如果要限制基类的子类集,那么可以将基类声明为 `sealed`(这也使其抽象化),在这种情况下,只能在同一文件中声明子类。然后,编译器知道了所有可能的子类的完整集合,这将使在不需要 `else` 子句的情况下对所有可能的子类型进行穷尽的 `when` 表达(如果以后添加另一个子类而忘记更新 `when`,编译器会告知)。 1795 | 1796 | 1797 | ## 对象与伴生对象 1798 | 1799 | 1800 | ### 对象声明 1801 | 1802 | 如果需要 _单例_(一个仅存在一个实例的类),那么可以按常规方式声明该类,但是使用 `object` 关键字而不是 `class`: 1803 | 1804 | ```kotlin 1805 | object CarFactory { 1806 | val cars = mutableListOf() 1807 | 1808 | fun makeCar(horsepowers: Int): Car { 1809 | val car = Car(horsepowers) 1810 | cars.add(car) 1811 | return car 1812 | } 1813 | } 1814 | ``` 1815 | 1816 | 该类将永远只有一个实例,并且该实例(以线程安全的方式在首次访问该实例时创建)将与该类本身具有相同的名称: 1817 | 1818 | ```kotlin 1819 | val car = CarFactory.makeCar(150) 1820 | println(CarFactory.cars.size) 1821 | ``` 1822 | 1823 | 1824 | ### 伴生对象 1825 | 1826 | 如果需要将函数或属性绑定到类而不是实例(类似于Python中的 `@staticmethod`),那么可以在 _伴生对象_ 中声明它: 1827 | 1828 | ```kotlin 1829 | class Car(val horsepowers: Int) { 1830 | companion object Factory { 1831 | val cars = mutableListOf() 1832 | 1833 | fun makeCar(horsepowers: Int): Car { 1834 | val car = Car(horsepowers) 1835 | cars.add(car) 1836 | return car 1837 | } 1838 | } 1839 | } 1840 | ``` 1841 | 1842 | 伴生对象是一个单例,可以通过包含类的名称直接访问其成员(如果要明确地访问伴生对象,也可以插入伴生对象的名称): 1843 | 1844 | ```kotlin 1845 | val car = Car.makeCar(150) 1846 | println(Car.Factory.cars.size) 1847 | ``` 1848 | 1849 | 尽管语法上很方便,但伴生对象本身是一个真正的对象,并且可以具有自己的超类型——可以将其赋值给变量并传递。如果要与 Java 代码集成,并且需要一个真正的 `static` 成员,那么可以使用 `@JvmStatic` 在一个伴生对象内部[注解](#注解)一个成员。 1850 | 1851 | 当类加载时(通常是第一次被其他正在执行的代码引用该类时),将以线程安全的方式初始化一个伴生对象。可以省略名称,在这种情况下,名称默认为 `Companion`。一个类只能有一个伴生对象,并且伴生对象不能嵌套。 1852 | 1853 | 伴生对象及其成员只能通过包含它的类名称访问,而不能通过包含它的类的实例访问。Kotlin 不支持也可以在子类中覆盖的类级函数(例如 Python 中的 `@classmethod`)。如果在子类中重新声明一个伴生对象,那么只需从基类中隐藏该对象即可。如果需要一个可覆盖的“类级”函数,请将其设为普通的开放函数,在该函数中不访问任何实例成员——可以在子类中覆盖它,并且当通过对象实例调用它时,将调用该对象类中的覆盖。通过 Kotlin 中的类引用来调用函数是可能的,但很不方便,因此在此不做介绍。 1854 | 1855 | 1856 | ### 对象表达式 1857 | 1858 | Java 几年前才获得对函数类型和 lambda 表达式的支持。以前,Java 通过使用接口定义函数签名并允许实现该接口类的内联匿名定义来解决此问题。这在 Kotlin 中也可用,部分是为了与 Java 库兼容,部分是因为它可以方便地指定事件处理程序(特别是如果同一侦听器对象必须侦听多个事件类型)。考虑一个接口或一个(可能是抽象的)类,以及一个采用其实例的函数: 1859 | 1860 | ```kotlin 1861 | interface Vehicle { 1862 | fun drive(): String 1863 | } 1864 | 1865 | fun start(vehicle: Vehicle) = println(vehicle.drive()) 1866 | ``` 1867 | 1868 | 通过使用 _对象表达式_,现在可以定义一个匿名的未命名类,并同时创建一个实例,称为 _匿名对象_: 1869 | 1870 | ```kotlin 1871 | start(object : Vehicle { 1872 | override fun drive() = "Driving really fast" 1873 | }) 1874 | ``` 1875 | 1876 | 如果超类型具有构造函数,那么必须在超类型名称之后用括号将其调用。可以根据需要指定多个超类型(但通常,最多只有一个超类)。 1877 | 1878 | 由于匿名类没有名称,因此不能将其用作返回类型——如果确实返回了匿名对象,那么该函数的返回类型必须为 `Any`。 1879 | 1880 | 尽管使用了 `object` 关键字,但无论何时对对象表达式求值,都会创建一个匿名类的新实例。 1881 | 1882 | 对象表达式的主体可以访问并可能修改包含它的作用域的局部变量。 1883 | 1884 | 1885 | ## 泛型 1886 | 1887 | 1888 | ### 泛型类型参数 1889 | 1890 | 可能有人认为静态类型会使创建集合类或需要包含其类型随每次使用而变化的成员的任何其他类变得非常不切实际。通用方法:它们可以在类或函数中指定“占位符”类型,每当使用类或函数时,都必须填写该类型。例如,链表中的节点需要包含某种类型的数据,而这些类型的数据在编写该类时是未知的,因此引入了 _泛型类型参数_ `T`(通常指定为单字母名称): 1891 | 1892 | ```kotlin 1893 | class TreeNode(val value: T?, val next: TreeNode? = null) 1894 | ``` 1895 | 1896 | 每当创建此类的实例时,都必须指定一个实际的类型代替 `T`,除非编译器可以从构造函数参数 `TreeNode("foo")` 或 `TreeNode(null)` 中推断出类型。每次使用此实例都会像看起来像是一个类的实例一样: 1897 | 1898 | ```kotlin 1899 | class TreeNode(val value: String?, val next: TreeNode? = null) 1900 | ``` 1901 | 1902 | 泛型类中的成员属性与成员函数在很大程度上可以像使用普通类型一样使用类的泛型类型参数,而不必重新声明它们。还可以使函数接受比类更多的泛型参数,使泛型函数驻留在非泛型类中,以及使泛型顶层函数成为泛型(将在下一个示例中执行这一操作)。请注意泛型函数声明中泛型类型参数的不同位置: 1903 | 1904 | ```kotlin 1905 | fun makeLinkedList(vararg elements: T): TreeNode? { 1906 | var node: TreeNode? = null 1907 | for (element in elements.reversed()) { 1908 | node = TreeNode(element, node) 1909 | } 1910 | return node 1911 | } 1912 | ``` 1913 | 1914 | 1915 | ### 约束 1916 | 1917 | 通过指定泛型类型参数必须是特定类型或其子类的实例,可以限制可用于泛型类型参数的类型。如果有一个名为 `Vehicle` 的类或接口,那么可以这样做: 1918 | 1919 | ```kotlin 1920 | class TreeNode 1921 | ``` 1922 | 1923 | 现在,可能无法创建类型不是 `Vehicle` 的子类/实现的 `TreeNode`。在类内部,只要获得类型为 `T` 的值,就可以访问其上所有 `Vehicle` 的公共成员。 1924 | 1925 | 如果要施加其他约束,那么必须使用单独的 `where` 子句,在这种情况下,类型参数必须是给定类的子类(如果指定了一个类,并且最多可以指定一个),_并且_ 实现所有给定的接口。然后,只要获得类型 `T` 的值,就可以访问所有给定类型的所有公共成员: 1926 | 1927 | ```kotlin 1928 | class TreeNode where T : Vehicle, T : HasWheels 1929 | ``` 1930 | 1931 | 1932 | ### 型变 1933 | 1934 | 1935 | #### 简介 1936 | 1937 | 流行测验:如果 `Apple` 是 `Fruit` 的子类型,并且 `Bowl` 是通用容器类,那么 `Bowl` 是否为 `Bowl` 的子类型?答案为——也许令人惊讶——_否_。原因是,如果它是子类型,将能够像这样破坏类型系统: 1938 | 1939 | ```kotlin 1940 | fun add(bowl: Bowl, fruit: Fruit) = bowl.add(fruit) 1941 | 1942 | val bowl = Bowl() 1943 | add(bowl, Pear()) // 实际上不编译! 1944 | val apple = bowl.get() // 裂开! 1945 | ``` 1946 | 1947 | 如果编译到倒数第二行,这将使可以在一个表面上只有一个苹果的盘子中放入一个梨,当尝试从盘子中提取“苹果”时,这代码就会裂开。但是,通常让泛型类型参数的类型层次结构“流”到泛型类通常很有用。但是,正如在上面看到的,必须注意一些问题——解决方案是限制将数据移入与移出通用对象的方向。 1948 | 1949 | 1950 | #### 声明处协变与逆变 1951 | 1952 | 如果有 `Generic` 的实例,并且想将其称为 `Generic`,那么可以安全地从中 _获取_ 泛型类型参数的实例——这些将确实是 `Subtype` 的实例。(因为它们来自 `Generic` 的实例),但是它们看起来是 `Supertype` 的实例(因为已经告诉编译器具有 `Generic`)。这很安全;它被称为 _协变_,而 Kotlin 可以通过在泛型参数前面放置 `out` 来进行 _声明处协变_。如果这样做,那么只能将该类型参数用作返回类型,而不能用作参数类型。这是最简单有用的协变接口: 1953 | 1954 | ```kotlin 1955 | interface Producer { 1956 | fun get(): T 1957 | } 1958 | ``` 1959 | 1960 | 将 `Producer` 视为 `Producer` 是安全的——它将产生的唯一东西是 `Apple` 实例,但这没关系,因为 `Apple` 是 `Fruit`。 1961 | 1962 | 相反,如果有 `Generic` 的实例,并且想将其引用为 `Generic`(不能使用非泛型类),那么可以安全地为其 _提供_ 泛型类型参数的实例——编译器将要求这些实例的类型为 `Subtype`,这对于实际实例是可接受的,因为它可以处理任何 `Supertype`。这被称为 _逆变_,而 Kotlin 可以通过在泛型参数的前面加 `in` 来进行 _声明处逆变_。如果这样做,那么只能将该类型参数用作参数类型,而不能用作返回类型。这是最简单有用的逆变接口: 1963 | 1964 | ```kotlin 1965 | interface Consumer { 1966 | fun add(item: T) 1967 | } 1968 | ``` 1969 | 1970 | 将 `Consumer ` 视为 `Consumer ` 是安全的——然后,只能在其中添加 `Apple` 实例,但这没关系,因为它能够接收任何 `Fruit`。 1971 | 1972 | 通过这两个接口,可以制作出更多用途的果盘。盘子本身需要产生与使用其泛型,所以它既不能是协变的也不能是逆变的,但是它可以实现协变与逆变接口: 1973 | 1974 | ```kotlin 1975 | class Bowl : Producer, Consumer { 1976 | private val items = mutableListOf() 1977 | override fun get(): T = items.removeAt(items.size - 1) 1978 | override fun add(item: T) { items.add(item) } 1979 | } 1980 | ``` 1981 | 1982 | 现在,可以将盘子 `T` 视为 `T` 的任何超类的生产者,以及 `T` 的任何子类的消费者: 1983 | 1984 | ```kotlin 1985 | val p: Producer = Bowl() 1986 | val c: Consumer = Bowl() 1987 | ``` 1988 | 1989 | 1990 | #### 型变方向 1991 | 1992 | 如果变量类型的成员的参数或返回类型本身就是变量,那将会变得有些复杂。参数中的函数类型和返回类型使其更具挑战性。如果想在特定位置使用变体类型参数 `T` 是否安全,请问自己: 1993 | 1994 | * 如果 `T` 是协变的:类的用户认为处于这个位置的 `T` 是 `Supertype`,而实际上是 `Subtype` 这样可以吗? 1995 | * 如果 `T` 是逆变的:类的用户认为处于这个位置的 `T` 是 `Subtype`,而实际上是 `Supertype` 这样可以吗? 1996 | 1997 | 这些注意事项导致以下规则。协变类型参数 `T`(对象的用户可能认为这是 `Fruit`,而实际上该对象是 `Apple`)可以用作: 1998 | 1999 | * `val v: T` 2000 | 2001 | 只读属性类型(用户期望获得 `Fruit`,并获得 `Apple`) 2002 | 2003 | * `val p: Producer` 2004 | 2005 | 只读属性类型的协变类型参数(用户期望 `Fruit` 的生产者,而得到 `Apple` 的生产者) 2006 | 2007 | * `fun f(): T` 2008 | 2009 | 返回类型(正如所见) 2010 | 2011 | * `fun f(): Producer` 2012 | 2013 | 返回类型的协变类型参数(用户期望返回的值将产生一个 `Fruit`,所以如果它确实产生一个 `Apple` 也是可以的) 2014 | 2015 | * `fun f(consumer: Consumer)` 2016 | 2017 | 参数类型的逆变类型参数(用户传递了可以处理任何 `Fruit` 的消费者,用户将得到一个 `Apple`) 2018 | 2019 | * `fun f(function: (T) -> Unit)` 2020 | 2021 | 函数类型参数的参数类型(用户正在传递可以处理任何 `Fruit` 的函数,用户将获得一个 `Apple`) 2022 | 2023 | * `fun f(function: (Producer) -> Unit)` 2024 | 2025 | 函数类型参数的参数类型的协变类型参数(用户正在传递可以处理任何 `Fruit` 生产者的函数,用户将获得 `Apple` 生产者) 2026 | 2027 | * `fun f(function: () -> Consumer)` 2028 | 2029 | 函数类型参数的返回类型的逆变类型参数(用户传递的函数将返回任何 `Fruit` 的消费者,为返回的消费者提供 `Apple` 实例) 2030 | 2031 | * `fun f(): () -> T` 2032 | 2033 | 函数类型的返回类型的返回类型(用户希望返回的函数返回 `Fruit`,因此,如果它确实返回 `Apple` 也是可以的) 2034 | 2035 | * `fun f(): () -> Producer` 2036 | 2037 | 函数类型的返回类型的返回类型的协变量类型参数(用户希望返回的函数返回产生 `Fruit` 的内容,因此如果它确实产生 `Apple`,也是可以的) 2038 | 2039 | * `fun f(): (Consumer) -> Unit` 2040 | 2041 | 函数类型返回类型的参数的变量类型参数(用户将使用可能消耗任何 `Fruit` 的东西来调用返回的函数,因此可以返回希望接收到可以处理 `Apple` 的东西的函数) 2042 | 2043 | 在逆变的情况下可以使用协变类型参数。至于这些成员的签名为何合法,则留给读者自行解答: 2044 | 2045 | * `val c: Consumer` 2046 | * `fun f(item: T)` 2047 | * `fun f(): Consumer` 2048 | * `fun f(producer: Producer)` 2049 | * `fun f(function: () -> T)` 2050 | * `fun f(function: () -> Producer)` 2051 | * `fun f(function: (Consumer) -> Unit)` 2052 | * `fun f(): (T) -> Unit` 2053 | * `fun f(): (Producer) -> Unit` 2054 | * `fun f(): () -> Consumer` 2055 | 2056 | 2057 | #### 类型投影(使用处协变与逆变) 2058 | 2059 | 如果使用的泛型类的类型参数没有以不同的方式声明(要么是因为作者没有想到,要么是因为类型参数不能具有任何一种型变类型,因为它们既用作参数类型又用作返回类型),由于 _类型投影_,仍然可以以其他方式使用它。术语“投影”是指这样的事实:执行此操作时,可能会限制自己仅使用其某些成员——因此,从某种意义上讲,只能看到类的部分或“投影”版本。再次关注 `Bowl` 类,但是这次没有变量接口: 2060 | 2061 | ```kotlin 2062 | class Bowl { 2063 | private val items = mutableListOf() 2064 | fun get(): T = items.removeAt(items.size - 1) 2065 | fun add(item: T) { items.add(item) } 2066 | } 2067 | ``` 2068 | 2069 | 因为 `T` 用作参数类型,所以它不能是协变的,并且因为它用作返回类型,所以它不能是逆变的。但是,如果只想使用 `get()` 函数,那么可以使用 `out` 进行协变地投影: 2070 | 2071 | ```kotlin 2072 | fun moveCovariantly(from: Bowl, to: Bowl) { 2073 | to.add(from.get()) 2074 | } 2075 | ``` 2076 | 2077 | 在这里的 `from` 的类型参数必须是 `to` 的类型参数的子类型。此函数将接受例如 `Bowl` 作为 `from`,而 `Bowl` 作为 `to`。而使用 `out` 投影而付出的代价是,无法在 `from()` 上调用 `add()`,因为不知道其真实类型参数,因此可能会给它添加不兼容的水果。 2078 | 2079 | 可以通过使用 `in` 来对逆变投影做类似的事情: 2080 | 2081 | ```kotlin 2082 | fun moveContravariantly(from: Bowl, to: Bowl) { 2083 | to.add(from.get()) 2084 | } 2085 | ``` 2086 | 2087 | 现在,`to` 的类型参数必须是 `from` 的类型参数的超类型。这次,将失去在 `to` 上调用 `get()` 的能力。 2088 | 2089 | 相同的类型参数可以用于协变与逆变投影(因为被投影的是泛型类,而不是类型参数) 2090 | 2091 | ```kotlin 2092 | fun moveContravariantly(from: Bowl, to: Bowl) { 2093 | to.add(from.get()) 2094 | } 2095 | ``` 2096 | 2097 | 虽然这样做在这个特殊的例子中没有用处,但可以通过添加未投影的参数类型 `via: Bowl` 来获得有趣的效果,在这种情况下,`via` 的泛型参数将被强制为“在 `from` 与 `to` 之间”。 2098 | 2099 | 如果不知道(或不在乎)泛型类型是什么,可以使用 _星投影_: 2100 | 2101 | ```kotlin 2102 | fun printSize(items: List<*>) = println(items.size) 2103 | ``` 2104 | 2105 | 如果使用的泛型类型中有一个或多个类型参数是星投影的,可以: 2106 | 2107 | * 使用任何未提及的成员的所有星投影类型参数 2108 | * 使用任何返回星投影类型参数的成员,但是返回类型将显示为 `Any?`。(除非类型参数受到约束,在这种情况下,将获得约束中提到的类型) 2109 | * 不要使用任何采用星投影类型作为参数的成员 2110 | 2111 | 2112 | ### 具体化的类型参数 2113 | 2114 | 可悲的是,Kotlin 继承了 Java 对泛型的限制:严格来说,它们是一个编译时概念——泛型类型信息在运行时被 _擦除_。因此,不能使用 `T()` 来构造泛型的新实例;无法在运行时检测对象是否为泛型类型参数的实例;如果尝试在泛型类型之间进行转换,编译器将无法保证其正确性。 2115 | 2116 | 幸运的是,Kotlin 有 _具体化的类型参数_,从而缓解了其中的一些问题。通过在泛型参数前面编写 `reified`,它在运行时确实可用,并且需要编写 `T::class` 来获取[类元数据](#由类引用获取成员引用)。只能在内联函数中这样做(因为内联函数将被编译到它的调用处,其中类型信息在运行时 _可用_),但它仍然需要很长时间。例如,可以为签名不太优雅的大型函数创建内联包装器函数。 2117 | 2118 | 在下面的示例中,假设有一个 `DbModel` 基类,并且每个子类都有一个无参数的主构造函数。在内联函数中, `T` 被具体化了,因此可以获得类元数据。将其传递给执行与数据库通信的实际工作的函数。 2119 | 2120 | ```kotlin 2121 | inline fun loadFromDb(id: String): T = 2122 | loadFromDb(T::class, id) 2123 | 2124 | fun loadFromDb(cls: KClass, id: String): T { 2125 | val entity = cls.primaryConstructor!!.call() 2126 | val tableName = cls.simpleName 2127 | // DB magic goes here - load from table `tableName`, 2128 | // and use the data to populate `entity` 2129 | // (possibly via `memberProperties`) 2130 | return entity 2131 | } 2132 | ``` 2133 | 2134 | 现在,可以使用 `loadFromDb("x01234567")` 从 `Exercise` 数据库表中加载对象。 2135 | 2136 | 2137 | ## 扩展函数/属性 2138 | 2139 | 由于无法修改内置或第三方类,因此无法直接向其添加函数或属性。如果仅通过使用类的公共成员就可以实现所需的目标,那么可以编写一个将类的实例作为参数的函数——但是有时候,真的很想说 `x.foo(y)` 而不是 `foo(x, y)`,特别是如果要进行一系列这样的调用或属性查找时:`x.foo(y).bar().baz` 而不是 `getBaz(bar(foo(x, y)))`。 2140 | 2141 | 有一个不错的语法糖可以做到这一点:_扩展函数_ 与 _扩展属性_。它们看起来像常规成员函数/属性,但是它们在任何类之外定义——然而它们引用了类名并且可以使用 `this`。总之,他们只能使用该类的可见成员(通常只是公共成员)。在幕后,它们被编译为以目标实例为参数的常规函数。 2142 | 2143 | 例如,如果处理大量字节,那么可能希望轻松获取 0 到 255 之间的无符号字节,而不是默认的 -128 到 127(结果必须采用 `Short`/`Int`/`Long`)。`Byte` 是无法修改的内置类,但是可以定义此扩展函数: 2144 | 2145 | ```kotlin 2146 | fun Byte.toUnsigned(): Int { 2147 | return if (this < 0) this + 256 else this.toInt() 2148 | } 2149 | ``` 2150 | 2151 | 现在,可以执行以下操作: 2152 | 2153 | ```kotlin 2154 | val x: Byte = -1 2155 | println(x.toUnsigned()) // Prints 255 2156 | ``` 2157 | 2158 | 如果愿意使用 `x.unsigned`,那么可以定义一个扩展属性: 2159 | 2160 | ```kotlin 2161 | val Byte.unsigned: Int 2162 | get() = if (this < 0) this + 256 else this.toInt() 2163 | ``` 2164 | 2165 | 请记住,这只是语法糖——实际上并没有在修改类或其实例。因此,必须在要使用扩展函数/属性的任何地方导入它(因为它不随类的实例一起提供)。出于同样的原因,不能覆盖扩展成员——可以为子类型重新实现扩展成员,但是解决方案是在编译时根据调用它的表达式的静态类型进行的。因此,如果为 `Vehicle` 声明了一个扩展函数,并且为其子类 `Car` 声明了相同的名称和签名,并且执行了以下操作,那么即使 `v` 实际上是 `Car`,也将调用 `Vehicle` 的扩展函数: 2166 | 2167 | ```kotlin 2168 | fun foo(v: Vehicle) = v.extension() 2169 | val x = foo(Car()) 2170 | ``` 2171 | 2172 | Kotlin 中有很多内置的扩展函数/属性——例如:`map()`、`filter()` 以及框架中使用扩展函数以函数式方式处理集合的其余部分。 2173 | 2174 | 2175 | ## 成员引用与反射 2176 | 2177 | 2178 | ### 属性引用 2179 | 2180 | 思考此类: 2181 | 2182 | ```kotlin 2183 | class Person(val name: String, var age: Int) { 2184 | fun present() = "I'm $name, and I'm $age years old" 2185 | fun greet(other: String) = "Hi, $other, I'm $name" 2186 | } 2187 | ``` 2188 | 2189 | 可以像这样获取其 `name` 属性的引用: 2190 | 2191 | ```kotlin 2192 | val prop = Person::name 2193 | ``` 2194 | 2195 | 结果是一个对象,该对象表示对该属性的引用(“柏拉图理想”属性,而不是特定实例上的属性)。属性对象有一个类型层次结构:基本接口是 `KProperty`,它能够获取有关属性的元数据,例如名称与类型。如果要使用属性对象读取或修改对象中属性的值那么需要使用一个子接口来指定它是什么类型的属性。不可变属性通常是 `KProperty1`,可变属性通常是 `KMutableProperty1`。这两个都是通用接口,其中 `R` 是接收者类型(在该类型中声明属性,在这种情况下是 `Person`),而 `V` 是属性值的类型。 2196 | 2197 | 给定一个 `R` 实例,`KProperty1` 将允许通过调用 `get()` 来读取该实例中该属性具有的值,而 `KMutableProperty1` 还可以通过调用 `set()` 来更改实例中的属性值。使用该实例,可以开始编写用于操作属性的函数,而无需事先知道它们将要处理哪个属性(或哪个类): 2198 | 2199 | ```kotlin 2200 | fun printProperty(instance: T, prop: KProperty1) { 2201 | println("${prop.name} = ${prop.get(instance)}") 2202 | } 2203 | 2204 | fun incrementProperty( 2205 | instance: T, prop: KMutableProperty1 2206 | ) { 2207 | val value = prop.get(instance) 2208 | prop.set(instance, value + 1) 2209 | } 2210 | 2211 | val person = Person("Lisa", 23) 2212 | printProperty(person, Person::name) 2213 | incrementProperty(person, Person::age) 2214 | ``` 2215 | 2216 | 还可以通过在属性名称前面加上 `::`(例如:`::foo`)来获得对顶层属性的引用,其类型将为 `KProperty0` 或 `KMutableProperty0`。 2217 | 2218 | 2219 | ### 函数引用 2220 | 2221 | 函数的作用类似于属性,但可以作为两种不同类型引用。 2222 | 2223 | 如果要查看函数的元数据(例如:函数名称),请使用 `KFunction` 或其子接口之一,其中 `V` 是函数的返回类型。 这是一个基本示例: 2224 | 2225 | ```kotlin 2226 | val person = Person("Lisa", 32) 2227 | val g: KFunction = Person::greet 2228 | println(g.name) 2229 | println(g.call(person, "Anne")) 2230 | ``` 2231 | 2232 | 在函数对象上调用 `call()` 将调用该函数。如果它是成员函数那么第一个参数必须是 _接收者_(要在其上调用函数的对象,在本例中为 `person`),其余参数必须为普通函数参数(在本例中为 `"Anne"`)。 2233 | 2234 | 由于在 `KFunction` 中未将参数类型编码为泛型参数,因此无法对传递的参数进行编译时类型验证。为了对参数类型进行编码,请使用以下子接口之一:`KFunction1`、`KFunction2`、`KFunction3`、依此类推,这取决于函数有多少个参数。请记住,如果要引用成员函数那么第一个泛型类型参数是接收者类型。例如:`KFunction3` 可以引用一个普通函数,该函数以 `A`、`B`、`C` 为参数并返回 `V`,也可以引用 `A` 上的一个成员函数,该函数以 `B`、`C` 为参数并返回 `V`。当使用这些类型中的任何一种时,都可以通过其引用来调用该函数,就好像该引用是一个函数一样。例如:`function(a, b)`,并且此调用将是类型安全的。 2235 | 2236 | 还可以直接在对象上引用成员属性,在这种情况下,将获得已绑定到其接收者的成员函数引用,因此在签名中不需要接收者类型。这是这两种方法的示例: 2237 | 2238 | ```kotlin 2239 | fun callAndPrintOneParam(function: KFunction1, a: A): V { 2240 | val result = function(a) 2241 | println("${function.name}($a) = $result") 2242 | return result 2243 | } 2244 | 2245 | fun callAndPrintTwoParam(function: KFunction2, a: A, b: B): V { 2246 | val result = function(a, b) 2247 | println("${function.name}($a, $b) = $result") 2248 | return result 2249 | } 2250 | 2251 | val p = Person("Lisa", 32) 2252 | callAndPrintOneParam(p::greet, "Alice") 2253 | callAndPrintTwoParam(Person::greet, person, "Lisa") 2254 | ``` 2255 | 2256 | 如果只想调用函数而不关心元数据,请使用函数类型,例如:`(A, B) -> V` 用于普通函数引用或绑定成员函数引用,或 `A.(B, C) -> V` 用于 `A` 上的未绑定成员函数引用。请注意,`KFunction` 及其子接口仅可用于已声明的函数(通过在代码中显式引用它或通过反射来获得,如稍后所示)——只有函数类型可用于函数字面量(lambda 表达式或匿名函数)。 2257 | 2258 | 可以在函数名称前加上 `::`(例如:`::foo`),以获得对顶层函数的引用。 2259 | 2260 | 2261 | ### 由类引用获取成员引用 2262 | 2263 | 尽管在 Kotlin 中可以在运行时动态创建新类或将成员添加到类中,但这既棘手又缓慢,并且通常不鼓励这样做。然而,动态地探查一个对象是很容易的,例如:看它包含什么属性和函数,以及它们上面有什么注解。这被称为 _反射_,它不是很高效,因此除非真正需要它,否则请避免使用它。 2264 | 2265 | Kotlin 有自己的反射库(构建中必须包含 `kotlin-reflect.jar`)。以 JVM 为目标时,还可以使用 Java 反射工具。请注意,Kotlin 反射特性还不是很完善——特别是,不能使用它来探查诸如 `String` 之类的内置类。 2266 | 2267 | 警告:使用反射通常是解决 Kotlin 问题的错误方法!特别是,如果有几个都具有某些公共属性/函数的类,并且想要编写一个可以接受任何这些类的实例并使用这些属性的函数那么正确的方法是用通用的属性/函数定义一个接口,并使所有相关的类都实现它;然后,该函数可以将该接口作为参数。如果不控制这些类那么可以使用[适配器模式](https://zh.wikipedia.org/wiki/%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F)并编写实现该接口的包装器类——由于 Kotlin 的[委托特性](#委托),这非常容易。通过巧妙地使用泛型,还可以获得很多优势。 2268 | 2269 | 在类名后附加 `::class` 将提供该类的 `KClass` 元数据对象。通用类型参数 `C` 是类本身,因此,如果要编写可用于任何类的元数据的函数那么可以使用 `KClass<*>`,或者可以使用类型参数 `T` 与参数类型 `KClass` 来创建泛型函数。由此,可以获得对类成员的引用。`KClass` 上最有趣的属性可能是 `primaryConstructor`、`constructors`、`memberProperties`、`declaredMemberProperties`、`memberFunctions` 与 `declaredMemberFunctions`。例如:`memberProperties` 与 `declaredMemberProperties` 之间的区别在于前者包括继承的属性,而后者只包括已经在类自己的主体中声明的属性。 2270 | 2271 | 在此示例中,使用上一节中的 `Person` 与 `callAndPrintTwoParam()`,按名称查找成员函数引用并对其进行调用: 2272 | 2273 | ```kotlin 2274 | val f = Person::class.memberFunctions.single { it.name == "greet" } as KFunction2 2275 | callAndPrintTwoParam(f, person, "Lisa") 2276 | ``` 2277 | 2278 | `greet()` 的签名为 `KFunction2`,因为它是 `Person` 上的一个函数,它接受 `String` 并返回 `String`。 2279 | 2280 | 构造函数引用实际上是工厂函数,用于创建类的新实例,这可能会派上用场: 2281 | 2282 | ```kotlin 2283 | val ctor = Person::class.primaryConstructor!! as (String, Int) -> Person 2284 | val newPerson = ctor("Karen", 45) 2285 | ``` 2286 | 2287 | 2288 | ### Java 风格反射 2289 | 2290 | 如果以 JVM 平台为目标,那么还可以直接使用 Java 的反射系统。在此示例中,通过将函数名称指定为字符串来从对象的类中获取函数引用(如果函数带有参数,那么还需要指定其类型),然后调用它。注意,在任何地方都没有提到 `String` ——这种技术在不知道对象的类是什么的情况下起作用,但是如果对象的类没有所请求的函数,将会引发异常。但是,Java 风格的函数引用没有类型信息,因此将无法验证参数类型,并且必须强制转换返回值: 2291 | 2292 | ```kotlin 2293 | val s = "Hello world" 2294 | val length = s.javaClass.getMethod("length") 2295 | val x = length.invoke(s) as Int 2296 | ``` 2297 | 2298 | 如果没有该类的实例,那么可以使用 `String::class.java` 获取该类的元数据(但是只有在拥有实例后才能调用其任何成员)。 2299 | 2300 | 如果还需要动态查找该类,那么可以使用 `Class.forName()` 并提供该类的全限定名称。 2301 | 2302 | 2303 | ## 注解 2304 | 2305 | 尽管 Kotlin 注解看起来像 Python 装饰器,但它们的灵活性要差得多:它们通常只能用于元数据。它们是纯数据类,不包含任何可执行代码。一些内置注解会影响编译过程(例如:`@JvmStatic`),但是自定义注解仅可用于提供可由反射系统在运行时探查的元数据。不会在这里深入研究注解,但这里有一个示例。注解声明本身上的注解指定了注解可以应用于哪些构造以及是否可用于运行时探查。 2306 | 2307 | ```kotlin 2308 | enum class TestSizes { SMALL, MEDIUM, LARGE } 2309 | 2310 | @Target(AnnotationTarget.CLASS) 2311 | @Retention(AnnotationRetention.RUNTIME) 2312 | annotation class TestSize(val size: TestSizes) 2313 | 2314 | @TestSize(TestSizes.SMALL) 2315 | class Tests { ... } 2316 | 2317 | fun getTestSize(cls: KClass<*>): TestSizes? = 2318 | cls.findAnnotation()?.size 2319 | 2320 | println(getTestSize(Tests::class)) 2321 | ``` 2322 | 2323 | 2324 | ## 文件 I/O 2325 | 2326 | Kotlin 继承了 Java 烦躁(但非常灵活)的 I/O 方式,但是简化了一些附加特性。不会在这里介绍所有内容,因此对于初学者来说,这就是如何遍历文件的所有行(需要 `import java.io.File`): 2327 | 2328 | ```kotlin 2329 | File("data.txt").forEachLine { println(it) } 2330 | ``` 2331 | 2332 | 默认[字符编码](https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/)为 UTF-8,但是如果需要的话可以指定其他字符编码: 2333 | 2334 | ```kotlin 2335 | File("data.txt").forEachLine(Charsets.UTF_16) { println(it) } 2336 | ``` 2337 | 2338 | 请注意,每一行的末尾换行符被去除。还可以在文件对象上调用 `readLines()` 以获取所有行的列表,或 `useLines()` 提供将在每一行上调用的函数。如果只希望将整个文件内容作为一个字符串或字节数组,请分别调用 `readText()` 或 `readBytes()`。 2339 | 2340 | 注意,虽然 `File()` 确实创建了一个“文件对象”,但实际上并没有打开文件——文件对象只是对文件路径的引用;打开文件是一个单独的操作。前面的函数会自动打开和关闭文件,而其他函数会分别打开和关闭文件。例如,如果要解析二进制数据,并且不想一次读取整个文件,那么必须创建一个 _输入流_(用于二进制数据)或一个 _输入流读取器_(用于字符串)——下面的示例将读取 16 个字节: 2341 | 2342 | ```kotlin 2343 | val stream = File("data.txt").inputStream() 2344 | val bytes = ByteArray(16) 2345 | stream.read(bytes) 2346 | stream.close() 2347 | println(bytes) 2348 | ``` 2349 | 2350 | 完成后关闭流很重要;否则,程序将泄漏文件句柄。请参见下一部分,以了解如何做到这一点。 2351 | 2352 | 如果有一个要写入文件的字符串,并且在文件已经存在的情况下覆盖现有内容,请执行此操作(同样,UTF-8 是默认编码): 2353 | 2354 | ```kotlin 2355 | File("data.txt").writeText("Hello world!") 2356 | ``` 2357 | 2358 | 如果希望逐步写入字符串,则需要通过在文件对象上调用 `writer()` 来创建一个 `OutputStreamWriter`。可以通过在文件对象上调用 `outputStream()` 并使用产生的 `OutputStream` 来写入字节,从而将二进制数据写入文件。 2359 | 2360 | 如果需要一种更高级的读取或写入文件数据的方式,那么可以访问完整的 Java I/O 类套件——特别是 `Scanner`,它可以解析文件或其他流中的数字与其他数据类型,以及 `BufferedReader`(可以高效地读取大量数据),可以通过在文件或流上调用 `bufferedReader()` 来获得该数据。请参见任何 Java 教程以了解如何使用它们。 2361 | 2362 | 2363 | ## 作用域内资源用法 2364 | 2365 | Kotlin 没有 Python 的 _资源管理器(resource managers)_ 或 Java 的 _try-with-resources_,但是多亏了扩展函数,有了 `use`: 2366 | 2367 | ```kotlin 2368 | File("/home/aasmund/test.txt").inputStream().use { 2369 | val bytes = it.readBytes() 2370 | println(bytes.size) 2371 | } 2372 | ``` 2373 | 2374 | 可以在实现 `Closeable` 接口的任何对象上调用 `use`,并且当 `use` 块结束时(无论是正常还是引发异常),都会在调用 `use` 的对象上调用 `close()`。如果在该代码块内或通过 `close()` 引发了异常,那么会抛出该异常并退出 `use` 。如果代码块与 `close()` 都抛异常了,那么最终抛出的会是来自代码块的异常。 2375 | 2376 | 因此,可以创建类似于资源管理器的东西,方法是创建一个实现 `Closeable` 的类,在 `init` 中进行设置工作,在 `close()` 中进行清理工作。 2377 | 2378 | 如果想知道如何“`use`”,它是一个函数,后面可以跟着一个这样的代码块,请参见 [DSL 支持](#接收者)一节。 2379 | 2380 | 2381 | ## 编写文档 2382 | 2383 | Kotlin 的文档语法称为 _KDoc_。 一个 KDoc 块放置在它描述的结构上方,并以 `/**` 开始,以 `*/` 结束(可能在一行上;如果没有,则每个中间行应以对齐的星号开头)。文本的第一块是摘要。然后,可以使用 _块标签_ 提供有关构造的特定部分的信息。一些块标签是用于函数参数和泛型类型参数的 `@param`,以及用于返回值的 `@return`。可以链接到方括号内的标识符。链接与块标签名称之外的所有文本均为 Markdown 格式。 2384 | 2385 | ```kotlin 2386 | /** 2387 | * Squares a number. 2388 | * 2389 | * @param number Any [Double] number whose absolute value is 2390 | * less than or equal to the square root of [Double.MAX_VALUE]. 2391 | * @return A nonnegative number: the result of multiplying [number] with itself. 2392 | */ 2393 | fun square(number: Double) = number * number 2394 | ``` 2395 | 2396 | 包级文档可以在单独的 Markdown 文件中提供。 2397 | 2398 | 与文档字符串不同,KDoc 在运行时对程序不可用。 2399 | 2400 | 可以使用名为 [Dokka](https://github.com/Kotlin/dokka/blob/master/README.md) 的工具从 KDoc 生成 HTML 格式的单独文档文件。 2401 | 2402 | --- 2403 | 2404 | *本资料英文原文的作者是 [Aasmund Eldhuset](https://eldhuset.net/);其所有权属于[可汗学院(Khan Academy)](https://www.khanacademy.org/),授权许可为 [CC BY-NC-SA 3.0 US(署名-非商业-相同方式共享)](https://creativecommons.org/licenses/by-nc-sa/3.0/us/)。中文版由[灰蓝天际](https://hltj.me/)、[Yue-plus](https://github.com/Yue-plus) 翻译,遵循相同授权方式。* 2405 | --------------------------------------------------------------------------------