基础

基本类型

数字

整数

Kotlin提供了以下几种整数类型:Byte、Short、Int、Long。

类型 大小(比特数) 最小值 最大值
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的自动推断基于类型的范围:

  • 所有以未超出 Int 最大值的整型值初始化的变量都会推断为 Int 类型
  • 如果初始值超过了其最大值,那么推断为 Long 类型
  • 如需显式指定 Long 型值,请在该值后追加 L 后缀

浮点数

Kotlin提供了以下几种浮点数类型:Float、Double,两种浮点类型的十进制位数(即可以存储多少位十进制数)不同,Float 反映了 IEEE 754 单精度,而 Double 提供了双精度。

类型 大小(比特数) 有效数字比特数 指数比特数 十进制位数
Float 32 24 8 6-7
Double 64 53 11 15-16

浮点数的推断规则如下:

  • 对于以小数初始化的变量,编译器会推断为 Double 类型
  • 如需将一个值显式指定为 Float 类型,请添加 f 或 F 后缀
1
2
val e = 2.7182818284 // Double
val eFloat = 2.7182818284f // Float,实际值为 2.7182817

TODO: 什么是单精度,什么是双精度?Double 转 Float是怎么损失精度的?

字面常量

数值常量字面值有以下几种:

  • 十进制:123
  • 十六进制:0x0F
  • 二进制:0b00001011
  • 不支持八进制

TODO 了解进制

数字面值中的下划线

可以使用下划线使得数字更易读

1
2
3
4
val oneMillion = 1_000_000
val creditCardNumber = 1234_5678_9012_3456L
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010

表示方式

数字装箱不一定保留同一性:

1
2
3
4
5
6
7
8
9
10
val a: Int = 100
val boxedA: Int? = a
val anotherBoxedA: Int? = a

val b: Int = 10000
val boxedB: Int? = b
val anotherBoxedB: Int? = b

println(boxedA === anotherBoxedA) // true
println(boxedB === anotherBoxedB) // false

另一方面,它保留了相等性:

1
2
3
4
5
val a: Int = 10000
println(a == a) // 输出“true”
val boxedA: Int? = a
val anotherBoxedA: Int? = a
println(boxedA == anotherBoxedA) // 输出“true”

显示转换

Kotlin 中的数字没有隐式拓宽转换,所以我们需要显示转换每个数字类型支持如下的转换:

1
2
3
4
5
6
7
- toByte(): Byte
- toShort(): Short
- toInt(): Int
- toLong(): Long
- toFloat(): Float
- toDouble(): Double
- toChar(): Char

运算

Kotlin支持数字运算的标准集(+ - * / %

1
2
3
//如需返回浮点类型,请将其中的一个参数显式转换为浮点类型。
val x = 5 / 2.toDouble()
println(x == 2.5) //true

位运算

位运算没有特殊字符,但是可以中缀方式调用具名函数,完整的位运算列表如下:

1
2
3
4
5
6
7
8
//只适用于 Int 和 Long
shl(bits) – 有符号左移
shr(bits) – 有符号右移
ushr(bits) – 无符号右移
and(bits) – 位与
or(bits) – 位或
xor(bits) – 位异或
inv() – 位非
1
2
//使用例子
val x = (1 shl 2) and 0x000FF000

TODO 什么是中缀表达式?了解位运算?

字符

字符用 Char 类型表示。它们不能直接当作数字,但是我们可以显式把字符转换为 Int 数字:

1
2
3
4
5
fun decimalDigitValue(c: Char): Int {
if (c !in '0'..'9')
throw IllegalArgumentException("Out of range")
return c.toInt() - '0'.toInt() // 显式转换为数字
}

当需要可空引用时,字符会被装箱,装箱不会保证一致性

布尔

布尔用 Boolean 类型表示,它有两个值:true 与 false
内置的布尔运算有:

1
2
3
|| – 短路逻辑或
&& – 短路逻辑与
! - 逻辑非

当需要可空引用时,字符会被装箱,装箱不会保证一致性

数组

Array

数组在 Kotlin 中使用 Array 类来表示,它定义了 getset 函数(按照运算符重载约定这会转变为 [])以及 size 属性,以及一些其他有用的成员函数:

1
2
3
4
5
6
7
8
class Array<T> private constructor() {
val size: Int
operator fun get(index: Int): T
operator fun set(index: Int, value: T): Unit

operator fun iterator(): Iterator<T>
// ……
}

使用 Array 一般有四种方式:

1
2
3
4
5
6
7
val array1 = arrayOf(1, 2, 3)
val array2 = arrayOfNulls<Int>(3)
val array3 = emptyArray<Int>()
// 创建一个 Array<String> 初始化为 ["0", "1", "4", "9", "16"]
val array4 = Array(5) {
(it * it).toString()
}

原生类型数组

Kotlin 也有无装箱开销的专门的类来表示原生类型数组: ByteArrayShortArrayIntArray 等等。这些类与 Array 并没有继承关系,但是它们有同样的方法属性集。它们也都有相应的工厂方法:

1
2
3
4
5
6
7
8
9
10
// 大小为 5、值为 [0, 0, 0, 0, 0] 的整型数组
val arr = IntArray(5)

// 例如:用常量初始化数组中的值
// 大小为 5、值为 [42, 42, 42, 42, 42] 的整型数组
val arr = IntArray(5) { 42 }

// 例如:使用 lambda 表达式初始化数组中的值
// 大小为 5、值为 [0, 1, 2, 3, 4] 的整型数组(值初始化为其索引值)
var arr = IntArray(5) { it * 1 }

字符串

字符串用 String 类型表示。字符串是不可变的。 字符串的元素——字符可以使用索引运算符访问: s[i]。 可以用 for 循环迭代字符串:

1
2
3
for (c in str) {
println(c)
}

原始字符串

原始字符串 使用三个引号(”””)分界符括起来,内部没有转义并且可以包含换行以及任何其他字符:

1
2
3
4
val text = """
for (c in "foo")
print(c)
"""

字符串模板

字符串字面值可以包含模板表达式 ,即一些小段代码,会求值并把结果合并到字符串中。 模板表达式以美元符($)开头,由一个简单的名字构成:

1
2
val i = 10
println("i = $i") // 输出“i = 10”

或者用花括号括起来的任意表达式:

1
2
val s = "abc"
println("$s.length is ${s.length}") // 输出“abc.length is 3”

包与导入

1.导入一个单独的名字:

1
import org.example.Message // 现在 Message 可以不用限定符访问

2.导入一个作用域下的所有内容(包、类、对象等):

1
import org.example.* // “org.example”中的一切都可访问

3.如果出现名字冲突,可以使用 as 关键字在本地重命名冲突项来消歧义:

1
2
import org.example.Message // Message 可访问
import org.test.Message as testMessage // testMessage 代表“org.test.Message”

4.关键字 import 并不仅限于导入类;也可用它来导入其他声明:

  • 顶层函数及属性;
  • 在对象声明中声明的函数和属性;
  • 枚举常量

控制流

if

在 Kotlin 中,if是一个表达式,即它会返回一个值。 因此就不需要三元运算符(条件 ? 然后 : 否则),因为普通的 if 就能胜任这个角色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 传统用法
var max = a
if (a < b) max = b

// With else
var max: Int
if (a > b) {
max = a
} else {
max = b
}

// 作为表达式
val max = if (a > b) {
print("Choose a")
a
} else {
print("Choose b")
b
}

when

when 表达式取代了类 C 语言的 switch 语句。其最简单的形式如下:

1
2
3
4
5
6
7
when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> { // 注意这个块
print("x is neither 1 nor 2")
}
}

如果很多分支需要用相同的方式处理,则可以把多个分支条件放在一起,用逗号分隔:

1
2
3
4
when (x) {
0, 1 -> print("x == 0 or x == 1")
else -> print("otherwise")
}

我们可以用任意表达式(而不只是常量)作为分支条件:

1
2
3
4
when (x) {
parseInt(s) -> print("s encodes x")
else -> print("s does not encode x")
}

自 Kotlin 1.3 起,可以使用以下语法将 when 的主语(subject,译注:指 when 所判断的表达式)捕获到变量中:

1
2
3
4
5
6
//在 when 主语中引入的变量的作用域仅限于 when 主体。
fun Request.getBody() =
when (val response = executeRequest()) {
is Success -> response.body
is HttpError -> throw HttpException(response.status)
}

最后和 if 一样, when 既可以被当做表达式使用也可以被当做语句使用。

for

for 循环可以对任何提供迭代器(iterator)的对象进行遍历,这相当于像 C# 这样的语言中的 foreach 循环。语法如下:

1
2
3
for (item in collection) {
print(item)
}

数字区间上循环

1
2
3
4
5
6
for (i in 1..3) {
println(i)
}
for (i in 6 downTo 0 step 2) {
println(i)
}

数组循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
val array = arrayOf(1, 2, 3)

//值遍历
array.forEach {
print(it)
}

//索引遍历
for (i in array.indices) {
print(array[i])
}

//索引+值遍历
for ((index, value) in array.withIndex()) {
print(array[index])
print(value)
}

while

while 与 do..while 照常使用。

1
2
3
4
5
6
7
8
while (x > 0) {
x--
}

do {
val y = retrieveData()
} while (y != null) // y 在此处可见

返回和跳转

Kotlin 有三种结构化跳转表达式:

  • return:默认从最直接包围它的函数或者匿名函数返回。
  • break:终止最直接包围它的循环。
  • continue:继续下一次最直接包围它的循环。

break 和 continue

在 Kotlin 中任何表达式都可以用标签(label)来标记。 标签的格式为标识符后跟 @ 符号,例如:abc@、fooBar@都是有效的标签。 要为一个表达式加标签,我们只要在其前加标签即可。

1
2
3
loop@ for (i in 1..100) {
// ……
}

现在,我们可以用标签限制 break 或者continue:

1
2
3
4
5
loop@ for (i in 1..100) {
for (j in 1..100) {
if (……) break@loop
}
}

return

Kotlin 有函数字面量、局部函数和对象表达式。因此 Kotlin 的函数可以被嵌套。 标签限制的 return 允许我们从外层函数返回。 最重要的一个用途就是从 lambda 表达式中返回。

如果我们需要从 lambda 表达式中返回,我们必须给它加标签并用以限制 return。

1
2
3
4
5
6
7
fun foo() {
listOf(1, 2, 3, 4, 5).forEach lit@{
if (it == 3) return@lit // 局部返回到该 lambda 表达式的调用者,即 forEach 循环
print(it)
}
print(" done with explicit label")
}

通常情况下使用隐式标签更方便。 该标签与接受该 lambda 的函数同名。

1
2
3
4
5
6
7
fun foo() {
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return@forEach // 局部返回到该 lambda 表达式的调用者,即 forEach 循环
print(it)
}
print(" done with implicit label")
}

类与对象

类与继承

Kotlin 中使用关键字 class 声明类。

1
class Invoice { /*……*/ }

类声明由类名、类头(指定其类型参数、主构造函数等)以及由花括号包围的类体构成。类头与类体都是可选的; 如果一个类没有类体,可以省略花括号。

构造函数

在 Kotlin 中的一个类可以有一个主构造函数以及一个或多个次构造函数。主构造函数是类头的一部分:它跟在类名(与可选的类型参数)后。

1
2
3
4
class Person constructor(firstName: String) { /*……*/ }

//如果主构造函数没有任何注解或者可见性修饰符,可以省略这个 constructor 关键字。
class Person(firstName: String) { /*……*/ }

主构造函数不能包含任何的代码。初始化的代码可以放到以 init 关键字作为前缀的初始化块(initializer blocks)中。

在实例初始化期间,初始化块按照它们出现在类体中的顺序执行,与属性初始化器交织在一起:

1
2
3
4
5
6
7
8
9
10
11
12
13
class InitOrderDemo(name: String) {
val firstProperty = "First property: $name".also(::println)

init {
println("First initializer block that prints ${name}")
}

val secondProperty = "Second property: ${name.length}".also(::println)

init {
println("Second initializer block that prints ${name.length}")
}
}

请注意,主构造的参数可以在初始化块中使用。它们也可以在类体内声明的属性初始化器中使用:

1
2
3
class Customer(name: String) {
val customerKey = name.toUpperCase()
}

事实上,声明属性以及从主构造函数初始化属性,Kotlin 有简洁的语法:

1
class Person(val firstName: String, val lastName: String, var age: Int) { /*……*/ }

次构造函数

类也可以声明前缀有 constructor的次构造函数:

1
2
3
4
5
6
class Person {
var children: MutableList<Person> = mutableListOf()
constructor(parent: Person) {
parent.children.add(this)
}
}

如果类有一个主构造函数,每个次构造函数需要委托给主构造函数, 可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数用 this 关键字即可:

1
2
3
4
5
6
class Person(val name: String) {
var children: MutableList<Person> = mutableListOf()
constructor(name: String, parent: Person) : this(name) {
parent.children.add(this)
}
}

请注意,初始化块中的代码实际上会成为主构造函数的一部分。委托给主构造函数会作为次构造函数的第一条语句,因此所有初始化块与属性初始化器中的代码都会在次构造函数体之前执行。即使该类没有主构造函数,这种委托仍会隐式发生,并且仍会执行初始化块。

如果一个非抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数。构造函数的可见性是 public。如果你不希望你的类有一个公有构造函数,你需要声明一个带有非默认可见性的空的主构造函数:

1
class DontCreateMe private constructor () { /*……*/ }

TODO 关于构造函数的困惑可以参考这一篇文章:https://rengwuxian.com/kotlin-basic-3/

继承

在 Kotlin 中所有类都有一个共同的超类 Any,这对于没有超类型声明的类是默认超类:

1
class Example // 从 Any 隐式继承

默认情况下,Kotlin 类是最终(final)的:它们不能被继承。 要使一个类可继承,请用 open 关键字标记它。

1
open class Base // 该类开放继承

如果派生类有一个主构造函数,其基类可以(并且必须) 用派生类主构造函数的参数就地初始化。

1
class Derived(p: Int) : Base(p)

如果派生类没有主构造函数,那么每个次构造函数必须使用 super 关键字初始化其基类型,或委托给另一个构造函数做到这一点。 注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数:

1
2
3
4
5
class MyView : View {
constructor(ctx: Context) : super(ctx)

constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}

覆盖方法

Kotlin 对于可覆盖的成员(我们称之为开放)以及覆盖后的成员需要显式修饰符:

1
2
3
4
5
6
7
8
open class Shape {
open fun draw() { /*……*/ }
fun fill() { /*……*/ }
}

class Circle() : Shape() {
override fun draw() { /*……*/ }
}

标记为 override 的成员本身是开放的,也就是说,它可以在子类中覆盖。如果你想禁止再次覆盖,使用 final 关键字:

1
2
3
open class Rectangle() : Shape() {
final override fun draw() { /*……*/ }
}

覆盖属性

属性覆盖与方法覆盖类似;在超类中声明然后在派生类中重新声明的属性必须以 override 开头,并且它们必须具有兼容的类型。 每个声明的属性可以由具有初始化器的属性或者具有 get 方法的属性覆盖。

1
2
3
4
5
6
7
open class Shape {
open val vertexCount: Int = 0
}

class Rectangle : Shape() {
override val vertexCount = 4
}

你也可以用一个 var 属性覆盖一个 val 属性,但反之则不行。 这是允许的,因为一个 val 属性本质上声明了一个 get 方法, 而将其覆盖为 var 只是在子类中额外声明一个 set 方法。

请注意,你可以在主构造函数中使用 override 关键字作为属性声明的一部分:

1
2
3
4
5
interface Shape {
val vertexCount: Int
}

class Rectangle(override val vertexCount: Int = 4) : Shape

派生类初始化顺序

在构造派生类的新实例的过程中,第一步完成其基类的初始化(在之前只有对基类构造函数参数的求值),因此发生在派生类的初始化逻辑运行之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
open class Base(val name: String) {

init { println("Initializing Base") }

open val size: Int =
name.length.also { println("Initializing size in Base: $it") }
}

class Derived(
name: String,
val lastName: String,
) : Base(name.capitalize().also { println("Argument for Base: $it") }) {

init { println("Initializing Derived") }

override val size: Int =
(super.size + lastName.length).also { println("Initializing size in Derived: $it") }
}

Constructing Derived("hello", "world")
Argument for Base: Hello
Initializing Base
Initializing size in Base: 5
Initializing Derived
Initializing size in Derived: 10

调用超类实现

派生类中的代码可以使用 super 关键字调用其超类的函数与属性访问器的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
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
}

覆盖规则

在 Kotlin 中,实现继承由下述规则规定:如果一个类从它的直接超类继承相同成员的多个实现, 它必须覆盖这个成员并提供其自己的实现(也许用继承来的其中之一)。 为了表示采用从哪个超类型继承的实现,我们使用由尖括号中超类型名限定的 super,如 super

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
open class Rectangle {
open fun draw() { /* …… */ }
}

interface Polygon {
fun draw() { /* …… */ } // 接口成员默认就是“open”的
}

class Square() : Rectangle(), Polygon {
// 编译器要求覆盖 draw():
override fun draw() {
super<Rectangle>.draw() // 调用 Rectangle.draw()
super<Polygon>.draw() // 调用 Polygon.draw()
}
}

抽象类

类以及其中的某些成员可以声明为 abstract。 抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用 open 标注一个抽象类或者函数——因为这不言而喻。

我们可以用一个抽象成员覆盖一个非抽象的开放成员:

1
2
3
4
5
6
7
open class Polygon {
open fun draw() {}
}

abstract class Rectangle : Polygon() {
abstract override fun draw()
}

伴生对象

如果你需要写一个可以无需用一个类的实例来调用、但需要访问类内部的函数(例如,工厂方法),你可以把它写成该类内对象声明中的一员。

更具体地讲,如果在你的类内声明了一个伴生对象, 你就可以访问其成员,只是以类名作为限定符。

属性与字段

声明属性

Kotlin 类中的属性既可以用关键字 var 声明为可变的,也可以用关键字 val 声明为只读的。

1
2
3
4
5
6
7
class Address {
var name: String = "Holmes, Sherlock"
var street: String = "Baker"
var city: String = "London"
var state: String? = null
var zip: String = "123456"
}

Getter 和 Setter

声明一个属性的完整语法是:

1
2
3
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]

其初始器(initializer)、getter 和 setter 都是可选的。属性类型如果可以从初始器 (或者从其 getter 返回值,如下文所示)中推断出来,也可以省略。

我们可以为属性定义自定义的访问器。如果我们定义了一个自定义的 getter,那么每次访问该属性时都会调用它 (这让我们可以实现计算出的属性)。以下是一个自定义 getter 的示例:

1
2
val isEmpty: Boolean
get() = this.size == 0

如果我们定义了一个自定义的 setter,那么每次给属性赋值时都会调用它。一个自定义的 setter 如下所示:

1
2
3
4
5
var stringRepresentation: String
get() = this.toString()
set(value) {
setDataFromString(value) // 解析字符串并赋值给其他属性
}

幕后字段

当一个属性需要一个幕后字段时,Kotlin 会自动提供。这个幕后字段可以使用field标识符在访问器中引用:

1
2
3
4
var counter = 0 // 注意:这个初始器直接为幕后字段赋值
set(value) {
if (value >= 0) field = value
}

使用幕后字段与否的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fun main() {
Test.name = "456"
print("name = " + Test.name)
}

object Test {
var name: String = "123"
set(value) {
println(value)
}
}
//结果
456
name = 123

//修改后
object Test {
var name: String = "123"
set(value) {
println(value)
field = value
}
}
//结果
456
name = 456

编译期常量

如果只读属性的值在编译期是已知的,那么可以使用 const 修饰符将其标记为编译期常量。 这种属性需要满足以下要求:

  • 位于顶层或者是 object 声明 或 companion object 的一个成员
  • 以 String 或原生类型值初始化
  • 没有自定义 getter

这些属性可以用在注解中:

1
2
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"
@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { …… }

延迟初始化属性与变量

一般地,属性声明为非空类型必须在构造函数中初始化。 然而,这经常不方便。例如:属性可以通过依赖注入来初始化, 或者在单元测试的 setup 方法中初始化。 这种情况下,你不能在构造函数内提供一个非空初始器。 但你仍然想在类体中引用该属性时避免空检测。

为处理这种情况,你可以用 lateinit 修饰符标记该属性:

1
2
3
4
5
6
7
8
9
10
11
public class MyTest {
lateinit var subject: TestSubject

@SetUp fun setup() {
subject = TestSubject()
}

@Test fun test() {
subject.method() // 直接解引用
}
}

在初始化前访问一个 lateinit 属性会抛出一个特定异常,该异常明确标识该属性被访问及它没有初始化的事实。

检测一个 lateinit var 是否已经初始化过,请在该属性的引用上使用 .isInitialized:

1
2
3
if (foo::bar.isInitialized) {
println(foo.bar)
}

接口

Kotlin 的接口可以既包含抽象方法的声明也包含实现。与抽象类不同的是,接口无法保存状态。它可以有属性但必须声明为抽象或提供访问器实现。

使用关键字 interface 来定义接口:

1
2
3
4
5
6
interface MyInterface {
fun bar()
fun foo() {
// 可选的方法体
}
}

接口中的属性

你可以在接口中定义属性。在接口中声明的属性要么是抽象的,要么提供访问器的实现。在接口中声明的属性不能有幕后字段(backing field),因此接口中声明的访问器不能引用它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface MyInterface {
val prop: Int // 抽象的

val propertyWithImplementation: String
get() = "foo"

fun foo() {
print(prop)
}
}

class Child : MyInterface {
override val prop: Int = 29
}

接口继承

一个接口可以从其他接口派生,从而既提供基类型成员的实现也声明新的函数与属性。很自然地,实现这样接口的类只需定义所缺少的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

解决覆盖冲突

实现多个接口时,可能会遇到同一方法继承多个实现的问题。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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<A>.foo()
super<B>.foo()
}

override fun bar() {
super<B>.bar()
}
}

函数式(SAM)接口

只有一个抽象方法的接口称为函数式接口或 SAM(单一抽象方法)接口。函数式接口可以有多个非抽象成员,但只能有一个抽象成员。

可以用 fun 修饰符在 Kotlin 中声明一个函数式接口。

1
2
3
fun interface KRunnable {
fun invoke()
}

使用 lambda 表达式可以替代手动创建实现函数式接口的类。 通过 SAM 转换, Kotlin 可以将其签名与接口的单个抽象方法的签名匹配的任何 lambda 表达式转换为实现该接口的类的实例。

例如,有这样一个 Kotlin 函数式接口:

1
2
3
fun interface IntPredicate {
fun accept(i: Int): Boolean
}

如果不使用 SAM 转换,那么你需要像这样编写代码:

1
2
3
4
5
6
// 创建一个类的实例
val isEven = object : IntPredicate {
override fun accept(i: Int): Boolean {
return i % 2 == 0
}
}

通过利用 Kotlin 的 SAM 转换,可以改为以下等效代码:

1
2
// 通过 lambda 表达式创建一个实例
val isEven = IntPredicate { it % 2 == 0 }

可见性修饰符

类、对象、接口、构造函数、方法、属性和它们的 setter 都可以有 可见性修饰符。 (getter 总是与属性有着相同的可见性。) 在 Kotlin 中有这四个可见性修饰符:private、 protected、 internal 和 public。 如果没有显式指定修饰符的话,默认可见性是 public。

函数、属性和类、对象和接口可以在顶层声明,即直接在包内:

1
2
3
4
5
// 文件名:example.kt
package foo

fun baz() { …… }
class Bar { …… }
  • 如果你不指定任何可见性修饰符,默认为 public,这意味着你的声明将随处可见;
  • 如果你声明为 private,它只会在声明它的文件内可见;
  • 如果你声明为 internal,它会在相同模块内随处可见;
  • protected 不适用于顶层声明。

类和接口

对于类内部声明的成员:

  • private:意味着只在这个类内部(包含其所有成员)可见;
  • protected:和 private一样 + 在子类中可见。
  • internal:能见到类声明的 本模块内 的任何客户端都可见其 internal 成员;
  • public:能见到类声明的任何客户端都可见其 public 成员。
    请注意在 Kotlin 中,外部类不能访问内部类的 private 成员。如果你覆盖一个 protected 成员并且没有显式指定其可见性,该成员还会是 protected 可见性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
open class Outer {
private val a = 1
protected open val b = 2
internal 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
}

class Unrelated(o: Outer) {
// o.a、o.b 不可见
// o.c 和 o.d 可见(相同模块)
// Outer.Nested 不可见,Nested::e 也不可见
}

模块

可见性修饰符 internal 意味着该成员只在相同模块内可见。更具体地说, 一个模块是编译在一起的一套 Kotlin 文件:

  • 一个 IntelliJ IDEA 模块;
  • 一个 Maven 项目;
  • 一个 Gradle 源集(例外是 test 源集可以访问 main 的 internal 声明);
  • 一次 Ant 任务执行所编译的一套文件。

扩展

Kotlin 能够扩展一个类的新功能而无需继承该类或者使用像装饰者这样的设计模式。 这通过叫做 扩展 的特殊声明完成。 例如,你可以为一个你不能修改的、来自第三方库中的类编写一个新的函数。 这个新增的函数就像那个原始类本来就有的函数一样,可以用普通的方法调用。 这种机制称为 扩展函数 。此外,也有 扩展属性 , 允许你为一个已经存在的类添加新的属性。

扩展函数

声明一个扩展函数,我们需要用一个 接收者类型 也就是被扩展的类型来作为他的前缀。 下面代码为 MutableList 添加一个swap 函数:

1
2
3
4
5
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // “this”对应该列表
this[index1] = this[index2]
this[index2] = tmp
}

这个 this 关键字在扩展函数内部对应到接收者对象(传过来的在点符号前的对象) 现在,我们对任意 MutableList 调用该函数了:

1
2
val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // “swap()”内部的“this”会保存“list”的值

当然,这个函数对任何 MutableList 起作用,我们可以泛化它:

1
2
3
4
5
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // “this”对应该列表
this[index1] = this[index2]
this[index2] = tmp
}

为了在接收者类型表达式中使用泛型,我们要在函数名前声明泛型参数。

扩展是静态解析的

扩展不能真正的修改他们所扩展的类。通过定义一个扩展,你并没有在一个类中插入新成员, 仅仅是可以通过该类型的变量用点表达式去调用这个新函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
open class Shape

class Rectangle: Shape()

fun Shape.getName() = "Shape"

fun Rectangle.getName() = "Rectangle"

fun printClassName(s: Shape) {
println(s.getName())
}

printClassName(Rectangle())

这个例子会输出 “Shape”,因为调用的扩展函数只取决于参数 s 的声明类型,该类型是 Shape 类。

如果一个类定义有一个成员函数与一个扩展函数,而这两个函数又有相同的接收者类型、 相同的名字,并且都适用给定的参数,这种情况总是取成员函数。 例如:

1
2
3
4
5
6
7
8
9
class Example {
fun printFunctionType() { println("Class method") }
}

fun Example.printFunctionType() { println("Extension function") }

Example().printFunctionType()

//结果 Class method

可空接受者

注意可以为可空的接收者类型定义扩展。这样的扩展可以在对象变量上调用, 即使其值为 null,并且可以在函数体内检测 this == null,这能让你在没有检测 null 的时候调用 Kotlin 中的toString():检测发生在扩展函数的内部。

1
2
3
4
5
6
fun Any?.toString(): String {
if (this == null) return "null"
// 空检测之后,“this”会自动转换为非空类型,所以下面的 toString()
// 解析为 Any 类的成员函数
return toString()
}

扩展属性

与函数类似,Kotlin 支持扩展属性:

1
2
val <T> List<T>.lastIndex: Int
get() = size - 1

注意:由于扩展没有实际的将成员插入类中,因此对扩展属性来说幕后字段是无效的。这就是为什么扩展属性不能有初始化器。他们的行为只能由显式提供的 getters/setters 定义。

伴生对象的扩展

如果一个类定义有一个伴生对象 ,你也可以为伴生对象定义扩展函数与属性。就像伴生对象的常规成员一样, 可以只使用类名作为限定符来调用伴生对象的扩展成员:

1
2
3
4
5
6
7
8
9
class MyClass {
companion object { } // 将被称为 "Companion"
}

fun MyClass.Companion.printCompanion() { println("companion") }

fun main() {
MyClass.printCompanion()
}

扩展的作用域

大多数时候我们在顶层定义扩展——直接在包里:

1
2
3
package org.example.declarations

fun List<String>.getLongestString() { /*……*/}

要使用所定义包之外的一个扩展,我们需要在调用方导入它:

1
2
3
4
5
6
7
8
package org.example.usage

import org.example.declarations.getLongestString

fun main() {
val list = listOf("red", "green", "blue")
list.getLongestString()
}

扩展例子

File 扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* Gets the entire content of this file as a String using UTF-8 or specified [charset].
*
* This method is not recommended on huge files. It has an internal limitation of 2 GB file size.
*
* @param charset character set to use.
* @return the entire content of this file as a String.
*/
public fun File.readText(charset: Charset = Charsets.UTF_8): String = reader(charset).use { it.readText() }

/**
* Sets the content of this file as [text] encoded using UTF-8 or specified [charset].
* If this file exists, it becomes overwritten.
*
* @param text text to write into file.
* @param charset character set to use.
*/
public fun File.writeText(text: String, charset: Charset = Charsets.UTF_8): Unit = writeBytes(text.toByteArray(charset))

/**
* Appends [text] to the content of this file using UTF-8 or the specified [charset].
*
* @param text text to append to file.
* @param charset character set to use.
*/
public fun File.appendText(text: String, charset: Charset = Charsets.UTF_8): Unit = appendBytes(text.toByteArray(charset))

/**
* Delete this file with all its children.
* Note that if this operation fails then partial deletion may have taken place.
*
* @return `true` if the file or directory is successfully deleted, `false` otherwise.
*/
public fun File.deleteRecursively(): Boolean = walkBottomUp().fold(true, { res, it -> (it.delete() || !it.exists()) && res })

/**
* Determines whether this file belongs to the same root as [other]
* and starts with all components of [other] in the same order.
* So if [other] has N components, first N components of `this` must be the same as in [other].
*
* @return `true` if this path starts with [other] path, `false` otherwise.
*/
public fun File.startsWith(other: File): Boolean {
val components = toComponents()
val otherComponents = other.toComponents()
if (components.root != otherComponents.root)
return false
return if (components.size < otherComponents.size) false
else components.segments.subList(0, otherComponents.size).equals(otherComponents.segments)
}

Closeable 扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@InlineOnly
@RequireKotlin("1.2", versionKind = RequireKotlinVersionKind.COMPILER_VERSION, message = "Requires newer compiler version to be inlined correctly.")
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
when {
apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception)
this == null -> {}
exception == null -> close()
else ->
try {
close()
} catch (closeException: Throwable) {
// cause.addSuppressed(closeException) // ignored here
}
}
}
}

数据类

我们经常创建一些只保存数据的类。 在这些类中,一些标准函数往往是从数据机械推导而来的。在 Kotlin 中,这叫做 数据类 并标记为 data:

1
data class User(val name: String, val age: Int)

编译器自动从主构造函数中声明的所有属性导出以下成员:

  • equals()/hashCode() 对;
  • toString() 格式是 “User(name=John, age=42)”;
  • componentN() 函数 按声明顺序对应于所有属性;
  • copy() 函数。

为了确保生成的代码的一致性以及有意义的行为,数据类必须满足以下要求:

  • 主构造函数需要至少有一个参数;
  • 主构造函数的所有参数需要标记为 val 或 var;
  • 数据类不能是抽象、开放、密封或者内部的;

复制

在很多情况下,我们需要复制一个对象改变它的一些属性,但其余部分保持不变。 copy() 函数就是为此而生成。对于上文的 User 类,其实现会类似下面这样:

1
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)

这让我们可以写:

1
2
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)

数据类与结构声明

为数据类生成的 Component 函数 使它们可在解构声明中使用:

1
2
3
val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age") // 输出 "Jane, 35 years of age"

标准数据类

标准库提供了 Pair 与 Triple。尽管在很多情况下具名数据类是更好的设计选择, 因为它们通过为属性提供有意义的名称使代码更具可读性。

1
2
3
4
5
6
7
//Pair
val test = Pair("age", 1)
print(test.first + ":" + test.second)

//Triple
val test = Triple("age", 1, 2)
print(test.first + ":" + test.second + test.third)

密封类

密封类用来表示受限的类继承结构:当一个值为有限几种的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。

要声明一个密封类,需要在类名前面添加 sealed 修饰符。虽然密封类也可以有子类,但是所有子类都必须在与密封类自身相同的文件中声明。

1
2
3
4
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

一个密封类是自身抽象的,它不能直接实例化并可以有抽象(abstract)成员。

密封类不允许有非-private 构造函数(其构造函数默认为 private)。

请注意,扩展密封类子类的类(间接继承者)可以放在任何位置,而无需在同一个文件中。

使用密封类的关键好处在于使用 when 表达式 的时候,如果能够验证语句覆盖了所有情况,就不需要为该语句再添加一个 else 子句了。当然,这只有当你用 when 作为表达式(使用结果)而不是作为语句时才有用。

1
2
3
4
5
6
fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
// 不再需要 `else` 子句,因为我们已经覆盖了所有的情况
}

泛型

暂不处理

嵌套类

类可以嵌套在其他类中:

1
2
3
4
5
6
7
8
class Outer {
private val bar: Int = 1
class Nested {
fun foo() = 2
}
}

val demo = Outer.Nested().foo() // == 2

内部类

标记为 inner 的嵌套类能够访问其外部类的成员。内部类会带有一个对外部类的对象的引用:

1
2
3
4
5
6
7
8
class Outer {
private val bar: Int = 1
inner class Inner {
fun foo() = bar
}
}

val demo = Outer().Inner().foo() // == 1

枚举类

枚举类的最基本的用法是实现类型安全的枚举:

1
2
3
enum class Direction {
NORTH, SOUTH, WEST, EAST
}

每个枚举常量都是一个对象。枚举常量用逗号分隔。

初始化
因为每一个枚举都是枚举类的实例,所以他们可以是这样初始化过的:

1
2
3
4
5
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}

匿名类
枚举常量还可以声明其带有相应方法以及覆盖了基类方法的匿名类。

1
2
3
4
5
6
7
8
9
10
11
enum class ProtocolState {
WAITING {
override fun signal() = TALKING
},

TALKING {
override fun signal() = WAITING
};

abstract fun signal(): ProtocolState
}

如果枚举类定义任何成员,那么使用分号将成员定义中的枚举常量定义分隔开。

在枚举类中实现接口
一个枚举类可以实现接口(但不能从类继承),可以为所有条目提供统一的接口成员实现,也可以在相应匿名类中为每个条目提供各自的实现。只需将接口添加到枚举类声明中即可,如下所示:

1
2
3
4
5
6
7
8
9
10
enum class IntArithmetics : BinaryOperator<Int>, 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)
}

使用枚举常量
Kotlin 中的枚举类也有合成方法允许列出定义的枚举常量以及通过名称获取枚举常量。这些方法的签名如下(假设枚举类的名称是 EnumClass):

1
2
EnumClass.valueOf(value: String): EnumClass
EnumClass.values(): Array<EnumClass>

如果指定的名称与类中定义的任何枚举常量均不匹配,valueOf() 方法将抛出 IllegalArgumentException 异常。

自 Kotlin 1.1 起,可以使用 enumValues() 与 enumValueOf() 函数以泛型的方式访问枚举类中的常量 :

1
2
3
4
5
6
7
enum class RGB { RED, GREEN, BLUE }

inline fun <reified T : Enum<T>> printAllValues() {
print(enumValues<T>().joinToString { it.name })
}

printAllValues<RGB>() // 输出 RED, GREEN, BLUE

每个枚举常量都具有在枚举类声明中获取其名称与位置的属性:

1
2
val name: String
val ordinal: Int

枚举常量还实现了 Comparable 接口, 其中自然顺序是它们在枚举类中定义的顺序。

对象

对象表达式

要创建一个继承自某个(或某些)类型的匿名类的对象,我们会这么写:

1
2
3
4
5
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { /*……*/ }

override fun mouseEntered(e: MouseEvent) { /*……*/ }
})

如果超类型有一个构造函数,则必须传递适当的构造函数参数给它。 多个超类型可以由跟在冒号后面的逗号分隔的列表指定:

1
2
3
4
5
6
7
8
9
open class A(x: Int) {
public open val y: Int = x
}

interface B { /*……*/ }

val ab: A = object : A(1), B {
override val y = 15
}

任何时候,如果我们只需要“一个对象而已”,并不需要特殊超类型,那么我们可以简单地写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun foo() {
val adHoc = object {
var x: Int = 0
var y: Int = 0
}
print(adHoc.x + adHoc.y)
}
fun foo() {
val adHoc = object {
var x: Int = 0
var y: Int = 0
}
print(adHoc.x + adHoc.y)
}

请注意,匿名对象可以用作只在本地和私有作用域中声明的类型。如果你使用匿名对象作为公有函数的返回类型或者用作公有属性的类型,那么该函数或属性的实际类型会是匿名对象声明的超类型,如果你没有声明任何超类型,就会是 Any。在匿名对象中添加的成员将无法访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class C {
// 私有函数,所以其返回类型是匿名对象类型
private fun foo() = object {
val x: String = "x"
}

// 公有函数,所以其返回类型是 Any
fun publicFoo() = object {
val x: String = "x"
}

fun bar() {
val x1 = foo().x // 没问题
val x2 = publicFoo().x // 错误:未能解析的引用“x”
}
}

对象声明

单例模式在一些场景中很有用, 而 Kotlin(继 Scala 之后)使单例声明变得很容易:

1
2
3
4
5
6
7
8
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ……
}

val allDataProviders: Collection<DataProvider>
get() = // ……
}

这称为对象声明。并且它总是在 object 关键字后跟一个名称。 就像变量声明一样,对象声明不是一个表达式,不能用在赋值语句的右边。

对象声明的初始化过程是线程安全的并且在首次访问时进行。

如需引用该对象,我们直接使用其名称即可:

1
DataProviderManager.registerDataProvider(……)

这些对象可以有超类型:

1
2
3
4
5
object DefaultListener : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { …… }

override fun mouseEntered(e: MouseEvent) { …… }
}

注意:对象声明不能在局部作用域(即直接嵌套在函数内部),但是它们可以嵌套到其他对象声明或非内部类中。

伴生对象

类内部的对象声明可以用 companion 关键字标记:

1
2
3
4
5
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}

该伴生对象的成员可通过只使用类名作为限定符来调用:

1
val instance = MyClass.create()

可以省略伴生对象的名称,在这种情况下将使用名称 Companion:

1
2
3
4
5
class MyClass {
companion object { }
}

val x = MyClass.Companion

其自身所用的类的名称(不是另一个名称的限定符)可用作对该类的伴生对象 (无论是否具名)的引用:

1
2
3
4
5
6
7
8
9
10
11
class MyClass1 {
companion object Named { }
}

val x = MyClass1

class MyClass2 {
companion object { }
}

val y = MyClass2

请注意,即使伴生对象的成员看起来像其他语言的静态成员,在运行时他们仍然是真实对象的实例成员,而且,例如还可以实现接口:

1
2
3
4
5
6
7
8
9
10
11
interface Factory<T> {
fun create(): T
}

class MyClass {
companion object : Factory<MyClass> {
override fun create(): MyClass = MyClass()
}
}

val f: Factory<MyClass> = MyClass

当然,在 JVM 平台,如果使用 @JvmStatic 注解,你可以将伴生对象的成员生成为真正的静态方法和字段。

对象表达式和对象声明之间的语义差异

对象表达式和对象声明之间有一个重要的语义差别:

  • 对象表达式是在使用他们的地方立即执行(及初始化)的;
  • 对象声明是在第一次被访问到时延迟初始化的;
  • 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配。

类型别名

类型别名为现有类型提供替代名称。 如果类型名称太长,你可以另外引入较短的名称,并使用新的名称替代原类型名。

它有助于缩短较长的泛型类型。 例如,通常缩减集合类型是很有吸引力的:

1
2
3
typealias NodeSet = Set<Network.Node>

typealias FileTable<K> = MutableMap<K, MutableList<File>>

你可以为函数类型提供另外的别名:

1
2
3
typealias MyHandler = (Int, String, Any) -> Unit

typealias Predicate<T> = (T) -> Boolean

你可以为内部类和嵌套类创建新名称:

1
2
3
4
5
6
7
8
9
class A {
inner class Inner
}
class B {
inner class Inner
}

typealias AInner = A.Inner
typealias BInner = B.Inner

类型别名不会引入新类型。 它们等效于相应的底层类型。 当你在代码中添加 typealias Predicate 并使用 Predicate 时,Kotlin 编译器总是把它扩展为 (Int) -> Boolean。 因此,当你需要泛型函数类型时,你可以传递该类型的变量,反之亦然:

1
2
3
4
5
6
7
8
9
10
11
typealias Predicate<T> = (T) -> Boolean

fun foo(p: Predicate<Int>) = p(42)

fun main() {
val f: (Int) -> Boolean = { it > 0 }
println(foo(f)) // 输出 "true"

val p: Predicate<Int> = { it > 0 }
println(listOf(1, -2).filter(p)) // 输出 "[1]"
}

内联类

内联类仅在 Kotlin 1.3 之后版本可用,目前处于 Alpha 版,先不讨论。

委托

委托模式已经证明是实现继承的一个很好的替代方式, 而 Kotlin 可以零样板代码地原生支持它。 Derived 类可以通过将其所有公有成员都委托给指定对象来实现一个接口 Base:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 b = BaseImpl(10)
Derived(b).print()
}

Derived 的超类型列表中的 by-子句表示 b 将会在 Derived 中内部存储, 并且编译器将生成转发给 b 的所有 Base 的方法。

覆盖由委托实现的接口成员

覆盖符合预期:编译器会使用 override 覆盖的实现而不是委托对象中的。如果将 override fun printMessage() { print(“abc”) } 添加到 Derived,那么当调用 printMessage 时程序会输出“abc”而不是“10”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 b = BaseImpl(10)
Derived(b).printMessage()
Derived(b).printMessageLine()
}

但请注意,以这种方式重写的成员不会在委托对象的成员中调用 ,委托对象的成员只能访问其自身对接口成员实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface Base {
val message: String
fun print()
}

class BaseImpl(val 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)
}

//结果
BaseImpl: x = 10
Message of Derived

委托属性

有一些常见的属性类型,虽然我们可以在每次需要的时候手动实现它们, 但是如果能够为大家把他们只实现一次并放入一个库会更好。例如包括:

  • 延迟属性(lazy properties): 其值只在首次访问时计算;
  • 可观察属性(observable properties): 监听器会收到有关此属性变更的通知;
  • 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。

为了涵盖这些(以及其他)情况,Kotlin 支持 委托属性:

1
2
3
class Example {
var p: String by Delegate()
}

语法是: val/var <属性名>: <类型> by <表达式>。在 by 后面的表达式是该 委托, 因为属性对应的 get()(与 set())会被委托给它的 getValue() 与 setValue() 方法。 属性的委托不必实现任何的接口,但是需要提供一个 getValue() 函数(与 setValue()——对于 var 属性)。 例如:

1
2
3
4
5
6
7
8
9
10
11
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.")
}
}

当我们从委托到一个 Delegate 实例的 p 读取时,将调用 Delegate 中的 getValue() 函数, 所以它第一个参数是读出 p 的对象、第二个参数保存了对 p 自身的描述 (例如你可以取它的名字)。 例如:

1
2
val e = Example()
println(e.p)

输出结果:

1
Example@33a17727, thank you for delegating ‘p’ to me!

类似地,当我们给 p 赋值时,将调用 setValue() 函数。前两个参数相同,第三个参数保存将要被赋予的值:

1
e.p = "NEW"

输出结果:

1
NEW has been assigned to ‘p’ in Example@33a17727.

标准委托

延迟属性 Lazy

lazy() 是接受一个 lambda 并返回一个 Lazy 实例的函数,返回的实例可以作为实现延迟属性的委托: 第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。

1
2
3
4
5
6
7
8
9
val lazyValue: String by lazy {
println("computed!")
"Hello"
}

fun main() {
println(lazyValue)
println(lazyValue)
}

默认情况下,对于 lazy 属性的求值是同步锁的(synchronized):该值只在一个线程中计算,并且所有线程会看到相同的值。如果初始化委托的同步锁不是必需的,这样多个线程可以同时执行,那么将 LazyThreadSafetyMode.PUBLICATION 作为参数传递给 lazy() 函数。 而如果你确定初始化将总是发生在与属性使用位于相同的线程, 那么可以使用 LazyThreadSafetyMode.NONE 模式:它不会有任何线程安全的保证以及相关的开销。

可观察属性 Observable

Delegates.observable() 接受两个参数:初始值与修改时处理程序(handler)。 每当我们给属性赋值时会调用该处理程序(在赋值后执行)。它有三个参数:被赋值的属性、旧值与新值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import kotlin.properties.Delegates

class User {
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("$old -> $new")
}
}

fun main() {
val user = User()
user.name = "first"
user.name = "second"
}

如果你想截获赋值并“否决”它们,那么使用 vetoable() 取代 observable()。 在属性被赋新值生效之前会调用传递给 vetoable 的处理程序。

委托给另一个属性

从 Kotlin 1.4 开始,一个属性可以把它的 getter 与 setter 委托给另一个属性。这种委托对于顶层和类的属性(成员和扩展)都可用。该委托属性可以为:

  • 顶层属性
  • 同一个类的成员或扩展属性
  • 另一个类的成员或扩展属性
    为将一个属性委托给另一个属性,应在委托名称中使用恰当的 :: 限定符,例如,this::delegate 或 MyClass::delegate。
    1
    2
    3
    4
    5
    6
    7
    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 注解来注解旧的属性、并委托其实现。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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' is deprecated.
    // Use 'newName' instead
    myClass.oldName = 42
    println(myClass.newName) // 42
    }

将属性储存在映射中

一个常见的用例是在一个映射(map)里存储属性的值。 这经常出现在像解析 JSON 或者做其他“动态”事情的应用中。 在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。

1
2
3
4
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}

在这个例子中,构造函数接受一个映射参数:

1
2
3
4
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))

委托属性会从这个映射中取值(通过字符串键——属性的名称):

1
2
println(user.name) // Prints "John Doe"
println(user.age) // Prints 25

这也适用于 var 属性,如果把只读的 Map 换成 MutableMap 的话:

1
2
3
4
class MutableUser(val map: MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
}

局部委托属性

你可以将局部变量声明为委托属性。 例如,你可以使一个局部变量惰性初始化:

1
2
3
4
5
6
7
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)

if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}

memoizedFoo 变量只会在第一次访问时计算。 如果 someCondition 失败,那么该变量根本不会计算。

属性委托要求

这里我们总结了委托对象的要求。

对于一个只读属性(即 val 声明的),委托必须提供一个操作符函数 getValue(),该函数具有以下参数:

  • thisRef —— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是其超类型。
  • property —— 必须是类型 KProperty<*> 或其超类型。

getValue() 必须返回与属性相同的类型(或其子类型)。

1
2
3
4
5
6
7
8
9
10
11
class Resource

class Owner {
val valResource: Resource by ResourceDelegate()
}

class ResourceDelegate {
operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
return Resource()
}
}

对于一个可变属性(即 var 声明的),委托必须额外提供一个操作符函数 setValue(), 该函数具有以下参数:

  • thisRef —— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是其超类型。
  • property —— 必须是类型 KProperty<*> 或其超类型。
  • value — 必须与属性类型相同(或者是其超类型)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 关键字来进行标记。

函数与 Lambda 表达式

函数

函数声明

Kotlin 中的函数使用 fun 关键字声明:

1
2
3
fun double(x: Int): Int {
return 2 * x
}

函数用法

调用函数使用传统的方法:

1
val result = double(2)

调用成员函数使用点表示法:

1
Stream().read() // 创建类 Stream 实例并调用 read()

参数

函数参数使用 Pascal 表示法定义,即 name: type。参数用逗号隔开。 每个参数必须有显式类型:

1
2
3
4
5
6
7
fun powerOf(number: Int, exponent: Int): Int { /*……*/ }
You can use a trailing comma when you declare function parameters:

fun powerOf(
number: Int,
exponent: Int, // trailing comma
) { /*...*/ }

默认参数

函数参数可以有默认值,当省略相应的参数时使用默认值。与其他语言相比,这可以减少重载数量:

1
2
3
4
5
6
fun read(
b: Array<Byte>,
off: Int = 0,
len: Int = b.size,
) { /*……*/ }
A default value is defined using the = after the type.

覆盖方法总是使用与基类型方法相同的默认参数值。 当覆盖一个带有默认参数值的方法时,必须从签名中省略默认参数值:

1
2
3
4
5
6
7
open class A {
open fun foo(i: Int = 10) { /*……*/ }
}

class B : A() {
override fun foo(i: Int) { /*……*/ } // 不能有默认值
}

如果一个默认参数在一个无默认值的参数之前,那么该默认值只能通过使用具名参数调用该函数来使用:

1
2
3
4
5
6
fun foo(
bar: Int = 0,
baz: Int,
) { /*……*/ }

foo(baz = 1) // 使用默认值 bar = 0

如果在默认参数之后的最后一个参数是 lambda 表达式,那么它既可以作为具名参数在括号内传入,也可以在括号外传入:

1
2
3
4
5
6
7
8
9
fun foo(
bar: Int = 0,
baz: Int = 1,
qux: () -> Unit,
) { /*……*/ }

foo(1) { println("hello") } // 使用默认值 baz = 1
foo(qux = { println("hello") }) // 使用两个默认值 bar = 0 与 baz = 1
foo { println("hello") } // 使用两个默认值 bar = 0 与 baz = 1

单表达式函数

当函数返回单个表达式时,可以省略花括号并且在 = 符号之后指定代码体即可:

1
fun double(x: Int): Int = x * 2

当返回值类型可由编译器推断时,显式声明返回类型是可选的:

1
fun double(x: Int) = x * 2

可变数量的参数(Varargs)

函数的参数(通常是最后一个)可以用 vararg 修饰符标记:

1
2
3
4
5
6
fun <T> asList(vararg ts: T): List<T> {
val result = ArrayList<T>()
for (t in ts) // ts is an Array
result.add(t)
return result
}

允许将可变数量的参数传递给函数:

1
val list = asList(1, 2, 3)

在函数内部,类型 T 的 vararg 参数的可见方式是作为 T 数组,即上例中的 ts 变量具有类型 Array

只有一个参数可以标注为 vararg。如果 vararg 参数不是列表中的最后一个参数, 可以使用具名参数语法传递其后的参数的值,或者,如果参数具有函数类型,则通过在括号外部传一个 lambda。

当我们调用 vararg-函数时,我们可以一个接一个地传参,例如 asList(1, 2, 3),或者,如果我们已经有一个数组并希望将其内容传给该函数,我们使用伸展(spread)操作符(在数组前面加 *):

1
2
val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)

中缀表示法

标有 infix 关键字的函数也可以使用中缀表示法(忽略该调用的点与圆括号)调用。中缀函数必须满足以下要求:

  • 它们必须是成员函数或扩展函数;
  • 它们必须只有一个参数;
  • 其参数不得接受可变数量的参数且不能有默认值。
1
2
3
4
5
6
7
infix fun Int.shl(x: Int): Int { …… }

// 用中缀表示法调用该函数
1 shl 2

// 等同于这样
1.shl(2)

中缀函数调用的优先级低于算术操作符、类型转换以及 rangeTo 操作符。 以下表达式是等价的:

1
2
3
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- 检测以及其他一些操作符。这些表达式也是等价的:

1
2
a && b xor c 等价于 a && (b xor c)
a xor b in c 等价于 (a xor b) in c

请注意,中缀函数总是要求指定接收者与参数。当使用中缀表示法在当前接收者上调用方法时,需要显式使用 this;不能像常规方法调用那样省略。这是确保非模糊解析所必需的。

1
2
3
4
5
6
7
8
9
class MyStringCollection {
infix fun add(s: String) { /*……*/ }

fun build() {
this add "abc" // 正确
add("abc") // 正确
//add "abc" // 错误:必须指定接收者
}
}

函数作用域

在 Kotlin 中函数可以在文件顶层声明,这意味着你不需要像一些语言如 Java、C# 或 Scala 那样需要创建一个类来保存一个函数。此外除了顶层函数,Kotlin 中函数也可以声明在局部作用域、作为成员函数以及扩展函数。

局部函数

Kotlin 支持局部函数,即一个函数在另一个函数内部:

1
2
3
4
5
6
7
8
9
fun dfs(graph: Graph) {
fun dfs(current: Vertex, visited: MutableSet<Vertex>) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v, visited)
}

dfs(graph.vertices[0], HashSet())
}

局部函数可以访问外部函数(即闭包)的局部变量,所以在上例中,visited 可以是局部变量:

1
2
3
4
5
6
7
8
9
10
fun dfs(graph: Graph) {
val visited = HashSet<Vertex>()
fun dfs(current: Vertex) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v)
}

dfs(graph.vertices[0])
}

成员函数

成员函数是在类或对象内部定义的函数:

1
2
3
class Sample {
fun foo() { print("Foo") }
}

成员函数以点表示法调用:

1
Sample().foo() // 创建类 Sample 实例并调用 foo

关于类和覆盖成员的更多信息参见类和继承。

泛型函数

函数可以有泛型参数,通过在函数名前使用尖括号指定:

1
fun <T> singletonList(item: T): List<T> { /*……*/ }

关于泛型函数的更多信息参见泛型。

内联函数

内联函数将在本文稍后部分讲述。

扩展函数

扩展函数在之前已经提到过了。

高阶函数和 Lambda 表达式

高阶函数和 Lambda 表达式将在本文稍后部分讲述。

尾递归函数

Kotlin 支持一种称为尾递归的函数式编程风格。 这允许一些通常用循环写的算法改用递归函数来写,而无堆栈溢出的风险。 当一个函数用 tailrec 修饰符标记并满足所需的形式时,编译器会优化该递归,留下一个快速而高效的基于循环的版本:

1
2
3
4
val eps = 1E-10 // "good enough", could be 10^-15

tailrec fun findFixPoint(x: Double = 1.0): Double
= if (Math.abs(x - Math.cos(x)) < eps) x else findFixPoint(Math.cos(x))

这段代码计算余弦的不动点(fixpoint of cosine),这是一个数学常数。 它只是重复地从 1.0 开始调用 Math.cos,直到结果不再改变,对于这里指定的 eps 精度会产生 0.7390851332151611 的结果。最终代码相当于这种更传统风格的代码:

1
2
3
4
5
6
7
8
9
10
val eps = 1E-10 // "good enough", could be 10^-15

private fun findFixPoint(): Double {
var x = 1.0
while (true) {
val y = Math.cos(x)
if (Math.abs(x - y) < eps) return x
x = Math.cos(x)
}
}

要符合 tailrec 修饰符的条件的话,函数必须将其自身调用作为它执行的最后一个操作。在递归调用后有更多代码时,不能使用尾递归,并且不能用在 try/catch/finally 块中。目前在 Kotlin for JVM 与 Kotlin/Native 中支持尾递归。

Lambda 表达式

高阶函数

高阶函数是将函数用作参数或返回值的函数。

一个不错的示例是集合的函数式风格的 fold, 它接受一个初始累积值与一个接合函数,并通过将当前累积值与每个集合元素连续接合起来代入累积值来构建返回值:

1
2
3
4
5
6
7
8
9
10
fun <T, R> Collection<T>.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 具有函数类型 (R, T) -> R,因此 fold 接受一个函数作为参数, 该函数接受类型分别为 R 与 T 的两个参数并返回一个 R 类型的值。 在 for-循环内部调用该函数,然后将其返回值赋值给 accumulator。

为了调用 fold,需要传给它一个函数类型的实例作为参数,而在高阶函数调用处,(下文详述的)lambda 表达 式广泛用于此目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
val items = listOf(1, 2, 3, 4, 5)

// Lambdas 表达式是花括号括起来的代码块。
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)

函数类型

Kotlin 使用类似 (Int) -> String 的一系列函数类型来处理函数的声明: val onClick: () -> Unit = ……。

这些类型具有与函数签名相对应的特殊表示法,即它们的参数和返回值:

  • 所有函数类型都有一个圆括号括起来的参数类型列表以及一个返回类型:(A, B) -> C 表示接受类型分别为 A 与 B 两个参数并返回一个 C 类型值的函数类型。 参数类型列表可以为空,如 () -> A。Unit 返回类型不可省略。
  • 函数类型可以有一个额外的接收者类型,它在表示法中的点之前指定: 类型 A.(B) -> C 表示可以在 A 的接收者对象上以一个 B 类型参数来调用并返回一个 C 类型值的函数。 带有接收者的函数字面值通常与这些类型一起使用。
  • 挂起函数属于特殊种类的函数类型,它的表示法中有一个 suspend 修饰符 ,例如 suspend () -> Unit 或者 suspend A.(B) -> C。

函数类型表示法可以选择性地包含函数的参数名:(x: Int, y: Int) -> Point。 这些名称可用于表明参数的含义。

如需将函数类型指定为可空,请使用圆括号:((Int, Int) -> Int)?。
函数类型可以使用圆括号进行接合:(Int) -> ((Int) -> Unit)
箭头表示法是右结合的,(Int) -> (Int) -> Unit 与前述示例等价,但不等于 ((Int) -> (Int)) -> Unit。

还可以通过使用类型别名给函数类型起一个别称:

1
typealias ClickHandler = (Button, ClickEvent) -> Unit

函数类型实例化

有几种方法可以获得函数类型的实例:

1.使用函数字面值的代码块,采用以下形式之一:

  • lambda 表达式: { a, b -> a + b },
  • 匿名函数: fun(s: String): Int { return s.toIntOrNull() ?: 0 }

带有接收者的函数字面值可用作带有接收者的函数类型的值。

2.使用已有声明的可调用引用:

  • 顶层、局部、成员、扩展函数:::isOdd、 String::toInt,
  • 顶层、成员、扩展属性:List::size,
  • 构造函数:::Regex

这包括指向特定实例成员的绑定的可调用引用:foo::toString。

使用实现函数类型接口的自定义类的实例:

1
2
3
4
5
class IntTransformer: (Int) -> Int {
override operator fun invoke(x: Int): Int = TODO()
}

val intFunction: (Int) -> Int = IntTransformer()

如果有足够信息,编译器可以推断变量的函数类型:

1
val a = { i: Int -> i + 1 } // 推断出的类型是 (Int) -> Int

带与不带接收者的函数类型非字面值可以互换,其中接收者可以替代第一个参数,反之亦然。例如,(A, B) -> C 类型的值可以传给或赋值给期待 A.(B) -> C 的地方,反之亦然:

1
2
3
4
5
6
7
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

请注意,默认情况下推断出的是没有接收者的函数类型,即使变量是通过扩展函数引用来初始化的。 如需改变这点,请显式指定变量类型。

函数类型实例调用

函数类型的值可以通过其 invoke(……) 操作符调用:f.invoke(x) 或者直接 f(x)。

如果该值具有接收者类型,那么应该将接收者对象作为第一个参数传递。 调用带有接收者的函数类型值的另一个方式是在其前面加上接收者对象, 就好比该值是一个扩展函数:1.foo(2),

例如:

1
2
3
4
5
6
7
8
9
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)) // 类扩展调用

Lambda 表达式与匿名函数

lambda 表达式与匿名函数是“函数字面值”,即未声明的函数, 但立即做为表达式传递。考虑下面的例子:

1
max(strings, { a, b -> a.length < b.length })

函数 max 是一个高阶函数,它接受一个函数作为第二个参数。 其第二个参数是一个表达式,它本身是一个函数,即函数字面值,它等价于以下具名函数:

1
fun compare(a: String, b: String): Boolean = a.length < b.length

Lambda 表达式语法

Lambda 表达式的完整语法形式如下:

1
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }

lambda 表达式总是括在花括号中, 完整语法形式的参数声明放在花括号内,并有可选的类型标注, 函数体跟在一个 -> 符号之后。如果推断出的该 lambda 的返回类型不是 Unit,那么该 lambda 主体中的最后一个(或可能是单个) 表达式会视为返回值。

如果我们把所有可选标注都留下,看起来如下:

1
val sum = { x: Int, y: Int -> x + y }

传递末尾的 lambda 表达式

在 Kotlin 中有一个约定:如果函数的最后一个参数是函数,那么作为相应参数传入的 lambda 表达式可以放在圆括号之外:

1
2
3
4
5
6
7
val product = items.fold(1) { acc, e -> acc * e }
````
这种语法也称为拖尾 lambda 表达式。

如果该 lambda 表达式是调用时唯一的参数,那么圆括号可以完全省略:
```kotlin
run { println("...") }

it:单个参数的隐式名称

一个 lambda 表达式只有一个参数是很常见的。

如果编译器自己可以识别出签名,也可以不用声明唯一的参数并忽略 ->。 该参数会隐式声明为 it:

1
ints.filter { it > 0 } // 这个字面值是“(it: Int) -> Boolean”类型的

从 lambda 表达式中返回一个值

我们可以使用限定的返回语法从 lambda 显式返回一个值。 否则,将隐式返回最后一个表达式的值。

因此,以下两个片段是等价的:

1
2
3
4
5
6
7
8
9
ints.filter {
val shouldFilter = it > 0
shouldFilter
}

ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}

下划线用于未使用的变量

如果 lambda 表达式的参数未使用,那么可以用下划线取代其名称:

1
map.forEach { _, value -> println("$value!") }

匿名函数

上面提供的 lambda 表达式语法缺少的一个东西是指定函数的返回类型的能力。在大多数情况下,这是不必要的。因为返回类型可以自动推断出来。然而,如果确实需要显式指定,可以使用另一种语法: 匿名函数 。

1
fun(x: Int, y: Int): Int = x + y

匿名函数看起来非常像一个常规函数声明,除了其名称省略了。其函数体可以是表达式(如上所示)或代码块:

1
2
3
fun(x: Int, y: Int): Int {
return x + y
}

参数和返回类型的指定方式与常规函数相同,除了能够从上下文推断出的参数类型可以省略:

1
ints.filter(fun(item) = item > 0)

匿名函数的返回类型推断机制与正常函数一样:对于具有表达式函数体的匿名函数将自动推断返回类型,而具有代码块函数体的返回类型必须显式指定(或者已假定为 Unit)。

请注意,匿名函数参数总是在括号内传递。 允许将函数留在圆括号外的简写语法仅适用于 lambda 表达式。

Lambda表达式与匿名函数之间的另一个区别是非局部返回的行为。一个不带标签的 return 语句总是在用 fun 关键字声明的函数中返回。这意味着 lambda 表达式中的 return 将从包含它的函数返回,而匿名函数中的 return 将从匿名函数自身返回。

闭包

Lambda 表达式或者匿名函数(以及局部函数和对象表达式) 可以访问其 闭包 ,即在外部作用域中声明的变量。 在 lambda 表达式中可以修改闭包中捕获的变量:

1
2
3
4
5
var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)

带有接收者的函数字面值

带有接收者的函数类型,例如 A.(B) -> C,可以用特殊形式的函数字面值实例化—— 带有接收者的函数字面值。

如上所述,Kotlin 提供了调用带有接收者(提供接收者对象)的函数类型实例的能力。

在这样的函数字面值内部,传给调用的接收者对象成为隐式的this,以便访问接收者对象的成员而无需任何额外的限定符,亦可使用 this 表达式 访问接收者对象。

这种行为与扩展函数类似,扩展函数也允许在函数体内部访问接收者对象的成员。

这里有一个带有接收者的函数字面值及其类型的示例,其中在接收者对象上调用了 plus :

1
val sum: Int.(Int) -> Int = { other -> plus(other) }

匿名函数语法允许你直接指定函数字面值的接收者类型。 如果你需要使用带接收者的函数类型声明一个变量,并在之后使用它,这将非常有用。

1
val sum = fun Int.(other: Int): Int = this + other

当接收者类型可以从上下文推断时,lambda 表达式可以用作带接收者的函数字面值。

1
2
3
4
5
6
7
8
9
10
11
12
13
class HTML {
fun body() { …… }
}

fun html(init: HTML.() -> Unit): HTML {
val html = HTML() // 创建接收者对象
html.init() // 将该接收者对象传给该 lambda
return html
}

html { // 带接收者的 lambda 由此开始
body() // 调用该接收者对象的一个方法
}

内联函数

使用高阶函数会带来一些运行时的效率损失:每一个函数都是一个对象,并且会捕获一个闭包。 即那些在函数体内会访问到的变量。 内存分配(对于函数对象和类)和虚拟调用会引入运行时间开销。

但是在许多情况下通过内联化 lambda 表达式可以消除这类的开销。 下述函数是这种情况的很好的例子。即 lock() 函数可以很容易地在调用处内联。 考虑下面的情况:

1
lock(l) { foo() }

编译器没有为参数创建一个函数对象并生成一个调用。取而代之,编译器可以生成以下代码:

1
2
3
4
5
6
7
l.lock()
try {
foo()
}
finally {
l.unlock()
}

这个不是我们从一开始就想要的吗?

为了让编译器这么做,我们需要使用 inline 修饰符标记 lock() 函数:

1
inline fun <T> lock(lock: Lock, body: () -> T): T { …… }

inline 修饰符影响函数本身和传给它的 lambda 表达式:所有这些都将内联到调用处。

内联可能导致生成的代码增加;不过如果我们使用得当(即避免内联过大函数),性能上会有所提升,尤其是在循环中的“超多态(megamorphic)”调用处。

禁用内联

如果希望只内联一部分传给内联函数的 lambda 表达式参数,那么可以用 noinline 修饰符标记不希望内联的函数参数:

1
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { …… }

可以内联的 lambda 表达式只能在内联函数内部调用或者作为可内联的参数传递, 但是 noinline 的可以以任何我们喜欢的方式操作:存储在字段中、传送它等等。

需要注意的是,如果一个内联函数没有可内联的函数参数并且没有具体化的类型参数,编译器会产生一个警告,因为内联这样的函数很可能并无益处(如果你确认需要内联,则可以用 @Suppress(“NOTHING_TO_INLINE”) 注解关掉该警告)。

非局部返回

在 Kotlin 中,我们只能对具名或匿名函数使用正常的、非限定的 return 来退出。 这意味着要退出一个 lambda 表达式,我们必须使用一个标签,并且在 lambda 表达式内部禁止使用裸 return,因为 lambda 表达式不能使包含它的函数返回:

1
2
3
4
5
fun foo() {
ordinaryFunction {
return // 错误:不能使 `foo` 在此处返回
}
}

但是如果 lambda 表达式传给的函数是内联的,该 return 也可以内联,所以它是允许的:

1
2
3
4
5
fun foo() {
inlined {
return // OK:该 lambda 表达式是内联的
}
}

这种返回(位于 lambda 表达式中,但退出包含它的函数)称为非局部返回。 我们习惯了在循环中用这种结构,其内联函数通常包含:

1
2
3
4
5
6
fun hasZeros(ints: List<Int>): Boolean {
ints.forEach {
if (it == 0) return true // 从 hasZeros 返回
}
return false
}

请注意,一些内联函数可能调用传给它们的不是直接来自函数体、而是来自另一个执行上下文的 lambda 表达式参数,例如来自局部对象或嵌套函数。在这种情况下,该 lambda 表达式中也不允许非局部控制流。为了标识这种情况,该 lambda 表达式参数需要用 crossinline 修饰符标记:

1
2
3
4
5
6
inline fun f(crossinline body: () -> Unit) {
val f = object: Runnable {
override fun run() = body()
}
// ……
}

break 和 continue 在内联的 lambda 表达式中还不可用,但我们也计划支持它们。

具体化的类型参数

有时候我们需要访问一个作为参数传给我们的一个类型:

1
2
3
4
5
6
7
8
fun <T> TreeNode.findParentOfType(clazz: Class<T>): T? {
var p = parent
while (p != null && !clazz.isInstance(p)) {
p = p.parent
}
@Suppress("UNCHECKED_CAST")
return p as T?
}

在这里我们向上遍历一棵树并且检测每个节点是不是特定的类型。 这都没有问题,但是调用处不是很优雅:

1
treeNode.findParentOfType(MyTreeNode::class.java)

我们真正想要的只是传一个类型给该函数,即像这样调用它:

1
treeNode.findParentOfType<MyTreeNode>()

为能够这么做,内联函数支持具体化的类型参数,于是我们可以这样写:

1
2
3
4
5
6
7
inline fun <reified T> TreeNode.findParentOfType(): T? {
var p = parent
while (p != null && p !is T) {
p = p.parent
}
return p as T?
}

我们使用 reified 修饰符来限定类型参数,现在可以在函数内部访问它了, 几乎就像是一个普通的类一样。由于函数是内联的,不需要反射,正常的操作符如 !is 和 as 现在都能用了。此外,我们还可以按照上面提到的方式调用它:

1
myTree.findParentOfType<MyTreeNodeType>()。

虽然在许多情况下可能不需要反射,但我们仍然可以对一个具体化的类型参数使用它:

1
2
3
4
5
inline fun <reified T> membersOf() = T::class.members

fun main(s: Array<String>) {
println(membersOf<StringBuilder>().joinToString("\n"))
}

普通的函数(未标记为内联函数的)不能有具体化参数。 不具有运行时表示的类型(例如非具体化的类型参数或者类似于Nothing的虚构类型) 不能用作具体化的类型参数的实参。

内联属性

inline 修饰符可用于没有幕后字段的属性的访问器。 你可以标注独立的属性访问器:

1
2
3
4
5
6
val foo: Foo
inline get() = Foo()

var bar: Bar
get() = ……
inline set(v) { …… }

你也可以标注整个属性,将它的两个访问器都标记为内联:

1
2
3
inline var bar: Bar
get() = ……
set(v) { …… }

在调用处,内联访问器如同内联函数一样内联。

集合

集合概述

集合类型

Kotlin 标准库提供了基本集合类型的实现: set、list 以及 map。 一对接口代表每种集合类型:

  • 一个 只读 接口,提供访问集合元素的操作。
  • 一个 可变 接口,通过写操作扩展相应的只读接口:添加、删除和更新其元素。

请注意,更改可变集合不需要它是以 var 定义的变量:写操作修改同一个可变集合对象,因此引用不会改变。 但是,如果尝试对 val 集合重新赋值,你将收到编译错误。

1
2
3
val numbers = mutableListOf("one", "two", "three", "four")
numbers.add("five") // 这是可以的
//numbers = mutableListOf("six", "seven") // 编译错误

只读集合类型是型变的。 这意味着,如果类 Rectangle 继承自 Shape,则可以在需要 List 的任何地方使用 List 。 换句话说,集合类型与元素类型具有相同的子类型关系。 map 在值(value)类型上是型变的,但在键(key)类型上不是。

反之,可变集合不是型变的;否则将导致运行时故障。 如果 MutableList 是 MutableList 的子类型,你可以在其中插入其他 Shape 的继承者(例如,Circle),从而违反了它的 Rectangle 类型参数。

下面是 Kotlin 集合接口的图表:

Collection interfaces hierarchy

让我们来看看接口及其实现。

Collection

Collection 是集合层次结构的根。此接口表示一个只读集合的共同行为:检索大小、检测是否为成员等等。 Collection 继承自 Iterable 接口,它定义了迭代元素的操作。可以使用 Collection 作为适用于不同集合类型的函数的参数。对于更具体的情况,请使用 Collection 的继承者: List 与 Set。

1
2
3
4
5
6
7
8
9
10
11
12
fun printAll(strings: Collection<String>) {
for(s in strings) print("$s ")
println()
}

fun main() {
val stringList = listOf("one", "two", "one")
printAll(stringList)

val stringSet = setOf("one", "two", "three")
printAll(stringSet)
}

MutableCollection 是一个具有写操作的 Collection 接口,例如 add 以及 remove。

1
2
3
4
5
6
7
8
9
10
11
12
13
fun List<String>.getShortWordsTo(shortWords: MutableList<String>, maxLength: Int) {
this.filterTo(shortWords) { it.length <= maxLength }
// throwing away the articles
val articles = setOf("a", "A", "an", "An", "the", "The")
shortWords -= articles
}

fun main() {
val words = "A long time ago in a galaxy far far away".split(" ")
val shortWords = mutableListOf<String>()
words.getShortWordsTo(shortWords, 3)
println(shortWords)
}

List

List 以指定的顺序存储元素,并提供使用索引访问元素的方法。索引从 0 开始 – 第一个元素的索引 – 直到 最后一个元素的索引 即 (list.size - 1)。

1
2
3
4
5
val numbers = listOf("one", "two", "three", "four")
println("Number of elements: ${numbers.size}")
println("Third element: ${numbers.get(2)}")
println("Fourth element: ${numbers[3]}")
println("Index of element \"two\" ${numbers.indexOf("two")}")

List 元素(包括空值)可以重复:List 可以包含任意数量的相同对象或单个对象的出现。 如果两个 List 在相同的位置具有相同大小和相同结构的元素,则认为它们是相等的。

1
2
3
4
5
6
val bob = Person("Bob", 31)
val people = listOf(Person("Adam", 20), bob, bob)
val people2 = listOf(Person("Adam", 20), Person("Bob", 31), bob)
println(people == people2)
bob.age = 32
println(people == people2)

MutableList 是可以进行写操作的 List,例如用于在特定位置添加或删除元素。

1
2
3
4
5
6
val numbers = mutableListOf(1, 2, 3, 4)
numbers.add(5)
numbers.removeAt(1)
numbers[0] = 0
numbers.shuffle()
println(numbers)

如你所见,在某些方面,List 与数组(Array)非常相似。 但是,有一个重要的区别:数组的大小是在初始化时定义的,永远不会改变; 反之,List 没有预定义的大小;作为写操作的结果,可以更改 List 的大小:添加,更新或删除元素。

在 Kotlin 中,List 的默认实现是 ArrayList,可以将其视为可调整大小的数组。

Set

Set 存储唯一的元素;它们的顺序通常是未定义的。null 元素也是唯一的:一个 Set 只能包含一个 null。当两个 set 具有相同的大小并且对于一个 set 中的每个元素都能在另一个 set 中存在相同元素,则两个 set 相等。

1
2
3
4
5
6
val numbers = setOf(1, 2, 3, 4)
println("Number of elements: ${numbers.size}")
if (numbers.contains(1)) println("1 is in the set")

val numbersBackwards = setOf(4, 3, 2, 1)
println("The sets are equal: ${numbers == numbersBackwards}")

MutableSet 是一个带有来自 MutableCollection 的写操作接口的 Set。

Set的默认实现 - LinkedHashSet – 保留元素插入的顺序。 因此,依赖于顺序的函数,例如 first() 或 last(),会在这些 set 上返回可预测的结果。

1
2
3
4
5
val numbers = setOf(1, 2, 3, 4)  // LinkedHashSet is the default implementation
val numbersBackwards = setOf(4, 3, 2, 1)

println(numbers.first() == numbersBackwards.first())
println(numbers.first() == numbersBackwards.last())

另一种实现方式 – HashSet – 不声明元素的顺序,所以在它上面调用这些函数会返回不可预测的结果。但是,HashSet 只需要较少的内存来存储相同数量的元素。

Map

Map<K, V> 不是 Collection 接口的继承者;但是它也是 Kotlin 的一种集合类型。 Map 存储 键-值 对(或 条目);键是唯一的,但是不同的键可以与相同的值配对。Map 接口提供特定的函数进行通过键访问值、搜索键和值等操作。

1
2
3
4
5
6
7
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)

println("All keys: ${numbersMap.keys}")
println("All values: ${numbersMap.values}")
if ("key2" in numbersMap) println("Value by key \"key2\": ${numbersMap["key2"]}")
if (1 in numbersMap.values) println("The value 1 is in the map")
if (numbersMap.containsValue(1)) println("The value 1 is in the map") // 同上

无论键值对的顺序如何,包含相同键值对的两个 Map 是相等的。

1
2
3
4
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)    
val anotherMap = mapOf("key2" to 2, "key1" to 1, "key4" to 1, "key3" to 3)

println("The maps are equal: ${numbersMap == anotherMap}")

MutableMap 是一个具有写操作的 Map 接口,可以使用该接口添加一个新的键值对或更新给定键的值。

1
2
3
4
5
val numbersMap = mutableMapOf("one" to 1, "two" to 2)
numbersMap.put("three", 3)
numbersMap["one"] = 11

println(numbersMap)

Map 的默认实现 – LinkedHashMap – 迭代 Map 时保留元素插入的顺序。 反之,另一种实现 – HashMap – 不声明元素的顺序。

构造集合

由元素构造

创建集合的最常用方法是使用标准库函数 listOf()、setOf()、mutableListOf()、mutableSetOf()。 如果以逗号分隔的集合元素列表作为参数,编译器会自动检测元素类型。创建空集合时,须明确指定类型。\

1
2
val numbersSet = setOf("one", "two", "three", "four")
val emptySet = mutableSetOf<String>()

同样的,Map 也有这样的函数 mapOf() 与 mutableMapOf()。映射的键和值作为 Pair 对象传递(通常使用中缀函数 to 创建)。

1
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)

注意,to 符号创建了一个短时存活的 Pair 对象,因此建议仅在性能不重要时才使用它。 为避免过多的内存使用,请使用其他方法。例如,可以创建可写 Map 并使用写入操作填充它。 apply() 函数可以帮助保持初始化流畅。

1
val numbersMap = mutableMapOf<String, String>().apply { this["one"] = "1"; this["two"] = "2" }

空集合

还有用于创建没有任何元素的集合的函数:emptyList()、emptySet() 与 emptyMap()。 创建空集合时,应指定集合将包含的元素类型。

1
val empty = emptyList<String>()

list 的初始化函数

对于 List,有一个接受 List 的大小与初始化函数的构造函数,该初始化函数根据索引定义元素的值。

1
2
val doubled = List(3, { it * 2 })  // 如果你想操作这个集合,应使用 MutableList
println(doubled)

具体类型构造函数

要创建具体类型的集合,例如 ArrayList 或 LinkedList,可以使用这些类型的构造函数。 类似的构造函数对于 Set 与 Map 的各实现中均有提供。

1
2
val linkedList = LinkedList<String>(listOf("one", "two", "three"))
val presizedSet = HashSet<Int>(32)

复制

要创建与现有集合具有相同元素的集合,可以使用复制操作。标准库中的集合复制操作创建了具有相同元素引用的 浅 复制集合。 因此,对集合元素所做的更改会反映在其所有副本中。

在特定时刻通过集合复制函数,例如toList()、toMutableList()、toSet() 等等。创建了集合的快照。 结果是创建了一个具有相同元素的新集合 如果在源集合中添加或删除元素,则不会影响副本。副本也可以独立于源集合进行更改。

1
2
3
4
5
6
7
8
val sourceList = mutableListOf(1, 2, 3)
val copyList = sourceList.toMutableList()
val readOnlyCopyList = sourceList.toList()
sourceList.add(4)
println("Copy size: ${copyList.size}")

//readOnlyCopyList.add(4) // 编译异常
println("Read-only copy size: ${readOnlyCopyList.size}")

这些函数还可用于将集合转换为其他类型,例如根据 List 构建 Set,反之亦然。

1
2
3
4
5
val sourceList = mutableListOf(1, 2, 3)    
val copySet = sourceList.toMutableSet()
copySet.add(3)
copySet.add(4)
println(copySet)

或者,可以创建对同一集合实例的新引用。使用现有集合初始化集合变量时,将创建新引用。 因此,当通过引用更改集合实例时,更改将反映在其所有引用中。

1
2
3
4
val sourceList = mutableListOf(1, 2, 3)
val referenceList = sourceList
referenceList.add(4)
println("Source size: ${sourceList.size}")

集合的初始化可用于限制其可变性。例如,如果构建了一个 MutableList 的 List 引用,当你试图通过此引用修改集合的时候,编译器会抛出错误。

1
2
3
4
5
val sourceList = mutableListOf(1, 2, 3)
val referenceList: List<Int> = sourceList
//referenceList.add(4) // 编译错误
sourceList.add(4)
println(referenceList) // 显示 sourceList 当前状态

调用其他集合的函数

可以通过其他集合各种操作的结果来创建集合。例如,过滤列表会创建与过滤器匹配的新元素列表:

1
2
3
val numbers = listOf("one", "two", "three", "four")  
val longerThan3 = numbers.filter { it.length > 3 }
println(longerThan3)

映射生成转换结果列表:

1
2
3
val numbers = setOf(1, 2, 3)
println(numbers.map { it * 3 })
println(numbers.mapIndexed { idx, value -> value * idx })

关联生成 Map:

1
2
val numbers = listOf("one", "two", "three", "four")
println(numbers.associateWith { it.length })

迭代器

对于遍历集合元素, Kotlin 标准库支持 迭代器 的常用机制——对象可按顺序提供对元素的访问权限,而不会暴露集合的底层结构。 当需要逐个处理集合的所有元素(例如打印值或对其进行类似更新)时,迭代器非常有用。

Iterable 接口的继承者(包括 Set 与 List)可以通过调用 iterator() 函数获得迭代器。 一旦获得迭代器它就指向集合的第一个元素;调用 next() 函数将返回此元素,并将迭代器指向下一个元素(如果下一个元素存在)。 一旦迭代器通过了最后一个元素,它就不能再用于检索元素;也无法重新指向到以前的任何位置。要再次遍历集合,请创建一个新的迭代器。

1
2
3
4
5
val numbers = listOf("one", "two", "three", "four")
val numbersIterator = numbers.iterator()
while (numbersIterator.hasNext()) {
println(numbersIterator.next())
}

遍历 Iterable 集合的另一种方法是众所周知的 for 循环。在集合中使用 for 循环时,将隐式获取迭代器。因此,以下代码与上面的示例等效:

1
2
3
4
val numbers = listOf("one", "two", "three", "four")
for (item in numbers) {
println(item)
}

最后,有一个好用的 forEach() 函数,可自动迭代集合并为每个元素执行给定的代码。因此,等效的示例如下所示:

1
2
3
4
val numbers = listOf("one", "two", "three", "four")
numbers.forEach {
println(it)
}

List 迭代器

对于列表,有一个特殊的迭代器实现: ListIterator 它支持列表双向迭代:正向与反向。 反向迭代由 hasPrevious() 和 previous() 函数实现。 此外, ListIterator 通过 nextIndex() 与 previousIndex() 函数提供有关元素索引的信息。

1
2
3
4
5
6
7
8
val numbers = listOf("one", "two", "three", "four")
val listIterator = numbers.listIterator()
while (listIterator.hasNext()) listIterator.next()
println("Iterating backwards:")
while (listIterator.hasPrevious()) {
print("Index: ${listIterator.previousIndex()}")
println(", value: ${listIterator.previous()}")
}

具有双向迭代的能力意味着 ListIterator 在到达最后一个元素后仍可以使用。

可变迭代器

为了迭代可变集合,于是有了 MutableIterator 来扩展 Iterator 使其具有元素删除函数 remove() 。因此,可以在迭代时从集合中删除元素。

1
2
3
4
5
6
val numbers = mutableListOf("one", "two", "three", "four") 
val mutableIterator = numbers.iterator()

mutableIterator.next()
mutableIterator.remove()
println("After removal: $numbers")

除了删除元素, MutableListIterator 还可以在迭代列表时插入和替换元素。

1
2
3
4
5
6
7
8
val numbers = mutableListOf("one", "four", "four") 
val mutableListIterator = numbers.listIterator()

mutableListIterator.next()
mutableListIterator.add("two")
mutableListIterator.next()
mutableListIterator.set("three")
println(numbers)

区间和数列

Kotlin 可通过调用 kotlin.ranges 包中的 rangeTo() 函数及其操作符形式的 .. 轻松地创建两个值的区间。 通常,rangeTo() 会辅以 in 或 !in 函数。

1
2
3
if (i in 1..4) {  // 等同于 1 <= i && i <= 4
print(i)
}

整数类型区间(IntRange、LongRange、CharRange)还有一个拓展特性:可以对其进行迭代。 这些区间也是相应整数类型的等差数列。 这种区间通常用于 for 循环中的迭代。

1
for (i in 1..4) print(i)

要反向迭代数字,请使用 downTo 函数而不是 .. 。

1
for (i in 4 downTo 1) print(i)

也可以通过任意步长(不一定为 1 )迭代数字。 这是通过 step 函数完成的。

1
2
3
for (i in 1..8 step 2) print(i)
println()
for (i in 8 downTo 1 step 2) print(i)

要迭代不包含其结束元素的数字区间,请使用 until 函数:

1
2
3
for (i in 1 until 10) {       // i in [1, 10), 10被排除
print(i)
}

区间

区间从数学意义上定义了一个封闭的间隔:它由两个端点值定义,这两个端点值都包含在该区间内。 区间是为可比较类型定义的:具有顺序,可以定义任意实例是否在两个给定实例之间的区间内。 区间的主要操作是 contains,通常以 in 与 !in 操作符的形式使用。

要为类创建一个区间,请在区间起始值上调用 rangeTo() 函数,并提供结束值作为参数。 rangeTo() 通常以操作符 .. 形式调用。

1
2
3
val versionRange = Version(1, 11)..Version(1, 30)
println(Version(0, 9) in versionRange)
println(Version(1, 20) in versionRange)

数列

如上个示例所示,整数类型的区间(例如 Int、Long 与 Char)可视为等差数列。 在 Kotlin 中,这些数列由特殊类型定义:IntProgression、LongProgression 与 CharProgression。

数列具有三个基本属性:first 元素、last 元素和一个非零的 step。 首个元素为 first,后续元素是前一个元素加上一个 step。 以确定的步长在数列上进行迭代等效于 Java/JavaScript 中基于索引的 for 循环。

1
2
3
for (int i = first; i <= last; i += step) {
// ……
}

通过迭代数列隐式创建区间时,此数列的 first 与 last 元素是区间的端点,step 为 1 。

1
for (i in 1..10) print(i)

要指定数列步长,请在区间上使用 step 函数。

1
for (i in 1..8 step 2) print(i)

数列的 last 元素是这样计算的:

  • 对于正步长:不大于结束值且满足 (last - first) % step == 0 的最大值。
  • 对于负步长:不小于结束值且满足 (last - first) % step == 0 的最小值。

因此,last 元素并非总与指定的结束值相同。

1
for (i in 1..9 step 3) print(i) // 最后一个元素是 7

要创建反向迭代的数列,请在定义其区间时使用 downTo 而不是 ..。

1
for (i in 4 downTo 1) print(i)

数列实现 Iterable,其中 N 分别是 Int、Long 或 Char,因此可以在各种集合函数(如 map、filter 与其他)中使用它们。

1
println((1..10).filter { it % 2 == 0 })

序列

除了集合之外,Kotlin 标准库还包含另一种容器类型——序列(Sequence)。 序列提供与 Iterable 相同的函数,但实现另一种方法来进行多步骤集合处理。

当 Iterable 的处理包含多个步骤时,它们会优先执行:每个处理步骤完成并返回其结果——中间集合。 在此集合上执行以下步骤。反过来,序列的多步处理在可能的情况下会延迟执行:仅当请求整个处理链的结果时才进行实际计算。

操作执行的顺序也不同:Sequence 对每个元素逐个执行所有处理步骤。 反过来,Iterable 完成整个集合的每个步骤,然后进行下一步。

因此,这些序列可避免生成中间步骤的结果,从而提高了整个集合处理链的性能。 但是,序列的延迟性质增加了一些开销,这些开销在处理较小的集合或进行更简单的计算时可能很重要。 因此,应该同时考虑使用 Sequence 与 Iterable,并确定在哪种情况更适合。

构造

由元素

要创建一个序列,请调用 sequenceOf() 函数,列出元素作为其参数。

1
val numbersSequence = sequenceOf("four", "three", "two", "one")

由 Iterable

如果已经有一个 Iterable 对象(例如 List 或 Set),则可以通过调用 asSequence() 从而创建一个序列。

1
2
val numbers = listOf("one", "two", "three", "four")
val numbersSequence = numbers.asSequence()

由函数

创建序列的另一种方法是通过使用计算其元素的函数来构建序列。 要基于函数构建序列,请以该函数作为参数调用 generateSequence()。 (可选)可以将第一个元素指定为显式值或函数调用的结果。 当提供的函数返回 null 时,序列生成停止。因此,以下示例中的序列是无限的。

1
2
3
val oddNumbers = generateSequence(1) { it + 2 } // `it` 是上一个元素
println(oddNumbers.take(5).toList())
//println(oddNumbers.count()) // 错误:此序列是无限的。

要使用 generateSequence() 创建有限序列,请提供一个函数,该函数在需要的最后一个元素之后返回 null。

1
2
val oddNumbersLessThan10 = generateSequence(1) { if (it + 2 < 10) it + 2 else null }
println(oddNumbersLessThan10.count())

由组块

最后,有一个函数可以逐个或按任意大小的组块生成序列元素——sequence() 函数。 此函数采用一个 lambda 表达式,其中包含 yield() 与 yieldAll() 函数的调用。 它们将一个元素返回给序列使用者,并暂停 sequence() 的执行,直到使用者请求下一个元素。 yield() 使用单个元素作为参数;yieldAll() 中可以采用 Iterable 对象、Iterable 或其他 Sequence。yieldAll() 的 Sequence 参数可以是无限的。 当然,这样的调用必须是最后一个:之后的所有调用都永远不会执行。

1
2
3
4
5
6
val oddNumbers = sequence {
yield(1)
yieldAll(listOf(3, 5))
yieldAll(generateSequence(7) { it + 2 })
}
println(oddNumbers.take(5).toList())

序列操作

关于序列操作,根据其状态要求可以分为以下几类:

  • 无状态 操作不需要状态,并且可以独立处理每个元素,例如 map() 或 filter()。 无状态操作还可能需要少量常数个状态来处理元素,例如 take() 与 drop()。
  • 有状态 操作需要大量状态,通常与序列中元素的数量成比例。

如果序列操作返回延迟生成的另一个序列,则称为 中间序列。 否则,该操作为 末端 操作。 末端操作的示例为 toList() 或 sum()。只能通过末端操作才能检索序列元素。

序列可以多次迭代;但是,某些序列实现可能会约束自己仅迭代一次。其文档中特别提到了这一点。

序列处理示例

我们通过一个示例来看 Iterable 与 Sequence 之间的区别。

Iterable

假定有一个单词列表。下面的代码过滤长于三个字符的单词,并打印前四个单词的长度。

1
2
3
4
5
6
7
val words = "The quick brown fox jumps over the lazy dog".split(" ")
val lengthsList = words.filter { println("filter: $it"); it.length > 3 }
.map { println("length: ${it.length}"); it.length }
.take(4)

println("Lengths of first 4 words longer than 3 chars:")
println(lengthsList)

运行此代码时,会看到 filter() 与 map() 函数的执行顺序与代码中出现的顺序相同。 首先,将看到 filter:对于所有元素,然后是 length:对于在过滤之后剩余的元素,然后是最后两行的输出。 列表处理如下图:

List processing

Sequence

现在用序列写相同的逻辑:

1
2
3
4
5
6
7
8
9
10
11
val words = "The quick brown fox jumps over the lazy dog".split(" ")
// 将列表转换为序列
val wordsSequence = words.asSequence()

val lengthsSequence = wordsSequence.filter { println("filter: $it"); it.length > 3 }
.map { println("length: ${it.length}"); it.length }
.take(4)

println("Lengths of first 4 words longer than 3 chars")
// 末端操作:以列表形式获取结果。
println(lengthsSequence.toList())

此代码的输出表明,仅在构建结果列表时才调用 filter() 与 map() 函数。 因此,首先看到文本 “Lengths of..” 的行,然后开始进行序列处理。 请注意,对于过滤后剩余的元素,映射在过滤下一个元素之前执行。 当结果大小达到 4 时,处理将停止,因为它是 take(4) 可以返回的最大大小。

序列处理如下图:

Sequences processing

在此示例中,序列处理需要 18 个步骤,而不是 23 个步骤来执行列表操作。

操作概述

扩展与成员函数

集合操作在标准库中以两种方式声明:集合接口的成员函数和扩展函数。

成员函数定义了对于集合类型是必不可少的操作。例如,Collection 包含函数 isEmpty() 来检查其是否为空; List 包含用于对元素进行索引访问的 get(),等等。

创建自己的集合接口实现时,必须实现其成员函数。 为了使新实现的创建更加容易,请使用标准库中集合接口的框架实现:AbstractCollection、AbstractList、AbstractSet、AbstractMap 及其相应可变抽象类。

其他集合操作被声明为扩展函数。这些是过滤、转换、排序和其他集合处理功能。

公共操作

公共操作可用于只读集合与可变集合。 常见操作分为以下几类:

  • 集合转换
  • 集合过滤
  • plus 与 minus 操作符
  • 分组
  • 取集合的一部分
  • 取单个元素
  • 集合排序
  • 集合聚合操作

这些页面中描述的操作将返回其结果,而不会影响原始集合。例如,一个过滤操作产生一个新集合,其中包含与过滤谓词匹配的所有元素。 此类操作的结果应存储在变量中,或以其他方式使用,例如,传到其他函数中。

1
2
3
4
5
val numbers = listOf("one", "two", "three", "four")  
numbers.filter { it.length > 3 } // `numbers` 没有任何改变,结果丢失
println("numbers are still $numbers")
val longerThan3 = numbers.filter { it.length > 3 } // 结果存储在 `longerThan3` 中
println("numbers longer than 3 chars are $longerThan3")

对于某些集合操作,有一个选项可以指定 目标 对象。 目标是一个可变集合,该函数将其结果项附加到该可变对象中,而不是在新对象中返回它们。 对于执行带有目标的操作,有单独的函数,其名称中带有 To 后缀,例如,用 filterTo() 代替 filter() 以及用 associateTo() 代替 associate()。 这些函数将目标集合作为附加参数。

1
2
3
4
5
val numbers = listOf("one", "two", "three", "four")
val filterResults = mutableListOf<String>() // 目标对象
numbers.filterTo(filterResults) { it.length > 3 }
numbers.filterIndexedTo(filterResults) { index, _ -> index == 0 }
println(filterResults) // 包含两个操作的结果

为了方便起见,这些函数将目标集合返回了,因此您可以在函数调用的相应参数中直接创建它:

1
2
3
4
// 将数字直接过滤到新的哈希集中,
// 从而消除结果中的重复项
val result = numbers.mapTo(HashSet()) { it.length }
println("distinct item lengths are $result")

具有目标的函数可用于过滤、关联、分组、展平以及其他操作。 有关目标操作的完整列表,请参见 Kotlin collections reference。

写操作

对于可变集合,还存在可更改集合状态的 写操作 。这些操作包括添加、删除和更新元素。写操作在集合写操作以及 List 写操作与 Map 写操作的相应部分中列出。

对于某些操作,有成对的函数可以执行相同的操作:一个函数就地应用该操作,另一个函数将结果作为单独的集合返回。 例如, sort() 就地对可变集合进行排序,因此其状态发生了变化; sorted() 创建一个新集合,该集合包含按排序顺序相同的元素。

1
2
3
4
5
val numbers = mutableListOf("one", "two", "three", "four")
val sortedNumbers = numbers.sorted()
println(numbers == sortedNumbers) // false
numbers.sort()
println(numbers == sortedNumbers) // true

转换

Kotlin 标准库为集合 转换 提供了一组扩展函数。 这些函数根据提供的转换规则从现有集合中构建新集合。 在此页面中,我们将概述可用的集合转换函数。

映射

映射 转换从另一个集合的元素上的函数结果创建一个集合。 基本的映射函数是 map()。 它将给定的 lambda 函数应用于每个后续元素,并返回 lambda 结果列表。 结果的顺序与元素的原始顺序相同。 如需应用还要用到元素索引作为参数的转换,请使用 mapIndexed()。

1
2
3
4
5
6
7
val numbers = setOf(1, 2, 3)
println(numbers.map { it * 3 })
println(numbers.mapIndexed { idx, value -> value * idx })

//结果
[3, 6, 9]
[0, 2, 6]

如果转换在某些元素上产生 null 值,则可以通过调用 mapNotNull() 函数取代 map() 或 mapIndexedNotNull() 取代 mapIndexed() 来从结果集中过滤掉 null 值。

1
2
3
4
5
6
7
val numbers = setOf(1, 2, 3)
println(numbers.mapNotNull { if ( it == 2) null else it * 3 })
println(numbers.mapIndexedNotNull { idx, value -> if (idx == 0) null else value * idx })

//结果
[3, 9]
[2, 6]

映射转换时,有两个选择:转换键,使值保持不变,反之亦然。 要将指定转换应用于键,请使用 mapKeys();反过来,mapValues() 转换值。 这两个函数都使用将映射条目作为参数的转换,因此可以操作其键与值。

1
2
3
4
5
6
7
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
println(numbersMap.mapKeys { it.key.toUpperCase() })
println(numbersMap.mapValues { it.value + it.key.length })

//结果
{KEY1=1, KEY2=2, KEY3=3, KEY11=11}
{key1=5, key2=6, key3=7, key11=16}

合拢

合拢 转换是根据两个集合中具有相同位置的元素构建配对。 在 Kotlin 标准库中,这是通过 zip() 扩展函数完成的。 在一个集合(或数组)上以另一个集合(或数组)作为参数调用时,zip() 返回 Pair 对象的列表(List)。 接收者集合的元素是这些配对中的第一个元素。 如果集合的大小不同,则 zip() 的结果为较小集合的大小;结果中不包含较大集合的后续元素。 zip() 也可以中缀形式调用 a zip b 。

1
2
3
4
5
6
7
8
9
10
val colors = listOf("red", "brown", "grey")
val animals = listOf("fox", "bear", "wolf")
println(colors zip animals)

val twoAnimals = listOf("fox", "bear")
println(colors.zip(twoAnimals))

//结果
[(red, fox), (brown, bear), (grey, wolf)]
[(red, fox), (brown, bear)]

也可以使用带有两个参数的转换函数来调用 zip():接收者元素和参数元素。 在这种情况下,结果 List 包含在具有相同位置的接收者对和参数元素对上调用的转换函数的返回值。

1
2
3
4
5
6
7
val colors = listOf("red", "brown", "grey")
val animals = listOf("fox", "bear", "wolf")

println(colors.zip(animals) { color, animal -> "The ${animal.capitalize()} is $color"})

//结果
[The Fox is red, The Bear is brown, The Wolf is grey]

当拥有 Pair 的 List 时,可以进行反向转换 unzipping——从这些键值对中构建两个列表:

  • 第一个列表包含原始列表中每个 Pair 的键。
  • 第二个列表包含原始列表中每个 Pair 的值。

要分割键值对列表,请调用 unzip()。

1
2
3
4
5
val numberPairs = listOf("one" to 1, "two" to 2, "three" to 3, "four" to 4)
println(numberPairs.unzip())

//结果
([one, two, three, four], [1, 2, 3, 4])

关联

关联 转换允许从集合元素和与其关联的某些值构建 Map。 在不同的关联类型中,元素可以是关联 Map 中的键或值。

基本的关联函数 associateWith() 创建一个 Map,其中原始集合的元素是键,并通过给定的转换函数从中产生值。 如果两个元素相等,则仅最后一个保留在 Map 中。

1
2
3
4
5
val numbers = listOf("one", "two", "three", "four")
println(numbers.associateWith { it.length })

//结果
{one=3, two=3, three=5, four=4}

为了使用集合元素作为值来构建 Map,有一个函数 associateBy()。 它需要一个函数,该函数根据元素的值返回键。如果两个元素相等,则仅最后一个保留在 Map 中。 还可以使用值转换函数来调用 associateBy()。

1
2
3
4
5
6
7
8
val numbers = listOf("one", "two", "three", "four")

println(numbers.associateBy { it.first().toUpperCase() })
println(numbers.associateBy(keySelector = { it.first().toUpperCase() }, valueTransform = { it.length }))

//结果
{O=one, T=three, F=four}
{O=3, T=5, F=4}

另一种构建 Map 的方法是使用函数 associate(),其中 Map 键和值都是通过集合元素生成的。 它需要一个 lambda 函数,该函数返回 Pair:键和相应 Map 条目的值。

请注意,associate() 会生成临时的 Pair 对象,这可能会影响性能。 因此,当性能不是很关键或比其他选项更可取时,应使用 associate()。

后者的一个示例:从一个元素一起生成键和相应的值。

1
2
3
4
5
val names = listOf("Alice Adams", "Brian Brown", "Clara Campbell")
println(names.associate { name -> parseFullName(name).let { it.lastName to it.firstName } })

//结果
{Adams=Alice, Brown=Brian, Campbell=Clara}

此时,首先在一个元素上调用一个转换函数,然后根据该函数结果的属性建立 Pair。

打平

如需操作嵌套的集合,则可能会发现提供对嵌套集合元素进行打平访问的标准库函数很有用。

第一个函数为 flatten()。可以在一个集合的集合(例如,一个 Set 组成的 List)上调用它。 该函数返回嵌套集合中的所有元素的一个 List。

1
2
3
4
5
val numberSets = listOf(setOf(1, 2, 3), setOf(4, 5, 6), setOf(1, 2))
println(numberSets.flatten())

//结果
[1, 2, 3, 4, 5, 6, 1, 2]

另一个函数——flatMap() 提供了一种灵活的方式来处理嵌套的集合。 它需要一个函数将一个集合元素映射到另一个集合。 因此,flatMap() 返回单个列表其中包含所有元素的值。 所以,flatMap() 表现为 map()(以集合作为映射结果)与 flatten() 的连续调用。

1
2
3
4
5
6
7
8
9
val containers = listOf(
StringContainer(listOf("one", "two", "three")),
StringContainer(listOf("four", "five", "six")),
StringContainer(listOf("seven", "eight"))
)
println(containers.flatMap { it.values })

//结果
[one, two, three, four, five, six, seven, eight]

字符串表示

如果需要以可读格式检索集合内容,请使用将集合转换为字符串的函数:joinToString() 与 joinTo()。

joinToString() 根据提供的参数从集合元素构建单个 String。 joinTo() 执行相同的操作,但将结果附加到给定的 Appendable 对象。

当使用默认参数调用时,函数返回的结果类似于在集合上调用 toString():各元素的字符串表示形式以空格分隔而成的 String。

1
2
3
4
5
6
7
8
9
10
11
12
13
val numbers = listOf("one", "two", "three", "four")

println(numbers)
println(numbers.joinToString())

val listString = StringBuffer("The list of numbers: ")
numbers.joinTo(listString)
println(listString)

//结果
[one, two, three, four]
one, two, three, four
The list of numbers: one, two, three, four

要构建自定义字符串表示形式,可以在函数参数 separator、prefix 与 postfix中指定其参数。 结果字符串将以 prefix 开头,以 postfix 结尾。除最后一个元素外,separator 将位于每个元素之后。

1
2
3
4
5
val numbers = listOf("one", "two", "three", "four")    
println(numbers.joinToString(separator = " | ", prefix = "start: ", postfix = ": end"))

//结果
start: one | two | three | four: end

对于较大的集合,可能需要指定 limit ——将包含在结果中元素的数量。 如果集合大小超出 limit,所有其他元素将被 truncated 参数的单个值替换。

1
2
3
4
5
val numbers = (1..100).toList()
println(numbers.joinToString(limit = 10, truncated = "<...>"))

//结果
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, <...>

最后,要自定义元素本身的表示形式,请提供 transform 函数。

1
2
3
4
5
val numbers = listOf("one", "two", "three", "four")
println(numbers.joinToString { "Element: ${it.toUpperCase()}"})

//结果
Element: ONE, Element: TWO, Element: THREE, Element: FOUR

过滤

过滤是最常用的集合处理任务之一。在Kotlin中,过滤条件由 谓词 定义——接受一个集合元素并且返回布尔值的 lambda 表达式:true 说明给定元素与谓词匹配,false 则表示不匹配。

标准库包含了一组让你能够通过单个调用就可以过滤集合的扩展函数。这些函数不会改变原始集合,因此它们既可用于可变集合也可用于只读集合。为了操作过滤结果,应该在过滤后将其赋值给变量或链接其他函数。

按谓词过滤

基本的过滤函数是 filter()。当使用一个谓词来调用时,filter() 返回与其匹配的集合元素。对于 List 和 Set,过滤结果都是一个 List,对 Map 来说结果还是一个 Map。

1
2
3
4
5
6
7
8
9
10
11
val numbers = listOf("one", "two", "three", "four")  
val longerThan3 = numbers.filter { it.length > 3 }
println(longerThan3)

val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
val filteredMap = numbersMap.filter { (key, value) -> key.endsWith("1") && value > 10}
println(filteredMap)

//结果
[three, four]
{key11=11}

filter() 中的谓词只能检查元素的值。如果想在过滤中使用元素在集合中的位置,应该使用 filterIndexed()。它接受一个带有两个参数的谓词:元素的索引和元素的值。

如果想使用否定条件来过滤集合,请使用 filterNot()。它返回一个让谓词产生 false 的元素列表。

1
2
3
4
5
6
7
8
9
10
11
val numbers = listOf("one", "two", "three", "four")

val filteredIdx = numbers.filterIndexed { index, s -> (index != 0) && (s.length < 5) }
val filteredNot = numbers.filterNot { it.length <= 3 }

println(filteredIdx)
println(filteredNot)

//结果
[two, four]
[three, four]

还有一些函数能够通过过滤给定类型的元素来缩小元素的类型:

  • filterIsInstance() 返回给定类型的集合元素。在一个 List 上被调用时,filterIsInstance() 返回一个 List,从而让你能够在集合元素上调用 T 类型的函数。
1
2
3
4
5
6
7
8
9
10
val numbers = listOf(null, 1, "two", 3.0, "four")
println("All String elements in upper case:")
numbers.filterIsInstance<String>().forEach {
println(it.toUpperCase())
}

//结果
All String elements in upper case:
TWO
FOUR
  • filterNotNull() 返回所有的非空元素。在一个 List<T?> 上被调用时,filterNotNull() 返回一个 List<T: Any>,从而让你能够将所有元素视为非空对象。
1
2
3
4
5
6
7
8
val numbers = listOf(null, "one", "two", null)
numbers.filterNotNull().forEach {
println(it.length) // 对可空的 String 来说长度不可用
}

//结果
3
3

划分

另一个过滤函数 – partition() – 通过一个谓词过滤集合并且将不匹配的元素存放在一个单独的列表中。因此,你得到一个 List 的 Pair 作为返回值:第一个列表包含与谓词匹配的元素并且第二个列表包含原始集合中的所有其他元素。

1
2
3
4
5
6
7
8
9
val numbers = listOf("one", "two", "three", "four")
val (match, rest) = numbers.partition { it.length > 3 }

println(match)
println(rest)

//结果
[three, four]
[one, two]

检验谓词

最后,有些函数只是针对集合元素简单地检测一个谓词:

  • 如果至少有一个元素匹配给定谓词,那么 any() 返回 true。
  • 如果没有元素与给定谓词匹配,那么 none() 返回 true。
  • 如果所有元素都匹配给定谓词,那么 all() 返回 true。注意,在一个空集合上使用任何有效的谓词去调用 all() 都会返回 true 。这种行为在逻辑上被称为 vacuous truth。
1
2
3
4
5
6
7
8
9
10
11
12
13
val numbers = listOf("one", "two", "three", "four")

println(numbers.any { it.endsWith("e") })
println(numbers.none { it.endsWith("a") })
println(numbers.all { it.endsWith("e") })

println(emptyList<Int>().all { it > 5 }) // vacuous truth

//结果
true
true
false
true

any() 和 none() 也可以不带谓词使用:在这种情况下它们只是用来检查集合是否为空。 如果集合中有元素,any() 返回 true,否则返回 false;none() 则相反。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val numbers = listOf("one", "two", "three", "four")
val empty = emptyList<String>()

println(numbers.any())
println(empty.any())

println(numbers.none())
println(empty.none())

//结果
true
false
false
true

加减操作符

在 Kotlin 中,为集合定义了 plus (+) 和 minus (-) 操作符。 它们把一个集合作为第一个操作数;第二个操作数可以是一个元素或者是另一个集合。 返回值是一个新的只读集合:

  • plus 的结果包含原始集合 和 第二个操作数中的元素。
  • minus 的结果包含原始集合中的元素,但第二个操作数中的元素 除外。 如果第二个操作数是一个元素,那么 minus 移除其在原始集合中的 第一次 出现;如果是一个集合,那么移除其元素在原始集合中的 所有 出现。
1
2
3
4
5
6
7
8
9
10
val numbers = listOf("one", "two", "three", "four")

val plusList = numbers + "five"
val minusList = numbers - listOf("three", "four")
println(plusList)
println(minusList)

//结果
[one, two, three, four, five]
[one, two]

有关 map 的 plus 和 minus 操作符的详细信息,请参见 Map 相关操作。 也为集合定义了广义赋值操作符 plusAssign (+=) 和 minusAssign (-=)。 然而,对于只读集合,它们实际上使用 plus 或者 minus 操作符并尝试将结果赋值给同一变量。 因此,它们仅在由 var 声明的只读集合中可用。 对于可变集合,如果它是一个 val,那么它们会修改集合。

分组

Kotlin 标准库提供用于对集合元素进行分组的扩展函数。 基本函数 groupBy() 使用一个 lambda 函数并返回一个 Map。 在此 Map 中,每个键都是 lambda 结果,而对应的值是返回此结果的元素 List。 例如,可以使用此函数将 String 列表按首字母分组。

还可以使用第二个 lambda 参数(值转换函数)调用 groupBy()。 在带有两个 lambda 的 groupBy() 结果 Map 中,由 keySelector 函数生成的键映射到值转换函数的结果,而不是原始元素。

1
2
3
4
5
6
7
8
val numbers = listOf("one", "two", "three", "four", "five")

println(numbers.groupBy { it.first().toUpperCase() })
println(numbers.groupBy(keySelector = { it.first() }, valueTransform = { it.toUpperCase() }))

//结果
{O=[one], T=[two, three], F=[four, five]}
{o=[ONE], t=[TWO, THREE], f=[FOUR, FIVE]}

如果要对元素进行分组,然后一次将操作应用于所有分组,请使用 groupingBy() 函数。 它返回一个 Grouping 类型的实例。 通过 Grouping 实例,可以以一种惰性的方式将操作应用于所有组:这些分组实际上是刚好在执行操作前构建的。

即,Grouping 支持以下操作:

  • eachCount() 计算每个组中的元素。
  • fold() 与 reduce() 对每个组分别执行 fold 与 reduce 操作,作为一个单独的集合并返回结果。
  • aggregate() 随后将给定操作应用于每个组中的所有元素并返回结果。 这是对 Grouping 执行任何操作的通用方法。当折叠或缩小不够时,可使用它来实现自定义操作。
    1
    2
    3
    4
    5
    val numbers = listOf("one", "two", "three", "four", "five", "six")
    println(numbers.groupingBy { it.first() }.eachCount())

    //结果
    {o=1, t=2, f=2, s=1}

取集合的一部分

Kotlin 标准库包含用于取集合的一部分的扩展函数。 这些函数提供了多种方法来选择结果集合的元素:显式列出其位置、指定结果大小等。

Slice

slice() 返回具有给定索引的集合元素列表。 索引既可以是作为区间传入的也可以是作为整数值的集合传入的。

1
2
3
4
5
6
7
8
9
val numbers = listOf("one", "two", "three", "four", "five", "six")    
println(numbers.slice(1..3))
println(numbers.slice(0..4 step 2))
println(numbers.slice(setOf(3, 5, 0)))

//结果
[two, three, four]
[one, three, five]
[four, six, one]

Take 与 drop

要从头开始获取指定数量的元素,请使用 take() 函数。 要从尾开始获取指定数量的元素,请使用 takeLast()。 当调用的数字大于集合的大小时,两个函数都将返回整个集合。

要从头或从尾去除给定数量的元素,请调用 drop() 或 dropLast() 函数。

1
2
3
4
5
6
7
8
9
10
11
val numbers = listOf("one", "two", "three", "four", "five", "six")
println(numbers.take(3))
println(numbers.takeLast(3))
println(numbers.drop(1))
println(numbers.dropLast(5))

//结果
[one, two, three]
[four, five, six]
[two, three, four, five, six]
[one]

还可以使用谓词来定义要获取或去除的元素的数量。 有四个与上述功能相似的函数:

  • takeWhile() 是带有谓词的 take():它将不停获取元素直到排除与谓词匹配的首个元素。如果首个集合元素与谓词匹配,则结果为空。
  • takeLastWhile() 与 takeLast() 类似:它从集合末尾获取与谓词匹配的元素区间。区间的首个元素是与谓词不匹配的最后一个元素右边的元素。如果最后一个集合元素与谓词匹配,则结果为空。
  • dropWhile() 与具有相同谓词的 takeWhile() 相反:它将首个与谓词不匹配的元素返回到末尾。
  • dropLastWhile() 与具有相同谓词的 takeLastWhile() 相反:它返回从开头到最后一个与谓词不匹配的元素。
1
2
3
4
5
6
7
8
9
10
11
val numbers = listOf("one", "two", "three", "four", "five", "six")
println(numbers.takeWhile { !it.startsWith('f') })
println(numbers.takeLastWhile { it != "three" })
println(numbers.dropWhile { it.length == 3 })
println(numbers.dropLastWhile { it.contains('i') })

//结果
[one, two, three]
[four, five, six]
[three, four, five, six]
[one, two, three, four]

Chunked

要将集合分解为给定大小的“块”,请使用 chunked() 函数。 chunked() 采用一个参数(块的大小),并返回一个 List 其中包含给定大小的 List。 第一个块从第一个元素开始并包含 size 元素,第二个块包含下一个 size 元素,依此类推。 最后一个块的大小可能较小。

1
2
3
4
5
val numbers = (0..13).toList()
println(numbers.chunked(3))

//结果
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11], [12, 13]]

还可以立即对返回的块应用转换。 为此,请在调用 chunked() 时将转换作为 lambda 函数提供。 lambda 参数是集合的一块。当通过转换调用 chunked() 时, 这些块是临时的 List,应立即在该 lambda 中使用。

1
2
3
4
5
val numbers = (0..13).toList() 
println(numbers.chunked(3) { it.sum() }) // `it` 为原始集合的一个块

//结果
[3, 12, 21, 30, 25]

Windowed

可以检索给定大小的集合元素中所有可能区间。 获取它们的函数称为 windowed():它返回一个元素区间列表,比如通过给定大小的滑动窗口查看集合,则会看到该区间。 与 chunked() 不同,windowed() 返回从每个集合元素开始的元素区间(窗口)。 所有窗口都作为单个 List 的元素返回。

1
2
3
4
5
val numbers = listOf("one", "two", "three", "four", "five")    
println(numbers.windowed(3))

//结果
[[one, two, three], [two, three, four], [three, four, five]]

windowed() 通过可选参数提供更大的灵活性:

  • step 定义两个相邻窗口的第一个元素之间的距离。默认情况下,该值为 1,因此结果包含从所有元素开始的窗口。如果将 step 增加到 2,将只收到以奇数元素开头的窗口:第一个、第三个等。
  • partialWindows 包含从集合末尾的元素开始的较小的窗口。例如,如果请求三个元素的窗口,就不能为最后两个元素构建它们。在本例中,启用 partialWindows 将包括两个大小为2与1的列表。

最后,可以立即对返回的区间应用转换。 为此,在调用 windowed() 时将转换作为 lambda 函数提供。

1
2
3
4
5
6
7
val numbers = (1..10).toList()
println(numbers.windowed(3, step = 2, partialWindows = true))
println(numbers.windowed(3) { it.sum() })

//结果
[[1, 2, 3], [3, 4, 5], [5, 6, 7], [7, 8, 9], [9, 10]]
[6, 9, 12, 15, 18, 21, 24, 27]

要构建两个元素的窗口,有一个单独的函数——zipWithNext()。 它创建接收器集合的相邻元素对。 请注意,zipWithNext() 不会将集合分成几对;它为 每个 元素创建除最后一个元素外的对,因此它在 [1, 2, 3, 4] 上的结果为 [[1, 2], [2, 3], [3, 4]],而不是 [[1, 2],[3, 4]]。 zipWithNext() 也可以通过转换函数来调用;它应该以接收者集合的两个元素作为参数。

1
2
3
4
5
6
7
val numbers = listOf("one", "two", "three", "four", "five")    
println(numbers.zipWithNext())
println(numbers.zipWithNext() { s1, s2 -> s1.length > s2.length})

//结果
[(one, two), (two, three), (three, four), (four, five)]
[false, false, true, false]

取单个元素

Kotlin 集合提供了一套从集合中检索单个元素的函数。 此页面描述的函数适用于 list 和 set。

正如 list 的定义所言,list 是有序集合。 因此,list 中的每个元素都有其位置可供你引用。 除了此页面上描述的函数外,list 还提供了更广泛的一套方法去按索引检索和搜索元素。 有关更多详细信息,请参见 List 相关操作。

反过来,从定义来看,set 并不是有序集合。 但是,Kotlin 中的 Set 按某些顺序存储元素。 这些可以是插入顺序(在 LinkedHashSet 中)、自然排序顺序(在 SortedSet 中)或者其他顺序。 一组元素的顺序也可以是未知的。 在这种情况下,元素仍会以某种顺序排序,因此,依赖元素位置的函数仍会返回其结果。 但是,除非调用者知道所使用的 Set 的具体实现,否则这些结果对于调用者是不可预测的。

按位置取

为了检索特定位置的元素,有一个函数 elementAt()。 用一个整数作为参数来调用它,你会得到给定位置的集合元素。 第一个元素的位置是 0,最后一个元素的位置是 (size - 1)。

elementAt() 对于不提供索引访问或非静态已知提供索引访问的集合很有用。 在使用 List 的情况下,使用索引访问操作符 (get() 或 [])更为习惯。

1
2
3
4
5
6
7
8
9
val numbers = linkedSetOf("one", "two", "three", "four", "five")
println(numbers.elementAt(3))

val numbersSortedSet = sortedSetOf("one", "two", "three", "four")
println(numbersSortedSet.elementAt(0)) // 元素以升序存储

//结果
four
four

还有一些有用的别名来检索集合的第一个和最后一个元素:first() 和 last()。

1
2
3
4
5
6
7
val numbers = listOf("one", "two", "three", "four", "five")
println(numbers.first())
println(numbers.last())

//结果
one
five

为了避免在检索位置不存在的元素时出现异常,请使用 elementAt() 的安全变体:

  • 当指定位置超出集合范围时,elementAtOrNull() 返回 null。
  • elementAtOrElse() 还接受一个 lambda 表达式,该表达式能将一个 Int 参数映射为一个集合元素类型的实例。 当使用一个越界位置来调用时,elementAtOrElse() 返回对给定值调用该 lambda 表达式的结果。
1
2
3
4
5
6
7
val numbers = listOf("one", "two", "three", "four", "five")
println(numbers.elementAtOrNull(5))
println(numbers.elementAtOrElse(5) { index -> "The value for index $index is undefined"})

//结果
null
The value for index 5 is undefined

按条件取

函数 first() 和 last() 还可以让你在集合中搜索与给定谓词匹配的元素。 当你使用测试集合元素的谓词调用 first() 时,你会得到对其调用谓词产生 true 的第一个元素。 反过来,带有一个谓词的 last() 返回与其匹配的最后一个元素。

1
2
3
4
5
6
7
val numbers = listOf("one", "two", "three", "four", "five", "six")
println(numbers.first { it.length > 3 })
println(numbers.last { it.startsWith("f") })

//结果
three
five

如果没有元素与谓词匹配,两个函数都会抛异常。 为了避免它们,请改用 firstOrNull() 和 lastOrNull():如果找不到匹配的元素,它们将返回 null。

1
2
3
4
5
val numbers = listOf("one", "two", "three", "four", "five", "six")
println(numbers.firstOrNull { it.length > 6 })

//结果
null

或者,如果别名更适合你的情况,那么可以使用别名:

  • 使用 find() 代替 firstOrNull()
  • 使用 findLast() 代替 lastOrNull()
1
2
3
4
5
6
7
val numbers = listOf(1, 2, 3, 4)
println(numbers.find { it % 2 == 0 })
println(numbers.findLast { it % 2 == 0 })

//结果
2
4

随机取元素

如果需要检索集合的一个随机元素,那么请调用 random() 函数。 你可以不带参数或者使用一个 Random 对象作为随机源来调用它。

1
2
3
4
5
val numbers = listOf(1, 2, 3, 4)
println(numbers.random())

//结果
2

检测存在与否

如需检查集合中某个元素的存在,可以使用 contains() 函数。 如果存在一个集合元素等于(equals())函数参数,那么它返回 true。 你可以使用 in 关键字以操作符的形式调用 contains()。

如需一次检查多个实例的存在,可以使用这些实例的集合作为参数调用 containsAll()。

1
2
3
4
5
6
7
8
9
10
11
12
val numbers = listOf("one", "two", "three", "four", "five", "six")
println(numbers.contains("four"))
println("zero" in numbers)

println(numbers.containsAll(listOf("four", "two")))
println(numbers.containsAll(listOf("one", "zero")))

//结果
true
false
true
false

此外,你可以通过调用 isEmpty() 和 isNotEmpty() 来检查集合中是否包含任何元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
val numbers = listOf("one", "two", "three", "four", "five", "six")
println(numbers.isEmpty())
println(numbers.isNotEmpty())

val empty = emptyList<String>()
println(empty.isEmpty())
println(empty.isNotEmpty())

//结果
false
true
true
false

排序

元素的顺序是某些集合类型的一个重要方面。 例如,如果拥有相同元素的两个列表的元素顺序不同,那么这两个列表也不相等。

在 Kotlin 中,可以通过多种方式定义对象的顺序。

首先,有 自然 顺序。它是为 Comparable 接口的继承者定义的。 当没有指定其他顺序时,使用自然顺序为它们排序。

大多数内置类型是可比较的:

  • 数值类型使用传统的数值顺序:1 大于 0; -3.4f 大于 -5f,以此类推。
  • Char 和 String 使用字典顺序: b 大于 a; world 大于 hello。

如需为用户定义的类型定义一个自然顺序,可以让这个类型继承 Comparable。 这需要实现 compareTo() 函数。 compareTo() 必须将另一个具有相同类型的对象作为参数并返回一个整数值来显示哪个对象更大:

  • 正值表明接收者对象更大。
  • 负值表明它小于参数。
  • 0 说明对象相等。

下面是一个类,可用于排序由主版本号和次版本号两部分组成的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Version(val major: Int, val minor: Int): Comparable<Version> {
override fun compareTo(other: Version): Int {
if (this.major != other.major) {
return this.major - other.major
} else if (this.minor != other.minor) {
return this.minor - other.minor
} else return 0
}
}

fun main() {
println(Version(1, 2) > Version(1, 3))
println(Version(2, 0) > Version(1, 5))
}

//结果
false
true

自定义 顺序让你可以按自己喜欢的方式对任何类型的实例进行排序。 特别是,你可以为不可比较类型定义顺序,或者为可比较类型定义非自然顺序。 如需为类型定义自定义顺序,可以为其创建一个 Comparator。 Comparator 包含 compare() 函数:它接受一个类的两个实例并返回它们之间比较的整数结果。 如上所述,对结果的解释与 compareTo() 的结果相同。

1
2
3
4
5
val lengthComparator = Comparator { str1: String, str2: String -> str1.length - str2.length }
println(listOf("aaa", "bb", "c").sortedWith(lengthComparator))

//结果
[c, bb, aaa]

有了 lengthComparator,你可以按照字符串的长度而不是默认的字典顺序来排列字符串。

定义一个 Comparator 的一种比较简短的方式是标准库中的 compareBy() 函数。 compareBy() 接受一个 lambda 表达式,该表达式从一个实例产生一个 Comparable 值,并将自定义顺序定义为生成值的自然顺序。 使用 compareBy(),上面示例中的长度比较器如下所示:

1
2
3
4
println(listOf("aaa", "bb", "c").sortedWith(compareBy { it.length }))

//结果
[c, bb, aaa]

Kotlin 集合包提供了用于按照自然顺序、自定义顺序甚至随机顺序对集合排序的函数。 在此页面上,我们将介绍适用于只读集合的排序函数。 这些函数将它们的结果作为一个新集合返回,集合里包含了按照请求顺序排序的来自原始集合的元素。

自然顺序

基本的函数 sorted() 和 sortedDescending() 返回集合的元素,这些元素按照其自然顺序升序和降序排序。 这些函数适用于 Comparable 元素的集合。

1
2
3
4
5
6
7
8
val numbers = listOf("one", "two", "three", "four")

println("Sorted ascending: ${numbers.sorted()}")
println("Sorted descending: ${numbers.sortedDescending()}")

//结果
Sorted ascending: [four, one, three, two]
Sorted descending: [two, three, one, four]

自定义顺序

为了按照自定义顺序排序或者对不可比较对象排序,可以使用函数 sortedBy() 和 sortedByDescending()。 它们接受一个将集合元素映射为 Comparable 值的选择器函数,并以该值的自然顺序对集合排序。

1
2
3
4
5
6
7
8
9
10
val numbers = listOf("one", "two", "three", "four")

val sortedNumbers = numbers.sortedBy { it.length }
println("Sorted by length ascending: $sortedNumbers")
val sortedByLast = numbers.sortedByDescending { it.last() }
println("Sorted by the last letter descending: $sortedByLast")

//结果
Sorted by length ascending: [one, two, four, three]
Sorted by the last letter descending: [four, two, one, three]

如需为集合排序定义自定义顺序,可以提供自己的 Comparator。 为此,调用传入 Comparator 的 sortedWith() 函数。 使用此函数,按照字符串长度排序如下所示:

1
2
3
4
5
val numbers = listOf("one", "two", "three", "four")
println("Sorted by length ascending: ${numbers.sortedWith(compareBy { it.length })}")

//结果
Sorted by length ascending: [one, two, four, three]

倒序

你可以使用 reversed() 函数以相反的顺序检索集合。

1
2
3
4
5
val numbers = listOf("one", "two", "three", "four")
println(numbers.reversed())

//结果
[four, three, two, one]

reversed() 返回带有元素副本的新集合。 因此,如果你之后改变了原始集合,这并不会影响先前获得的 reversed() 的结果。

另一个反向函数——asReversed()——返回相同集合实例的一个反向视图,因此,如果原始列表不会发生变化,那么它会比 reversed() 更轻量,更合适。

1
2
3
4
5
6
val numbers = listOf("one", "two", "three", "four")
val reversedNumbers = numbers.asReversed()
println(reversedNumbers)

//结果
[four, three, two, one]

如果原始列表是可变的,那么其所有更改都会反映在其反向视图中,反之亦然。

1
2
3
4
5
6
7
8
9
val numbers = mutableListOf("one", "two", "three", "four")
val reversedNumbers = numbers.asReversed()
println(reversedNumbers)
numbers.add("five")
println(reversedNumbers)

//结果
[four, three, two, one]
[five, four, three, two, one]

但是,如果列表的可变性未知或者源根本不是一个列表,那么 reversed() 更合适,因为其结果是一个未来不会更改的副本。

随机顺序

最后,shuffled() 函数返回一个包含了以随机顺序排序的集合元素的新的 List。 你可以不带参数或者使用 Random 对象来调用它。

1
2
3
4
5
val numbers = listOf("one", "two", "three", "four")
println(numbers.shuffled())

//结果
[four, three, one, two]

聚合操作

Kotlin 集合包含用于常用的 聚合操作 (基于集合内容返回单个值的操作)的函数 。 其中大多数是众所周知的,并且其工作方式与在其他语言中相同。

  • maxOrNull() 与 maxOrNull() 分别返回最小和最大的元素;
  • average() 返回数字集合中元素的平均值;
  • sum() 返回数字集合中元素的总和;
  • count() 返回集合中元素的数量;
1
2
3
4
5
6
7
val numbers = listOf(6, 42, 10, 4)

println("Count: ${numbers.count()}")
println("Max: ${numbers.maxOrNull()}")
println("Min: ${numbers.minOrNull()}")
println("Average: ${numbers.average()}")
println("Sum: ${numbers.sum()}")

还有一些通过某些选择器函数或自定义 Comparator 来检索最小和最大元素的函数。

  • maxOrNullBy()/minOrNullBy() 接受一个选择器函数并返回使选择器返回最大或最小值的元素。
  • maxOrNullWith()/minOrNullWith() 接受一个 Comparator 对象并且根据此 Comparator 对象返回最大或最小元素。
1
2
3
4
5
6
7
val numbers = listOf(5, 42, 10, 4)
val min3Remainder = numbers.minOrNullBy { it % 3 }
println(min3Remainder)

val strings = listOf("one", "two", "three", "four")
val longestString = strings.maxOrNullWith(compareBy { it.length })
println(longestString)

此外,有一些高级的求和函数,它们接受一个函数并返回对所有元素调用此函数的返回值的总和:

  • sumBy() 使用对集合元素调用返回 Int 值的函数。
  • sumByDouble() 与返回 Double 的函数一起使用。
1
2
3
val numbers = listOf(5, 42, 10, 4)
println(numbers.sumBy { it * 2 })
println(numbers.sumByDouble { it.toDouble() / 2 })

Fold 与 reduce

对于更特定的情况,有函数 reduce() 和 fold(),它们依次将所提供的操作应用于集合元素并返回累积的结果。 操作有两个参数:先前的累积值和集合元素。

这两个函数的区别在于:fold() 接受一个初始值并将其用作第一步的累积值,而 reduce() 的第一步则将第一个和第二个元素作为第一步的操作参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
val numbers = listOf(5, 2, 10, 4)

val sum = numbers.reduce { sum, element -> sum + element }
println(sum)
val sumDoubled = numbers.fold(0) { sum, element -> sum + element * 2 }
println(sumDoubled)

//val sumDoubledReduce = numbers.reduce { sum, element -> sum + element * 2 } //错误:第一个元素在结果中没有加倍
//println(sumDoubledReduce)

//结果
21
42

上面的实例展示了区别:fold() 用于计算加倍的元素之和。 如果将相同的函数传给 reduce(),那么它会返回另一个结果,因为在第一步中它将列表的第一个和第二个元素作为参数,所以第一个元素不会被加倍。

如需将函数以相反的顺序应用于元素,可以使用函数 reduceRight() 和 foldRight() 它们的工作方式类似于 fold() 和 reduce(),但从最后一个元素开始,然后再继续到前一个元素。 记住,在使用 foldRight 或 reduceRight 时,操作参数会更改其顺序:第一个参数变为元素,然后第二个参数变为累积值。

1
2
3
4
5
6
val numbers = listOf(5, 2, 10, 4)
val sumDoubledRight = numbers.foldRight(0) { element, sum -> sum + element * 2 }
println(sumDoubledRight)

//结果
42

你还可以使用将元素索引作为参数的操作。 为此,使用函数 reduceIndexed() 和 foldIndexed() 传递元素索引作为操作的第一个参数。

最后,还有将这些操作从右到左应用于集合元素的函数——reduceRightIndexed() 与 foldRightIndexed()。

1
2
3
4
5
6
7
8
9
10
11
12
mbers.foldRightIndexed(0) { idx, element, sum -> if (idx % 2 == 0) sum + element else sum }
println(sumEvenRight)
val numbers = listOf(5, 2, 10, 4)
val sumEven = numbers.foldIndexed(0) { idx, sum, element -> if (idx % 2 == 0) sum + element else sum }
println(sumEven)

val sumEvenRight = numbers.foldRightIndexed(0) { idx, element, sum -> if (idx % 2 == 0) sum + element else sum }
println(sumEvenRight)

//结果
15
15

集合写操作

可变集合支持更改集合内容的操作,例如添加或删除元素。 在此页面上,我们将描述实现 MutableCollection 的所有写操作。 有关 List 与 Map 可用的更多特定操作,请分别参见 List 相关操作与 Map 相关操作。

添加元素

要将单个元素添加到列表或集合,请使用 add() 函数。指定的对象将添加到集合的末尾。

1
2
3
4
5
6
val numbers = mutableListOf(1, 2, 3, 4)
numbers.add(5)
println(numbers)

//结果
[1, 2, 3, 4, 5]

addAll() 将参数对象的每个元素添加到列表或集合中。参数可以是 Iterable、Sequence 或 Array。 接收者的类型和参数可能不同,例如,你可以将所有内容从 Set 添加到 List。

当在列表上调用时,addAll() 会按照在参数中出现的顺序添加各个新元素。 你也可以调用 addAll() 时指定一个元素位置作为第一参数。 参数集合的第一个元素会被插入到这个位置。 其他元素将跟随在它后面,将接收者元素移到末尾。

1
2
3
4
5
6
7
8
9
val numbers = mutableListOf(1, 2, 5, 6)
numbers.addAll(arrayOf(7, 8))
println(numbers)
numbers.addAll(2, setOf(3, 4))
println(numbers)

//结果
[1, 2, 5, 6, 7, 8]
[1, 2, 3, 4, 5, 6, 7, 8]

你还可以使用 plus 运算符 - plusAssign (+=) 添加元素。 当应用于可变集合时,+= 将第二个操作数(一个元素或另一个集合)追加到集合的末尾。

1
2
3
4
5
6
7
8
9
val numbers = mutableListOf("one", "two")
numbers += "three"
println(numbers)
numbers += listOf("four", "five")
println(numbers)

//结果
[one, two, three]
[one, two, three, four, five]

删除元素

若要从可变集合中移除元素,请使用 remove() 函数。 remove() 接受元素值,并删除该值的一个匹配项。

1
2
3
4
5
6
7
8
9
val numbers = mutableListOf(1, 2, 3, 4, 3)
numbers.remove(3) // 删除了第一个 `3`
println(numbers)
numbers.remove(5) // 什么都没删除
println(numbers)

//结果
[1, 2, 4, 3]
[1, 2, 4, 3]

要一次删除多个元素,有以下函数:

  • removeAll() 移除参数集合中存在的所有元素。 或者,你可以用谓词作为参数来调用它;在这种情况下,函数移除谓词产生 true 的所有元素。
  • retainAll() 与 removeAll() 相反:它移除除参数集合中的元素之外的所有元素。 当与谓词一起使用时,它只留下与之匹配的元素。
  • clear() 从列表中移除所有元素并将其置空。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
val numbers = mutableListOf(1, 2, 3, 4)
println(numbers)
numbers.retainAll { it >= 3 }
println(numbers)
numbers.clear()
println(numbers)

val numbersSet = mutableSetOf("one", "two", "three", "four")
numbersSet.removeAll(setOf("one", "two"))
println(numbersSet)

//结果
[1, 2, 3, 4]
[3, 4]
[]
[three, four]

从集合中移除元素的另一种方法是使用 minusAssign (-=) ——原地修改版的 minus 操作符。 minus 操作符。 第二个参数可以是元素类型的单个实例或另一个集合。 右边是单个元素时,-= 会移除它的第一个匹配项。 反过来,如果它是一个集合,那么它的所有元素的每次出现都会删除。 例如,如果列表包含重复的元素,它们将被同时删除。 第二个操作数可以包含集合中不存在的元素。这些元素不会影响操作的执行。

1
2
3
4
5
6
7
8
9
10
val numbers = mutableListOf("one", "two", "three", "three", "four")
numbers -= "three"
println(numbers)
numbers -= listOf("four", "five")
//numbers -= listOf("four") // 与上述相同
println(numbers)

//结果
[one, two, three, four]
[one, two, three]

更新元素

list 和 map 还提供更新元素的操作。 它们在 List 相关操作与 Map 相关操作中有所描述。 对于 set 来说,更新没有意义,因为它实际上是移除一个元素并添加另一个元素。

List 相关操作

List 是 Kotlin 标准库中最受欢迎的集合类型。对列表元素的索引访问为 List 提供了一组强大的操作。

按索引取元素
List 支持按索引取元素的所有常用操作: elementAt() 、 first() 、 last() 与取单个元素中列出的其他操作。 List 的特点是能通过索引访问特定元素,因此读取元素的最简单方法是按索引检索它。 这是通过 get() 函数或简写语法 [index] 来传递索引参数完成的。

如果 List 长度小于指定的索引,则抛出异常。 另外,还有两个函数能避免此类异常:

  • getOrElse() 提供用于计算默认值的函数,如果集合中不存在索引,则返回默认值。
  • getOrNull() 返回 null 作为默认值。
1
2
3
4
5
6
7
8
9
10
11
12
val numbers = listOf(1, 2, 3, 4)
println(numbers.get(0))
println(numbers[0])
//numbers.get(5) // exception!
println(numbers.getOrNull(5)) // null
println(numbers.getOrElse(5, {it})) // 5

//结果
1
1
null
5

取列表的一部分

除了取集合的一部分中常用的操作, List 还提供 subList() 该函数将指定元素范围的视图作为列表返回。 因此,如果原始集合的元素发生变化,则它在先前创建的子列表中也会发生变化,反之亦然。

1
2
3
4
5
val numbers = (0..13).toList()
println(numbers.subList(3, 6))

//结果
[3, 4, 5]

查找元素位置

线性查找

在任何列表中,都可以使用 indexOf() 或 lastIndexOf() 函数找到元素的位置。 它们返回与列表中给定参数相等的元素的第一个或最后一个位置。 如果没有这样的元素,则两个函数均返回 -1。

1
2
3
4
5
6
7
val numbers = listOf(1, 2, 3, 4, 2, 5)
println(numbers.indexOf(2))
println(numbers.lastIndexOf(2))

//结果
1
4

还有一对函数接受谓词并搜索与之匹配的元素:

  • indexOfFirst() 返回与谓词匹配的第一个元素的索引,如果没有此类元素,则返回 -1。
  • indexOfLast() 返回与谓词匹配的最后一个元素的索引,如果没有此类元素,则返回 -1。
1
2
3
4
5
6
7
val numbers = mutableListOf(1, 2, 3, 4)
println(numbers.indexOfFirst { it > 2})
println(numbers.indexOfLast { it % 2 == 1})

//结果
2
2

在有序列表中二分查找

还有另一种搜索列表中元素的方法——二分查找算法。 它的工作速度明显快于其他内置搜索功能,但要求该列表按照一定的顺序(自然排序或函数参数中提供的另一种排序)按升序排序过。 否则,结果是不确定的。

要搜索已排序列表中的元素,请调用 binarySearch() 函数,并将该值作为参数传递。 如果存在这样的元素,则函数返回其索引;否则,将返回 (-insertionPoint - 1),其中 insertionPoint 为应插入此元素的索引,以便列表保持排序。 如果有多个具有给定值的元素,搜索则可以返回其任何索引。

还可以指定要搜索的索引区间:在这种情况下,该函数仅在两个提供的索引之间搜索。

1
2
3
4
5
6
7
8
9
10
11
12
val numbers = mutableListOf("one", "two", "three", "four")
numbers.sort()
println(numbers)
println(numbers.binarySearch("two")) // 3
println(numbers.binarySearch("z")) // -5
println(numbers.binarySearch("two", 0, 2)) // -3

//结果
[four, one, three, two]
3
-5
-3

Comparator 二分搜索

如果列表元素不是 Comparable,则应提供一个用于二分搜索的 Comparator。 该列表必须根据此 Comparator 以升序排序。来看一个例子:

1
2
3
4
5
6
7
8
9
10
val productList = listOf(
Product("WebStorm", 49.0),
Product("AppCode", 99.0),
Product("DotTrace", 129.0),
Product("ReSharper", 149.0))

println(productList.binarySearch(Product("AppCode", 99.0), compareBy<Product> { it.price }.thenBy { it.name }))

//结果
1

这是一个不可排序的 Product 实例列表,以及一个定义排序的 Comparator:如果 p1 的价格小于 p2 的价格,则产品 p1 在产品 p2 之前。 因此,按照此顺序对列表进行升序排序后,使用 binarySearch() 查找指定的 Product的索引。

当列表使用与自然排序不同的顺序时(例如,对 String 元素不区分大小写的顺序),自定义 Comparator 也很方便。

1
2
3
4
5
val colors = listOf("Blue", "green", "ORANGE", "Red", "yellow")
println(colors.binarySearch("RED", String.CASE_INSENSITIVE_ORDER)) // 3

//结果
3

比较函数二分搜索

使用 比较 函数的二分搜索无需提供明确的搜索值即可查找元素。 取而代之的是,它使用一个比较函数将元素映射到 Int 值,并搜索函数返回 0 的元素。 该列表必须根据提供的函数以升序排序;换句话说,比较的返回值必须从一个列表元素增长到下一个列表元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data class Product(val name: String, val price: Double)

fun priceComparison(product: Product, price: Double) = sign(product.price - price).toInt()

fun main() {
val productList = listOf(
Product("WebStorm", 49.0),
Product("AppCode", 99.0),
Product("DotTrace", 129.0),
Product("ReSharper", 149.0))

println(productList.binarySearch { priceComparison(it, 99.0) })
}

//结果
1

Comparator 与比较函数二分搜索都可以针对列表区间执行。

List 写操作

除了集合写操作中描述的集合修改操作之外,可变列表还支持特定的写操作。 这些操作使用索引来访问元素以扩展列表修改功能。

添加

要将元素添加到列表中的特定位置,请使用 add() 或 addAll() 并提供元素插入的位置作为附加参数。 位置之后的所有元素都将向右移动。

1
2
3
4
5
6
7
val numbers = mutableListOf("one", "five", "six")
numbers.add(1, "two")
numbers.addAll(2, listOf("three", "four"))
println(numbers)

//结果
[one, two, three, four, five, six]

更新

列表还提供了在指定位置替换元素的函数——set() 及其操作符形式 []。set() 不会更改其他元素的索引。

1
2
3
4
5
6
val numbers = mutableListOf("one", "five", "three")
numbers[1] = "two"
println(numbers)

//结果
[one, two, three]

fill() 简单地将所有集合元素的值替换为指定值。

1
2
3
4
5
6
val numbers = mutableListOf(1, 2, 3, 4)
numbers.fill(3)
println(numbers)

//结果
[3, 3, 3, 3]

删除

要从列表中删除指定位置的元素,请使用 removeAt() 函数,并将位置作为参数。 在元素被删除之后出现的所有元素索引将减 1。

1
2
3
4
5
6
val numbers = mutableListOf(1, 2, 3, 4, 3)    
numbers.removeAt(1)
println(numbers)

//结果
[1, 3, 4, 3]

For removing the first and the last element, there are handy shortcuts removeFirst() and removeLast(). Note that on empty lists, they throw an exception. To receive null instead, use removeFirstOrNull() and removeLastOrNull()

1
2
3
4
5
6
7
8
9
10
11
val numbers = mutableListOf(1, 2, 3, 4, 3)    
numbers.removeFirst()
numbers.removeLast()
println(numbers)

val empty = mutableListOf<Int>()
// empty.removeFirst() // NoSuchElementException: List is empty.
empty.removeFirstOrNull() //null

//结果
[2, 3, 4]

排序

在集合排序中,描述了按特定顺序检索集合元素的操作。 对于可变列表,标准库中提供了类似的扩展函数,这些扩展函数可以执行相同的排序操作。 将此类操作应用于列表实例时,它将更改指定实例中元素的顺序。

就地排序函数的名称与应用于只读列表的函数的名称相似,但没有 ed/d 后缀:

  • sort* 在所有排序函数的名称中代替 sorted*:sort()、sortDescending()、sortBy() 等等。
  • shuffle() 代替 shuffled()。
  • reverse() 代替 reversed()。
  • asReversed() 在可变列表上调用会返回另一个可变列表,该列表是原始列表的反向视图。在该视图中的更改将反映在原始列表中。 以下示例展示了可变列表的排序函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
val numbers = mutableListOf("one", "two", "three", "four")

numbers.sort()
println("Sort into ascending: $numbers")
numbers.sortDescending()
println("Sort into descending: $numbers")

numbers.sortBy { it.length }
println("Sort into ascending by length: $numbers")
numbers.sortByDescending { it.last() }
println("Sort into descending by the last letter: $numbers")
s
numbers.sortWith(compareBy<String> { it.length }.thenBy { it })
println("Sort by Comparator: $numbers")

numbers.shuffle()
println("Shuffle: $numbers")

numbers.reverse()
println("Reverse: $numbers")

//结果
Sort into ascending: [four, one, three, two]
Sort into descending: [two, three, one, four]
Sort into ascending by length: [two, one, four, three]
Sort into descending by the last letter: [four, two, one, three]
Sort by Comparator: [one, two, four, three]
Shuffle: [two, three, one, four]
Reverse: [four, one, three, two]

Set 相关操作

Kotlin 集合包中包含 set 常用操作的扩展函数:查找交集、并集或差集。

要将两个集合合并为一个(并集),可使用 union() 函数。也能以中缀形式使用 a union b。 注意,对于有序集合,操作数的顺序很重要:在结果集合中,左侧操作数在前。

要查找两个集合中都存在的元素(交集),请使用 intersect() 。 要查找另一个集合中不存在的集合元素(差集),请使用 subtract() 。 这两个函数也能以中缀形式调用,例如, a intersect b 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val numbers = setOf("one", "two", "three")

println(numbers union setOf("four", "five"))
println(setOf("four", "five") union numbers)

println(numbers intersect setOf("two", "one"))
println(numbers subtract setOf("three", "four"))
println(numbers subtract setOf("four", "three")) // 相同的输出

//结果
[one, two, three, four, five]
[four, five, one, two, three]
[one, two]
[one, two]
[one, two]

注意, List 也支持 Set 操作。 但是,对 List 进行 Set 操作的结果仍然是 Set ,因此将删除所有重复的元素。

Map 相关操作

在 map 中,键和值的类型都是用户定义的。 对基于键的访问启用了各种特定于 map 的处理函数,从键获取值到对键和值进行单独过滤。 在此页面上,我们提供了来自标准库的 map 处理功能的描述。

取键与值

要从 Map 中检索值,必须提供其键作为 get() 函数的参数。 还支持简写 [key] 语法。 如果找不到给定的键,则返回 null 。 还有一个函数 getValue() ,它的行为略有不同:如果在 Map 中找不到键,则抛出异常。 此外,还有两个选项可以解决键缺失的问题:

  • getOrElse() 与 list 的工作方式相同:对于不存在的键,其值由给定的 lambda 表达式返回。
  • getOrDefault() 如果找不到键,则返回指定的默认值。
1
2
3
4
5
6
7
8
9
10
11
12
val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3)
println(numbersMap.get("one"))
println(numbersMap["one"])
println(numbersMap.getOrDefault("four", 10))
println(numbersMap["five"]) // null
//numbersMap.getValue("six") // exception!

//结果
1
1
10
null

要对 map 的所有键或所有值执行操作,可以从属性 keys 和 values 中相应地检索它们。 keys 是 Map 中所有键的集合, values 是 Map 中所有值的集合。

1
2
3
4
5
6
7
val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3)
println(numbersMap.keys)
println(numbersMap.values)

//结果
[one, two, three]
[1, 2, 3]

过滤

可以使用 filter() 函数来过滤 map 或其他集合。 对 map 使用 filter() 函数时, Pair 将作为参数的谓词传递给它。 它将使用谓词同时过滤其中的键和值。

1
2
3
4
5
6
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
val filteredMap = numbersMap.filter { (key, value) -> key.endsWith("1") && value > 10}
println(filteredMap)

//结果
{key11=11}

还有两种用于过滤 map 的特定函数:按键或按值。 这两种方式,都有对应的函数: filterKeys() 和 filterValues() 。 两者都将返回一个新 Map ,其中包含与给定谓词相匹配的条目。 filterKeys() 的谓词仅检查元素键, filterValues() 的谓词仅检查值。

1
2
3
4
5
6
7
8
9
10
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
val filteredKeysMap = numbersMap.filterKeys { it.endsWith("1") }
val filteredValuesMap = numbersMap.filterValues { it < 10 }

println(filteredKeysMap)
println(filteredValuesMap)

//结果
{key1=1, key11=11}
{key1=1, key2=2, key3=3}

plus 与 minus 操作

由于需要访问元素的键,plus(+)与 minus(-)运算符对 map 的作用与其他集合不同。 plus 返回包含两个操作数元素的 Map :左侧的 Map 与右侧的 Pair 或另一个 Map 。 当右侧操作数中有左侧 Map 中已存在的键时,该条目将使用右侧的值。

1
2
3
4
5
6
7
8
9
val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3)
println(numbersMap + Pair("four", 4))
println(numbersMap + Pair("one", 10))
println(numbersMap + mapOf("five" to 5, "one" to 11))

//结果
{one=1, two=2, three=3, four=4}
{one=10, two=2, three=3}
{one=11, two=2, three=3, five=5}

minus 将根据左侧 Map 条目创建一个新 Map ,右侧操作数带有键的条目将被剔除。 因此,右侧操作数可以是单个键或键的集合: list 、 set 等。

1
2
3
4
5
6
7
val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3)
println(numbersMap - "one")
println(numbersMap - listOf("two", "four"))

//结果
{two=2, three=3}
{one=1, three=3}

关于在可变 Map 中使用 plusAssign(+=)与 minusAssign(-=)运算符的详细信息,请参见 Map 写操作 。

Map 写操作

Mutable Map (可变 Map )提供特定的 Map 写操作。 这些操作使你可以使用键来访问或更改 Map 值。

Map 写操作的一些规则:

  • 值可以更新。 反过来,键也永远不会改变:添加条目后,键是不变的。
  • 每个键都有一个与之关联的值。也可以添加和删除整个条目。
    下面是对可变 Map 中可用写操作的标准库函数的描述。

添加与更新条目

要将新的键值对添加到可变 Map ,请使用 put() 。 将新条目放入 LinkedHashMap (Map的默认实现)后,会添加该条目,以便在 Map 迭代时排在最后。 在 Map 类中,新元素的位置由其键顺序定义。

1
2
3
4
5
6
val numbersMap = mutableMapOf("one" to 1, "two" to 2)
numbersMap.put("three", 3)
println(numbersMap)

//结果
{one=1, two=2, three=3}

要一次添加多个条目,请使用 putAll() 。它的参数可以是 Map 或一组 Pair : Iterable 、 Sequence 或 Array 。

1
2
3
4
5
6
val numbersMap = mutableMapOf("one" to 1, "two" to 2, "three" to 3)
numbersMap.putAll(setOf("four" to 4, "five" to 5))
println(numbersMap)

//结果
{one=1, two=2, three=3, four=4, five=5}

如果给定键已存在于 Map 中,则 put() 与 putAll() 都将覆盖值。 因此,可以使用它们来更新 Map 条目的值。

1
2
3
4
5
6
7
8
val numbersMap = mutableMapOf("one" to 1, "two" to 2)
val previousValue = numbersMap.put("one", 11)
println("value associated with 'one', before: $previousValue, after: ${numbersMap["one"]}")
println(numbersMap)

//结果
value associated with 'one', before: 1, after: 11
{one=11, two=2}

还可以使用快速操作符将新条目添加到 Map 。 有两种方式:

  • plusAssign (+=) 操作符。
  • [] 操作符为 set() 的别名。
1
2
3
4
5
6
7
val numbersMap = mutableMapOf("one" to 1, "two" to 2)
numbersMap["three"] = 3 // 调用 numbersMap.set("three", 3)
numbersMap += mapOf("four" to 4, "five" to 5)
println(numbersMap)

//结果
{one=1, two=2, three=3, four=4, five=5}

使用 Map 中存在的键进行操作时,将覆盖相应条目的值。

删除条目

要从可变 Map 中删除条目,请使用 remove() 函数。 调用 remove() 时,可以传递键或整个键值对。 如果同时指定键和值,则仅当键值都匹配时,才会删除此的元素。

1
2
3
4
5
6
7
8
9
val numbersMap = mutableMapOf("one" to 1, "two" to 2, "three" to 3)
numbersMap.remove("one")
println(numbersMap)
numbersMap.remove("three", 4) //不会删除任何条目
println(numbersMap)

//结果
{two=2, three=3}
{two=2, three=3}

还可以通过键或值从可变 Map 中删除条目。 在 Map 的 .keys 或 .values 中调用 remove() 并提供键或值来删除条目。 在 .values 中调用时, remove() 仅删除给定值匹配到的的第一个条目。

1
2
3
4
5
6
7
8
9
val numbersMap = mutableMapOf("one" to 1, "two" to 2, "three" to 3, "threeAgain" to 3)
numbersMap.keys.remove("one")
println(numbersMap)
numbersMap.values.remove(3)
println(numbersMap)

//结果
{two=2, three=3, threeAgain=3}
{two=2, threeAgain=3}

minusAssign (-=) 操作符也可用于可变 Map 。

1
2
3
4
5
6
7
8
9
val numbersMap = mutableMapOf("one" to 1, "two" to 2, "three" to 3)
numbersMap -= "two"
println(numbersMap)
numbersMap -= "five" //不会删除任何条目
println(numbersMap)

//结果
{one=1, three=3}
{one=1, three=3}

协程

暂时不更新

更多语言结构

解构声明

有时把一个对象 解构 成很多变量会很方便,例如:

1
val (name, age) = person

这种语法称为 解构声明 。一个解构声明同时创建多个变量。 我们已经声明了两个新变量: name 和 age,并且可以独立使用它们:

1
2
println(name)
println(age)

一个解构声明会被编译成以下代码:

1
2
val name = person.component1()
val age = person.component2()

其中的 component1() 和 component2() 函数是在 Kotlin 中广泛使用的 约定原则 的另一个例子。 (参见像 + 和 *、for-循环等操作符)。 任何表达式都可以出现在解构声明的右侧,只要可以对它调用所需数量的 component 函数即可。 当然,可以有 component3() 和 component4() 等等。

请注意,componentN() 函数需要用 operator 关键字标记,以允许在解构声明中使用它们。

解构声明也可以用在 for-循环中:当你写:

1
for ((a, b) in collection) { …… }

变量 a 和 b 的值取自对集合中的元素上调用 component1() 和 component2() 的返回值。

例:从函数中返回两个变量
让我们假设我们需要从一个函数返回两个东西。例如,一个结果对象和一个某种状态。 在 Kotlin 中一个简洁的实现方式是声明一个数据类 并返回其实例:

1
2
3
4
5
6
7
8
9
data class Result(val result: Int, val status: Status)
fun function(……): Result {
// 各种计算

return Result(result, status)
}

// 现在,使用该函数:
val (result, status) = function(……)

因为数据类自动声明 componentN() 函数,所以这里可以用解构声明。

注意:我们也可以使用标准类 Pair 并且让 function() 返回 Pair<Int, Status>, 但是让数据合理命名通常更好。

例:解构声明和映射

可能遍历一个映射(map)最好的方式就是这样:

1
2
3
for ((key, value) in map) {
// 使用该 key、value 做些事情
}

为使其能用,我们应该

  • 通过提供一个 iterator() 函数将映射表示为一个值的序列;
  • 通过提供函数 component1() 和 component2() 来将每个元素呈现为一对。

当然事实上,标准库提供了这样的扩展:

1
2
3
operator fun <K, V> Map<K, V>.iterator(): Iterator<Map.Entry<K, V>> = entrySet().iterator()
operator fun <K, V> Map.Entry<K, V>.component1() = getKey()
operator fun <K, V> Map.Entry<K, V>.component2() = getValue()

因此你可以在 for-循环中对映射(以及数据类实例的集合等)自由使用解构声明。

下划线用于未使用的变量

如果在解构声明中你不需要某个变量,那么可以用下划线取代其名称:

1
val (_, status) = getResult()

对于以这种方式跳过的组件,不会调用相应的 componentN() 操作符函数。

在 lambda 表达式中解构

你可以对 lambda 表达式参数使用解构声明语法。 如果 lambda 表达式具有 Pair 类型(或者 Map.Entry 或任何其他具有相应 componentN 函数的类型)的参数,那么可以通过将它们放在括号中来引入多个新参数来取代单个新参数:

1
2
map.mapValues { entry -> "${entry.value}!" }
map.mapValues { (key, value) -> "$value!" }

注意声明两个参数和声明一个解构对来取代单个参数之间的区别:

1
2
3
4
{ a //-> …… } // 一个参数
{ a, b //-> …… } // 两个参数
{ (a, b) //-> …… } // 一个解构对
{ (a, b), c //-> …… } // 一个解构对以及其他参数

如果解构的参数中的一个组件未使用,那么可以将其替换为下划线,以避免编造其名称:

1
map.mapValues { (_, value) -> "$value!" }

你可以指定整个解构的参数的类型或者分别指定特定组件的类型:

1
2
3
map.mapValues { (_, value): Map.Entry<Int, String> -> "$value!" }

map.mapValues { (_, value: String) -> "$value!" }

类型检测与转换

is 与 !is 操作符

我们可以在运行时通过使用 is 操作符或其否定形式 !is 来检测对象是否符合给定类型:

1
2
3
4
5
6
7
8
9
10
if (obj is String) {
print(obj.length)
}

if (obj !is String) { // 与 !(obj is String) 相同
print("Not a String")
}
else {
print(obj.length)
}

智能转换

在许多情况下,不需要在 Kotlin 中使用显式转换操作符,因为编译器跟踪不可变值的 is-检测以及显式转换,并在需要时自动插入(安全的)转换:

1
2
3
4
5
fun demo(x: Any) {
if (x is String) {
print(x.length) // x 自动转换为字符串
}
}

编译器足够聪明,能够知道如果反向检测导致返回那么该转换是安全的:

1
2
3
if (x !is String) return

print(x.length) // x 自动转换为字符串

或者在 && 和 || 的右侧:

1
2
3
4
5
6
7
// `||` 右侧的 x 自动转换为字符串
if (x !is String || x.length == 0) return

// `&&` 右侧的 x 自动转换为字符串
if (x is String && x.length > 0) {
print(x.length) // x 自动转换为字符串
}

这些 智能转换 用于 when-表达式 和 while-循环 也一样:

1
2
3
4
5
when (x) {
is Int -> print(x + 1)
is String -> print(x.length + 1)
is IntArray -> print(x.sum())
}

请注意,当编译器不能保证变量在检测和使用之间不可改变时,智能转换不能用。 更具体地,智能转换能否适用根据以下规则:

  • val 局部变量——总是可以,局部委托属性除外;
  • val 属性——如果属性是 private 或 internal,或者该检测在声明属性的同一模块中执行。智能转换不适用于 open 的属性或者具有自定义 getter 的属性;
  • var 局部变量——如果变量在检测和使用之间没有修改、没有在会修改它的 lambda 中捕获、并且不是局部委托属性;
  • var 属性——决不可能(因为该变量可以随时被其他代码修改)。

“不安全的”转换操作符

通常,如果转换是不可能的,转换操作符会抛出一个异常。因此,我们称之为不安全的。 Kotlin 中的不安全转换由中缀操作符 as(参见operator precedence)完成:

1
val x: String = y as String

请注意,null 不能转换为 String 因该类型不是可空的, 即如果 y 为空,上面的代码会抛出一个异常。 为了让这样的代码用于可空值,请在类型转换的右侧使用可空类型:

1
val x: String? = y as String?

Please note that the “unsafe” cast operator is not equivalent to the unsafeCast() method available in Kotlin/JS. unsafeCast will do no type-checking at all, whereas the cast operator throws a ClassCastException when the cast fails.

“安全的”(可空)转换操作符

为了避免抛出异常,可以使用安全转换操作符 as?,它可以在失败时返回 null:

1
val x: String? = y as? String

请注意,尽管事实上 as? 的右边是一个非空类型的 String,但是其转换的结果是可空的。

类型擦除与泛型检测

Kotlin 在编译时确保涉及泛型操作的类型安全性, 而在运行时,泛型类型的实例并未带有关于它们实际类型参数的信息。例如, List 会被擦除为 List<*>。通常,在运行时无法检测一个实例是否属于带有某个类型参数的泛型类型。

为此,编译器会禁止由于类型擦除而无法执行的 is 检测,例如 ints is List 或者 list is T(类型参数)。当然,你可以对一个实例检测星投影的类型:

1
2
3
if (something is List<*>) {
something.forEach { println(it) } // 这些项的类型都是 `Any?`
}

类似地,当已经让一个实例的类型参数(在编译期)静态检测, 就可以对涉及非泛型部分做 is 检测或者类型转换。请注意, 在这种情况下,会省略尖括号:

1
2
3
4
5
fun handleStrings(list: List<String>) {
if (list is ArrayList) {
// `list` 会智能转换为 `ArrayList<String>`
}
}

省略类型参数的这种语法可用于不考虑类型参数的类型转换:list as ArrayList。

带有具体化的类型参数的内联函数使其类型实参在每个调用处内联,这就能够对类型参数进行 arg is T 检测,但是如果 arg 自身是一个泛型实例,其类型参数还是会被擦除。例如:

1
2
3
4
5
6
7
8
9
10
11
inline fun <reified A, reified B> Pair<*, *>.asPairOf(): Pair<A, B>? {
if (first !is A || second !is B) return null
return first as A to second as B
}

val somePair: Pair<Any?, Any?> = "items" to listOf(1, 2, 3)

val stringToSomething = somePair.asPairOf<String, Any>()
val stringToInt = somePair.asPairOf<String, Int>()
val stringToList = somePair.asPairOf<String, List<*>>()
val stringToStringList = somePair.asPairOf<String, List<String>>() // 破坏类型安全!

非受检类型转换

如上所述,类型擦除使运行时不可能对泛型类型实例的类型实参进行检测,并且代码中的泛型可能相互连接不够紧密,以致于编译器无法确保类型安全。

即便如此,有时候我们有高级的程序逻辑来暗示类型安全。例如:

1
2
3
4
5
6
7
8
9
fun readDictionary(file: File): Map<String, *> = file.inputStream().use { 
TODO("Read a mapping of strings to arbitrary elements.")
}

// 我们已将存有一些 `Int` 的映射保存到该文件
val intsFile = File("ints.dictionary")

// Warning: Unchecked cast: `Map<String, *>` to `Map<String, Int>`
val intsDictionary: Map<String, Int> = readDictionary(intsFile) as Map<String, Int>

编译器会对最后一行的类型转换产生一个警告。该类型转换不能在运行时完全检测,并且不能保证映射中的值是“Int”。

为避免未受检类型转换,可以重新设计程序结构:在上例中,可以使用具有类型安全实现的不同接口 DictionaryReader 与 DictionaryWriter。 可以引入合理的抽象,将未受检的类型转换从调用代码移动到实现细节中。 正确使用泛型型变也有帮助。

对于泛型函数,使用具体化的类型参数可以使诸如 arg as T 这样的类型转换受检,除非 arg 对应类型的自身类型参数已被擦除。

可以通过在产生警告的语句或声明上用注解 @Suppress(“UNCHECKED_CAST”) 标注来禁止未受检类型转换警告:

1
2
3
4
5
inline fun <reified T> List<*>.asListOfType(): List<T>? =
if (all { it is T })
@Suppress("UNCHECKED_CAST")
this as List<T> else
null

IntelliJ IDEA can also automatically generate the @Suppress annotation. Open the intentions menu via the light bulb icon or Alt-Enter, and click the small arrow next to the “Change type arguments” quick-fix. Here, you can select the suppression scope, and your IDE will add the annotation to your file accordingly.

在 JVM 平台中,数组类型(Array)会保留关于其元素被擦除类型的信息,并且类型转换为一个数组类型可以部分受检: 元素类型的可空性与类型实参仍然会被擦除。例如, 如果 foo 是一个保存了任何 List<*>(无论可不可空)的数组的话,类型转换 foo as Array<List?> 都会成功。

This 表达式

为了表示当前的 接收者 我们使用 this 表达式:

  • 在类的成员中,this 指的是该类的当前对象。
  • 在扩展函数或者带有接收者的函数字面值中, this 表示在点左侧传递的 接收者 参数。

如果 this 没有限定符,它指的是最内层的包含它的作用域。要引用其他作用域中的 this,请使用 标签限定符:

限定的 this

要访问来自外部作用域的this(一个类 或者扩展函数, 或者带标签的带有接收者的函数字面值)我们使用this@label,其中 @label 是一个代指 this 来源的标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 的接收者
}

val funLit2 = { s: String ->
// foo() 的接收者,因为它包含的 lambda 表达式
// 没有任何接收者
val d1 = this
}
}
}
}

Implicit this

当对 this 调用成员函数时,可以省略 this. 部分。 但是如果有一个同名的非成员函数时,请谨慎使用,因为在某些情况下会调用同名的非成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
fun printLine() { println("Top-level 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) // Top-level function

操作符重载

Kotlin 允许我们为自己的类型提供预定义的一组操作符的实现。这些操作符具有固定的符号表示 (如 + 或 *)和固定的优先级。为实现这样的操作符,我们为相应的类型(即二元操作符左侧的类型和一元操作符的参数类型)提供了一个固定名字的成员函数或扩展函数。 重载操作符的函数需要用 operator 修饰符标记。

另外,我们描述为不同操作符规范操作符重载的约定。

一元操作

一元前缀操作符

表达式 翻译为
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
这个表是说,当编译器处理例如表达式 +a 时,它执行以下步骤:
  • 确定 a 的类型,令其为 T;
  • 为接收者 T 查找一个带有 operator 修饰符的无参函数 unaryPlus(),即成员函数或扩展函数;
  • 如果函数不存在或不明确,则导致编译错误;
  • 如果函数存在且其返回类型为 R,那就表达式 +a 具有类型 R;

注意 这些操作以及所有其他操作都针对基本类型做了优化,不会为它们引入函数调用的开销。

以下是如何重载一元减运算符的示例:

1
2
3
4
5
6
7
8
9
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;
  • 查找一个适用于类型为 T 的接收者的、带有 operator 修饰符的无参数函数 inc();
  • 检测函数的返回类型是 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.mod(b) (已弃用)
a..b a.rangeTo(b)
对于此表中的操作,编译器只是解析成翻译为列中的表达式。

请注意,自 Kotlin 1.1 起支持 rem 运算符。Kotlin 1.0 使用 mod 运算符,它在 Kotlin 1.1 中被弃用。

示例

下面是一个从给定值起始的 Counter 类的示例,它可以使用重载的 + 运算符来增加计数:

1
2
3
4
5
data class Counter(val dayIndex: Int) {
operator fun plus(increment: Int): Counter {
return Counter(dayIndex + increment)
}
}

“In”操作符

表达式 翻译为
a in b b.contains(a)
a !in b !b.contains(a)
对于 in 和 !in,过程是相同的,但是参数的顺序是相反的。

索引访问操作符

表达式 翻译为
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, ……, i_n] a.get(i_1, ……, i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, ……, i_n] = b a.set(i_1, ……, i_n, b)
方括号转换为调用带有适当数量参数的 get 和 set。

调用操作符

表达式 翻译为
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ……, i_n) a.invoke(i_1, ……, i_n)
圆括号转换为调用带有适当数量参数的 invoke。

广义赋值

表达式 翻译为
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b), a.modAssign(b)(已弃用)
对于赋值操作,例如 a += b,编译器执行以下步骤:

1.如果右列的函数可用

  • 如果相应的二元函数(即 plusAssign() 对应于 plus())也可用,那么报告错误(模糊),
  • 确保其返回类型是 Unit,否则报告错误,
  • 生成 a.plusAssign(b) 的代码;

2.否则试着生成 a = a + b 的代码(这里包含类型检测:a + b 的类型必须是 a 的子类型)。

注意:赋值在 Kotlin 中不是表达式。

相等与不等操作符

表达式 翻译为
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))
这些操作符只使用函数 equals(other: Any?): Boolean,可以覆盖它来提供自定义的相等性检测实现。不会调用任何其他同名函数(如 equals(other: Foo))。

注意:=== 和 !==(同一性检测)不可重载,因此不存在对他们的约定。

这个 == 操作符有些特殊:它被翻译成一个复杂的表达式,用于筛选 null 值。 null == null 总是 true,对于非空的 x,x == null 总是 false 而不会调用 x.equals()。

比较操作符

表达式 翻译为
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 值

空安全

可空类型与非空类型

Kotlin 的类型系统旨在消除来自代码空引用的危险,也称为《十亿美元的错误》。

许多编程语言(包括 Java)中最常见的陷阱之一,就是访问空引用的成员会导致空引用异常。在 Java 中,这等同于 NullPointerException 或简称 NPE。

Kotlin 的类型系统旨在从我们的代码中消除 NullPointerException。NPE 的唯一可能的原因可能是:

  • 显式调用 throw NullPointerException();
  • 使用了下文描述的 !! 操作符;
  • 有些数据在初始化时不一致,例如当:

1.传递一个在构造函数中出现的未初始化的 this 并用于其他地方(“泄漏 this”);
2.超类的构造函数调用一个开放成员,该成员在派生中类的实现使用了未初始化的状态;

  • Java 互操作:

1.企图访问平台类型的 null 引用的成员;
2.用于具有错误可空性的 Java 互操作的泛型类型,例如一段 Java 代码可能会向 Kotlin 的 MutableList 中加入 null,这意味着应该使用 MutableList<String?> 来处理它;
3.由外部 Java 代码引发的其他问题。

在 Kotlin 中,类型系统区分一个引用可以容纳 null (可空引用)还是不能容纳(非空引用)。 例如,String 类型的常规变量不能容纳 null:

1
2
var a: String = "abc" // 默认情况下,常规初始化意味着非空
a = null // 编译错误

如果要允许为空,我们可以声明一个变量为可空字符串,写作 String?:

1
2
3
var b: String? = "abc" // 可以设置为空
b = null // ok
print(b)

现在,如果你调用 a 的方法或者访问它的属性,它保证不会导致 NPE,这样你就可以放心地使用:

1
val l = a.length

但是如果你想访问 b 的同一个属性,那么这是不安全的,并且编译器会报告一个错误:

1
val l = b.length // 错误:变量“b”可能为空

但是我们还是需要访问该属性,对吧?有几种方式可以做到。

在条件中检测 null

首先,你可以显式检测 b 是否为 null,并分别处理两种可能:

1
val l = if (b != null) b.length else -1

编译器会跟踪所执行检测的信息,并允许你在 if 内部调用 length。 同时,也支持更复杂(更智能)的条件:

1
2
3
4
5
6
val b: String? = "Kotlin"
if (b != null && b.length > 0) {
print("String of length ${b.length}")
} else {
print("Empty string")
}

请注意,这只适用于 b 是不可变的情况(即在检测和使用之间没有修改过的局部变量 ,或者不可覆盖并且有幕后字段的 val 成员),因为否则可能会发生在检测之后 b 又变为 null 的情况。

安全的调用

你的第二个选择是安全调用操作符,写作 ?.:

1
2
3
4
5
6
7
8
9
10
val a = "Kotlin"
val b: String? = null
println(b?.length)
println(a?.length) // 无需安全调用
``````kotlin
如果 b 非空,就返回 b.length,否则返回 null,这个表达式的类型是 Int?。

安全调用在链式调用中很有用。例如,如果一个员工 Bob 可能会(或者不会)分配给一个部门, 并且可能有另外一个员工是该部门的负责人,那么获取 Bob 所在部门负责人(如果有的话)的名字,我们写作:
```kotlin
bob?.department?.head?.name

如果任意一个属性(环节)为空,这个链式调用就会返回 null。

如果要只对非空值执行某个操作,安全调用操作符可以与 let 一起使用:

1
2
3
4
val listWithNulls: List<String?> = listOf("Kotlin", null)
for (item in listWithNulls) {
item?.let { println(it) } // 输出 Kotlin 并忽略 null
}

安全调用也可以出现在赋值的左侧。这样,如果调用链中的任何一个接收者为空都会跳过赋值,而右侧的表达式根本不会求值:

1
2
// 如果 `person` 或者 `person.department` 其中之一为空,都不会调用该函数:
person?.department?.head = managersPool.getManager()

Elvis 操作符

当我们有一个可空的引用 b 时,我们可以说“如果 b 非空,我使用它;否则使用某个非空的值”:

1
val l: Int = if (b != null) b.length else -1

除了完整的 if-表达式,这还可以通过 Elvis 操作符表达,写作 ?::

1
val l = b?.length ?: -1

如果 ?: 左侧表达式非空,elvis 操作符就返回其左侧表达式,否则返回右侧表达式。 请注意,当且仅当左侧为空时,才会对右侧表达式求值。

请注意,因为 throw 和 return 在 Kotlin 中都是表达式,所以它们也可以用在 elvis 操作符右侧。这可能会非常方便,例如,检测函数参数:

1
2
3
4
5
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ……
}

!! 操作符

第三种选择是为 NPE 爱好者准备的:非空断言运算符(!!)将任何值转换为非空类型,若该值为空则抛出异常。我们可以写 b!! ,这会返回一个非空的 b 值 (例如:在我们例子中的 String)或者如果 b 为空,就会抛出一个 NPE 异常:

1
val l = b!!.length

因此,如果你想要一个 NPE,你可以得到它,但是你必须显式要求它,否则它不会不期而至。

安全的类型转换

如果对象不是目标类型,那么常规类型转换可能会导致 ClassCastException。 另一个选择是使用安全的类型转换,如果尝试转换不成功则返回 null:

1
val aInt: Int? = a as? Int

可空类型的集合

如果你有一个可空类型元素的集合,并且想要过滤非空元素,你可以使用 filterNotNull 来实现:

1
2
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()

异常

异常类
Kotlin 中所有异常类都是 Throwable 类的子孙类。 每个异常都有消息、堆栈回溯信息以及可选的原因。

使用 throw-表达式来抛出异常:

1
throw Exception("Hi There!")

使用 try-表达式来捕获异常:

1
2
3
4
5
6
7
8
9
try {
// 一些代码
}
catch (e: SomeException) {
// 处理程序
}
finally {
// 可选的 finally 块
}

可以有零到多个 catch 块。finally 块可以省略。 但是 catch 与 finally 块至少应该存在一个。

Try 是一个表达式

try 是一个表达式,即它可以有一个返回值:

1
val a: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }

try-表达式的返回值是 try 块中的最后一个表达式或者是(所有)catch 块中的最后一个表达式。 finally 块中的内容不会影响表达式的结果。

受检的异常

Kotlin 没有受检的异常。这其中有很多原因,但我们会提供一个简单的例子。

以下是 JDK 中 StringBuilder 类实现的一个示例接口:

1
Appendable append(CharSequence csq) throws IOException;

这个签名是什么意思? 它是说,每次我追加一个字符串到一些东西(一个 StringBuilder、某种日志、一个控制台等)上时我就必须捕获那些 IOException。 为什么?因为它可能正在执行 IO 操作(Writer 也实现了 Appendable)…… 所以它导致这种代码随处可见的出现:

1
2
3
4
5
6
try {```kotlin
log.append(message)
}
catch (IOException e) {
// 必须要安全
}

这并不好,参见《Effective Java》第三版 第 77 条:不要忽略异常。

Bruce Eckel says about checked exceptions:

通过一些小程序测试得出的结论是异常规范会同时提高开发者的生产力与代码质量,但是大型软件项目的经验表明一个不同的结论——生产力降低、代码质量很少或没有提高。

Nothing 类型

在 Kotlin 中 throw 是表达式,所以你可以使用它(比如)作为 Elvis 表达式的一部分:

1
val s = person.name ?: throw IllegalArgumentException("Name required")

throw 表达式的类型是特殊类型 Nothing。 该类型没有值,而是用于标记永远不能达到的代码位置。 在你自己的代码中,你可以使用 Nothing 来标记一个永远不会返回的函数:

1
2
3
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}

当你调用该函数时,编译器会知道在该调用后就不再继续执行了:

1
2
val s = person.name ?: fail("Name required")
println(s) // 在此已知“s”已初始化

可能会遇到这个类型的另一种情况是类型推断。这个类型的可空变体 Nothing? 有一个可能的值是 null。如果用 null 来初始化一个要推断类型的值,而又没有其他信息可用于确定更具体的类型时,编译器会推断出 Nothing? 类型:

1
2
val x = null           // “x”具有类型 `Nothing?`
val l = listOf(null) // “l”具有类型 `List<Nothing?>

作用域函数

Kotlin 标准库包含几个函数,它们的唯一目的是在对象的上下文中执行代码块。当对一个对象调用这样的函数并提供一个 lambda 表达式时,它会形成一个临时作用域。在此作用域中,可以访问该对象而无需其名称。这些函数称为作用域函数。共有以下五种:let、run、with、apply 以及 also。

这些函数基本上做了同样的事情:在一个对象上执行一个代码块。不同的是这个对象在块中如何使用,以及整个表达式的结果是什么。

下面是作用域函数的典型用法:

1
2
3
4
5
6
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}

如果不使用 let 来写这段代码,就必须引入一个新变量,并在每次使用它时重复其名称。

1
2
3
4
5
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)

作用域函数没有引入任何新的技术,但是它们可以使你的代码更加简洁易读。

由于作用域函数的相似性质,为你的案例选择正确的函数可能有点棘手。选择主要取决于你的意图和项目中使用的一致性。下面我们将详细描述各种作用域函数及其约定用法之间的区别。

区别

由于作用域函数本质上都非常相似,因此了解它们之间的区别很重要。每个作用域函数之间有两个主要区别:

  • 引用上下文对象的方式
  • 返回值

上下文对象:this 还是 it

在作用域函数的 lambda 表达式里,上下文对象可以不使用其实际名称而是使用一个更简短的引用来访问。每个作用域函数都使用以下两种方式之一来访问上下文对象:作为 lambda 表达式的接收者(this)或者作为 lambda 表达式的参数(it)。两者都提供了同样的功能,因此我们将针对不同的场景描述两者的优缺点,并提供使用建议。

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() {
val str = "Hello"
// this
str.run {
println("The receiver string length: $length")
//println("The receiver string length: ${this.length}") // 和上句效果相同
}

// it
str.let {
println("The receiver string's length is ${it.length}")
}
}
this

run、with 以及 apply 通过关键字 this 引用上下文对象。因此,在它们的 lambda 表达式中可以像在普通的类函数中一样访问上下文对象。在大多数场景,当你访问接收者对象时你可以省略 this,来让你的代码更简短。相对地,如果省略了 this,就很难区分接收者对象的成员及外部对象或函数。因此,对于主要对对象成员进行操作(调用其函数或赋值其属性)的 lambda 表达式,建议将上下文对象作为接收者(this)。

1
2
3
4
5
val adam = Person("Adam").apply { 
age = 20 // 和 this.age = 20 或者 adam.age = 20 一样
city = "London"
}
println(adam)
it

反过来,let 及 also 将上下文对象作为 lambda 表达式参数。如果没有指定参数名,对象可以用隐式默认名称 it 访问。it 比 this 简短,带有 it 的表达式通常更容易阅读。然而,当调用对象函数或属性时,不能像 this 这样隐式地访问对象。因此,当上下文对象在作用域中主要用作函数调用中的参数时,使用 it 作为上下文对象会更好。若在代码块中使用多个变量,则 it 也更好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}

val i = getRandomInt()
``````kotlin
此外,当将上下文对象作为参数传递时,可以为上下文对象指定在作用域内的自定义名称。
```kotlin
fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
writeToLog("getRandomInt() generated value $value")
}
}

val i = getRandomInt()

返回值

根据返回结果,作用域函数可以分为以下两类:

  • apply 及 also 返回上下文对象。
  • let、run 及 with 返回 lambda 表达式结果.

这两个选项使你可以根据在代码中的后续操作来选择适当的函数。

上下文对象

apply 及 also 的返回值是上下文对象本身。因此,它们可以作为辅助步骤包含在调用链中:你可以继续在同一个对象上进行链式函数调用。

1
2
3
4
5
6
7
8
9
val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") }
.apply {
add(2.71)
add(3.14)
add(1.0)
}
.also { println("Sorting the list") }
.sort()

它们还可以用在返回上下文对象的函数的 return 语句中。

1
2
3
4
5
6
7
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}

val i = getRandomInt()
Lambda 表达式结果

let、run 及 with 返回 lambda 表达式的结果。所以,在需要使用其结果给一个变量赋值,或者在需要对其结果进行链式操作等情况下,可以使用它们。

1
2
3
4
5
6
7
val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run {
add("four")
add("five")
count { it.endsWith("e") }
}
println("There are $countEndsWithE elements that end with e.")

此外,还可以忽略返回值,仅使用作用域函数为变量创建一个临时作用域。

1
2
3
4
5
6
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")
}

几个函数

为了帮助你为你的场景选择合适的作用域函数,我们会详细地描述它们并且提供一些使用建议。从技术角度来说,作用域函数在很多场景里是可以互换的,所以这些示例展示了定义通用使用风格的约定用法。

let

上下文对象作为 lambda 表达式的参数(it)来访问。返回值是 lambda 表达式的结果。

let 可用于在调用链的结果上调用一个或多个函数。例如,以下代码打印对集合的两个操作的结果:

1
2
3
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)

使用 let,可以写成这样:

1
2
3
4
5
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// 如果需要可以调用更多函数
}

若代码块仅包含以 it 作为参数的单个函数,则可以使用方法引用(::)代替 lambda 表达式:

1
2
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)

let 经常用于仅使用非空值执行代码块。如需对非空对象执行操作,可对其使用安全调用操作符 ?. 并调用 let 在 lambda 表达式中执行操作。

1
2
3
4
5
6
7
val str: String? = "Hello" 
//processNonNullString(str) // 编译错误:str 可能为空
val length = str?.let {
println("let() called on $it")
processNonNullString(it) // 编译通过:'it' 在 '?.let { }' 中必不为空
it.length
}

使用 let 的另一种情况是引入作用域受限的局部变量以提高代码的可读性。如需为上下文对象定义一个新变量,可提供其名称作为 lambda 表达式参数来替默认的 it。

1
2
3
4
5
6
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem ->
println("The first item of the list is '$firstItem'")
if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.toUpperCase()
println("First item after modifications: '$modifiedFirstItem'")

with

一个非扩展函数:上下文对象作为参数传递,但是在 lambda 表达式内部,它可以作为接收者(this)使用。 返回值是 lambda 表达式结果。

我们建议使用 with 来调用上下文对象上的函数,而不使用 lambda 表达式结果。 在代码中,with 可以理解为“对于这个对象,执行以下操作。”

1
2
3
4
5
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}

with 的另一个使用场景是引入一个辅助对象,其属性或函数将用于计算一个值。

1
2
3
4
5
6
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," +
" the last element is ${last()}"
}
println(firstAndLast)

run

上下文对象 作为接收者(this)来访问。 返回值 是 lambda 表达式结果。

run 和 with 做同样的事情,但是调用方式和 let 一样——作为上下文对象的扩展函数.

当 lambda 表达式同时包含对象初始化和返回值的计算时,run 很有用。

1
2
3
4
5
6
7
8
9
10
11
12
val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}

// 同样的代码如果用 let() 函数来写:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}

除了在接收者对象上调用 run 之外,还可以将其用作非扩展函数。 非扩展 run 可以使你在需要表达式的地方执行一个由多个语句组成的块。

1
2
3
4
5
6
7
8
9
10
11
val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"

Regex("[$sign]?[$digits$hexDigits]+")
}

for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
println(match.value)
}

apply

上下文对象 作为接收者(this)来访问。 返回值 是上下文对象本身。

对于不返回值且主要在接收者(this)对象的成员上运行的代码块使用 apply。apply 的常见情况是对象配置。这样的调用可以理解为“将以下赋值操作应用于对象”。

1
2
3
4
5
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)

将接收者作为返回值,你可以轻松地将 apply 包含到调用链中以进行更复杂的处理。

also

上下文对象作为 lambda 表达式的参数(it)来访问。 返回值是上下文对象本身。

also 对于执行一些将上下文对象作为参数的操作很有用。 对于需要引用对象而不是其属性与函数的操作,或者不想屏蔽来自外部作用域的 this 引用时,请使用 also。

当你在代码中看到 also 时,可以将其理解为“并且用该对象执行以下操作”。

1
2
3
4
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")

函数选择

为了帮助你选择合适的作用域函数,我们提供了它们之间的主要区别表。

函数 对象引用 返回值 是否是扩展函数
let it Lambda 表达式结果
run this Lambda 表达式结果
run - Lambda 表达式结果 不是:调用无需上下文对象
with this Lambda 表达式结果 不是:把上下文对象当做参数
apply this 上下文对象
also it 上下文对象

以下是根据预期目的选择作用域函数的简短指南:

  • 对一个非空(non-null)对象执行 lambda 表达式:let
  • 将表达式作为变量引入为局部作用域中:let
  • 对象配置:apply
  • 对象配置并且计算结果:run
  • 在需要表达式的地方运行语句:非扩展的 run
  • 附加效果:also
  • 一个对象的一组函数调用:with

不同函数的使用场景存在重叠,你可以根据项目或团队中使用的特定约定选择函数。

尽管作用域函数是使代码更简洁的一种方法,但请避免过度使用它们:这会降低代码的可读性并可能导致错误。避免嵌套作用域函数,同时链式调用它们时要小心:此时很容易对当前上下文对象及 this 或 it 的值感到困惑。

takeIf 与 takeUnless

除了作用域函数外,标准库还包含函数 takeIf 及 takeUnless。这俩函数使你可以将对象状态检查嵌入到调用链中。

当以提供的谓词在对象上进行调用时,若该对象与谓词匹配,则 takeIf 返回此对象。否则返回 null。因此,takeIf 是单个对象的过滤函数。反之,takeUnless如果不匹配谓词,则返回对象,如果匹配则返回 null。该对象作为 lambda 表达式参数(it)来访问。

1
2
3
4
5
val number = Random.nextInt(100)

val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")

当在 takeIf 及 takeUnless 之后链式调用其他函数,不要忘记执行空检查或安全调用(?.),因为他们的返回值是可为空的。

1
2
3
4
val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.toUpperCase()
//val caps = str.takeIf { it.isNotEmpty() }.toUpperCase() // 编译错误
println(caps)

takeIf 及 takeUnless 与作用域函数一起特别有用。 一个很好的例子是用 let 链接它们,以便在与给定谓词匹配的对象上运行代码块。 为此,请在对象上调用 takeIf,然后通过安全调用(?.)调用 let。对于与谓词不匹配的对象,takeIf 返回 null,并且不调用 let。

1
2
3
4
5
6
7
8
9
fun displaySubstringPosition(input: String, sub: String) {
input.indexOf(sub).takeIf { it >= 0 }?.let {
println("The substring $sub is found in $input.")
println("Its start position is $it.")
}
}

displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")

没有标准库函数时,相同的函数看起来是这样的:

1
2
3
4
5
6
7
8
9
10
fun displaySubstringPosition(input: String, sub: String) {
val index = input.indexOf(sub)
if (index >= 0) {
println("The substring $sub is found in $input.")
println("Its start position is $index.")
}
}

displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")