Kotlin

Table of Contents

1. 简介

Kotlin 是一个由 JetBrains 开发的多平台的静态语言。Kotlin 可以编译成 Java 字节码/JavaScript 代码/Native 代码,跨平台开发(Kotlin Multiplatform)是 Kotlin 的主要使用场景之一。

安装 IntelliJ IDEA 可以获得 Kotlin 的开发编译环境;如果只是想简单试试,则可以使用在线环境:https://play.kotlinlang.org/

1.1. Hello world

下面是 Kotlin 版本的 Hello world 程序:

fun main() {
    println("Hello, world!!!")
}

2. 基本语法

2.1. 一次赋值变量(val)和可重赋值变量(var)

Kotlin 中, 使用 val (来源于单词 value 前 3 个字母)声明的变量只能赋值一次;而使用 var (来源于单词 variable 前 3 个字母)声明的变量可以赋值多次。

下面是 valvar 的例子:

val a = 1         // a 使用 val 声明,不能被重赋值
a = 2             // 这一行会报错。因为 a 是使用 val 声明,所以 a 在上一行被赋值后不能再赋值

val b: Int        // 声明 b 为 Int 类型
b = 3             // 这一行不会报错。因为 b 在上一行只使用 val 进行了声明,并没有赋值

var c = 3
c += 4            // c 使用 var 声明,可以被重赋值

2.1.1. 常量(constant)

如果一个用 val 声明的 top-level 变量在编译时就已经知道它的值,那么我们可以为它增加修饰符 constant,这样编译器会把它进行内联以提高性能。

const val CONSTANT_A: String = "This is constant A"           // 这是正确用法

fun main() {
    println(CONSTANT_A)

    // const val CONSTANT_B: String = "This is constant B"    // 这行会报错,因为它是函数内的局部变量,不是 top-level 变量
    // println(CONSTANT_B)
}

关于 constant 的其它约束可以参考:https://kotlinlang.org/docs/properties.html#compile-time-constants

2.2. 基本类型(Basic types)

Kotlin 中,什么东西都是对象,基本类型也是对象,我们可以调用对象的方法。例如,可以调用 InttoChar() 方法把整数 65 转换为字符 A:

val x: Int = 65
println(x.toChar())     // Print: A

2.2.1. 整数类型

Kotlin 中的整数类型如表 1 所示。

Table 1: Kotlin 中的整数类型
Type Size (bits) Min value Max value Literal
Byte 8 -128 127 N/A
Short 16 -32768 32767 N/A
Int 32 -2,147,483,648 (\(-2^{31}\)) 2,147,483,647 (\(2^{31}\) - 1) 10, -12, 0xA3B4, 0xA3_B4, 0b11, 1_000_000
Long 64 -9,223,372,036,854,775,808 (\(-2^{63}\)) 9,223,372,036,854,775,807 (\(2^{63}\) - 1) 10L, -12L, 0xA3B4, 0xA3_B4L, 0b11L, 1_000_000L
UByte 8 0 255 N/A
UShort 16 0 65,535 N/A
UInt 32 0 4,294,967,295 (\(2^{32}\) - 1) 10u, 10U, 0xA3u, 0xA3U, 0b11u, 0b11U
ULong 64 0 18,446,744,073,709,551,615 (\(2^{64}\) - 1) 10uL, 10UL, 0xA3uL, 0xA3UL, 0b11uL, 0b11UL

如果没有明确指定类型,编译器会根据 Literal 的形式自动推导为 Int/UInt 或者 Long/ULong 类型。

val one = 1                     // Int
val threeBillion = 3000000000   // Long,由于 Int 类型不足以保存这么大的数字,所以会自动推导为 Long
val oneLong = 1L                // Long

val one = 1u                     // UInt
val threeBillion = 3000000000uL  // ULong
val oneLong = 1uL                // ULong

2.2.2. 浮点数类型

Kotlin 中的整数类型如表 2 所示。

Table 2: Kotlin 中的浮点数类型
Type Size (bits) Significant bits Exponent bits Decimal digits Literal
Float 32 24 8 6-7 1.0f, 2.3f
Double 64 53 11 15-16 1.0, 2.3

2.2.3. 布尔类型

Kotlin 中的 Boolean 类型有两个字面量 truefalse ,它们支持逻辑或 || 、逻辑与 && 和逻辑取反 ! 运算。比如:

val myTrue: Boolean = true
val myFalse: Boolean = false

println(myTrue || myFalse)    // true
println(myTrue && myFalse)    // false
println(!myTrue)              // false

2.2.4. 字符类型

Kotlin 中的 Char 类型的字面量使用单引号表示。单引号中的反斜线有特殊含义,会进行转义。比如:

val a = 'h'         // a 类型为 Char
val b = '我'        // b 类型为 Char,表示一个汉字
var c = '\n'        // c 类型为 Char,表示换行符

println(a)
println(b)
println(c)          // Prints an extra newline character

2.2.5. 字符串

Kotlin 中的 String 类型的字面量使用双引号表示。比如:

val s = "Hello, world!\n"

多行字符串使用 3 个双引号表示。比如:

val text = """
    for (c in "foo")
        print(c)
"""

如果多行字符串有 margin prefix,则可以使用 trimMargin() 删除它们。

2.2.5.1. String templates

下面是 String templates 的例子:

val s = "abc"
println("s is $s")                          // s is abc
println("s is ${s}")                        // s is abc, same as above
println("$s.length is ${s.length}")         // abc.length is 3
println("${s}.length is ${s.length}")       // abc.length is 3, same as above
println("uppercase is ${s.uppercase()}")    // uppercase is ABC
2.2.5.2. String format

Kotlin 中的 String format 只支持 JVM 平台,所以更推荐使用上一节介绍的 String templates。

下面是 String format 的例子:

// Formats two strings to uppercase, each taking one placeholder
val helloString = String.format("%S %S", "hello", "world")
println(helloString)                      // HELLO WORLD

2.2.6. 数组

数组(Array)中的元素必须是相同的类型,而且元素个数不能动态改变(即:当我们增加或删除数组中元素时,会创建新的数组然后进行复制)。在 Kotlin 中,更常使用的是标准库中的 Collections,仅当一些特别需求(比如 low-level 操作)情况下才会使用 Array。

通过下面方式可以创建数组:

  1. arrayOf(), arrayOfNulls()
  2. emptyArray()
  3. Array 构造函数

下面是创建 Array 的例子:

// Creates an array with values [1, 2, 3]
val simpleArray = arrayOf(1, 2, 3)                                    // 创建数组的方式:使用 arrayOf
println(simpleArray.joinToString())

// Creates an array with values [null, null, null]
val nullArray: Array<Int?> = arrayOfNulls(3)                          // 创建数组的方式:使用 arrayOfNulls
println(nullArray.joinToString())

// Create an empty array with the emptyArray() function:
var exampleArray1 = emptyArray<String>()                              // 创建数组的方式:使用 emptyArray
var exampleArray2: Array<String> = emptyArray()  // same as above     // 创建数组的方式:使用 emptyArray

// Creates an Array<String> with values ["10", "10", "10", "10", "10"]
val initArray = Array<Int>(5) { 10 }                                  // 创建数组的方式:使用 Array 构造函数
println(initArray.joinToString())  // 10, 10, 10, 10, 10

// Creates an Array<String> with values ["0", "1", "4", "9", "16"]
val asc = Array(5) { i -> (i * i).toString() } // i starts from 0     // 创建数组的方式:使用 Array 构造函数
asc.forEach { print(it) }   // it 是隐式参数 https://kotlinlang.org/docs/lambdas.html#it-implicit-name-of-a-single-parameter
2.2.6.1. Primitive-type arrays

如果对 Primitive 值使用 Array 类,这些值将被装箱到对象中。如果你想追求更好的性能,则不要使用 Array 类,而应该直接使用表 3 中的类。

Table 3: Primitive-type arrays(比 Array 类性能更好)
Primitive-type array Equivalent in Java
BooleanArray boolean[]
ByteArray byte[]
CharArray char[]
DoubleArray double[]
FloatArray float[]
IntArray int[]
LongArray long[]
ShortArray short[]

2.2.7. Collections

List 实例:

fun main() {
    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")}")
}

Set 实例:

fun main() {
    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}")
}

Map(Dictionary)实例:

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

    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") // same as previous
}

参考:https://kotlinlang.org/docs/collections-overview.html

2.3. 流程控制

2.3.1. if

Kotlin 中 if 是表示式,它返回一个值。这样,我们不再需要三元操作符了。

下面是 if 的使用例子:

fun main() {
    val a = 2
    val b = 3

    var max = a
    if (a < b) max = b                            // if 可省略大括号,可以没有 else

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

    // As expression
    max = if (a > b) a else b                     // 把 if 表示式的值赋值给 max,相当于三元操作符

    // You can also use `else if` in expressions:
    val maxLimit = 1
    val maxOrLimit = if (maxLimit > a) maxLimit else if (a > b) a else b


    println("max is $max")
    println("maxOrLimit is $maxOrLimit")
}

2.3.2. when

Kotlin 中 when 相当于其它语言中的 switch 语句。

下面是 when 的使用例子:

fun main() {
    val x = 4

    when (x) {
        0, 1 -> print("x == 0 or x == 1")
        2 -> print("x == 2")
        in 3..10 -> print("x is in the range [3, 10]")
        else -> print("otherwise")
    }
}

2.3.3. for

下面是 for 的使用例子:

fun main() {
    for (i in 1..4) {         // 会输入 1,2,3,4
        println(i)
    }

    val a = arrayOf(1, 3, 9)
    for (i in a) {
        println(i)
    }
}

2.3.4. while

Kotlin 中支持 whiledo...while ,它们的区别在于 do...while 中的语句至少会执行一次。

下面是 while 的使用例子:

fun main() {
    var i = 1

    while (i <= 5) {
        println("i = $i")
        ++i
    }
}

下面是 do...while 的使用例子:

fun main() {
    var sum: Int = 0
    var input: String

    do {
        print("Enter an integer (enter 0 to end): ")
        input = readLine()!!
        sum += input.toInt()

    } while (input != "0")

    println("sum = $sum")
}

2.3.5. break, continue, return

break 可以跳出最内层的循环,如:

fun main() {
    for (i in 1..5) {                   // 输出 1,2
        if (i == 3) {
            break
        }
        println(i)
    }
}

continue 可以中止最内层循环的当前迭代,继续下一次迭代,如:

fun main() {
    for (i in 1..5) {                   // 输出 1,2,4,5
        if (i == 3) {
            break
        }
        println(i)
    }
}
2.3.5.1. labels(@)

如果有嵌套的循环,想使用 break/continue 跳到外层循环,则可以使用 label。

下面是 label 的使用例子:

fun main() {
    outer@ for (i in 1..4) {            // 这里在外层循环外定义了 label,名为 outer,名称最后要接个符号 @
        for (j in 1..2) {
            if (i == 3)
                break@outer             // 指定 label 名 break。注意:break 和 label 名 outer 之间只有一个符号 @,没有空格

            println("i = $i; j = $j")
        }
    }

    println("------------")
    outer@ for (i in 1..3) {
        for (j in 1..2) {
            if (i == 2)
                continue@outer          // 指定 label 名 continue。注意:continue 和 label 名 outer 之间只有一个符号 @,没有空格

            println("i = $i; j = $j")
        }
    }
}

运行上面例子,会输出:

i = 1; j = 1
i = 1; j = 2
------------
i = 1; j = 1
i = 1; j = 2
i = 3; j = 1
i = 3; j = 2
2.3.5.2. Return to labels

关键字 return 一般用于从函数返回。比如:

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach {
        if (it == 3) return                       // non-local return directly to the caller of foo()
        print(it)
    }
    print("this point is unreachable")
}

上面函数会输出 12。但如果我只想跳出 lambda 表达式(比如跳过这次 forEach 迭代,接着执行下一个 forEach 迭代,即想让 foo 函数输出 1245),怎么办呢?答案是使用 label,比如:

// 模拟 continue 办法 1:明确指定 label 名称
fun foo() {
    listOf(1, 2, 3, 4, 5).forEach lit@{           // 定义了一个名为 lit 的 label
        if (it == 3) return@lit                   // 跳过这次 forEach 迭代,接着执行下一个 forEach 迭代。有点像 continue
        print(it)
    }
    print(" done with explicit label")
}

你可能有疑问为什么不直接使用 continue 呢?这是 因为 continue/break 都只能用于常规的循环(如 for/while/do...while )。

我们也可以直接使用隐含的 label 名,比如:

// 模拟 continue 办法 2:使用隐含的 label 名
fun foo() {
    listOf(1, 2, 3, 4, 5).forEach {
        if (it == 3) return@forEach               // lambda 表达式名称 forEach 是一个隐含的 label 名
        print(it)
    }
    print(" done with implicit label")
}

最后,介绍第 3 种方法:使用匿名函数替换 lambda 表达式,比如:

// 模拟 continue 办法 3:使用匿名函数替换 lambda 表达式,在匿名函数中直接使用 return 语句
fun foo() {
    listOf(1, 2, 3, 4, 5).forEach(fun(value: Int) {
        if (value == 3) return                    // local return to the caller of the anonymous function - the forEach loop
        print(value)
    })
    print(" done with anonymous function")
}

上面 3 个例子的语义都和 continue 类似,如何实现 break 语义呢?方法如下:

// 模拟 break
fun foo() {
    run label1@{                                  // 关于 run 可参考 https://kotlinlang.org/docs/scope-functions.html#run
        listOf(1, 2, 3, 4, 5).forEach {
            if (it == 3) return@label1            // non-local return from the lambda passed to run
            print(it)
        }
    }
    print(" done with nested loop")               // 前面的 print 会输出 12,这一行 print 内容也会被输出
}

2.4. 异常处理

Kotlin 中所有异常都继承于 Throwable 类。Kotlin 没有 Java 中的 Checked exceptions(即必须抛出的异常)。

使用 throw 可以抛出异常,比如 throw IllegalArgumentException("Invalid arg")

使用 try...catch...finally 可以捕获异常,如:

fun main() {
    try {
        var num = 10 / 0                // throws exception
        println(num)
    } catch (e: Exception) {
        println("$e")
    }
}

可以有零个或者多个 catch 语句,可以省略 finally 语句,但是至少有一个 catch 或者 finally 语句。

2.4.1. Try 是表达式

Kotlin 中, try...catch...finally 是表达式,也就是说它会返回某个值。比如:

val a: Int? = try { input.toInt() } catch (e: NumberFormatException) { null }

2.4.2. Nothing 类型

throw 表达式的类型是 Nothing 。这个类型没有值,它一般用于标记一个不会到达的代码位置。你可以声明一个函数的返回类型为 Nothing ,表示它不会返回。比如:

fun fail(message: String): Nothing {              // 也可以不明确指定它的返回类型,因为编译器有类型推导系统
    throw IllegalArgumentException(message)
}

你也可能在其它地方看到过 Nothing 类型。比如:

val x = null                                      // 'x' has type `Nothing?`
val l = listOf(null)                              // 'l' has type `List<Nothing?>

2.5. 类和对象

2.5.1. 类构造函数

Kotlin 中使用关键字 class 来定义类。它支持两种构造函数:

  1. Primary Constructor,一个类只能有一个 Primary Constructor;
  2. Secondary Constructors,一个类可以没有,也可以有多个 Secondary Constructors。

Primary Constructor 是在类声明头中的构造函数,下面是 Primary Constructor 的例子:

class Person constructor(val name: String, var age: Int) {  // 这是 Primary Constructor。可省掉 constructor 关键字,即写为 class Person (val name: String, var age: Int) {

    fun getInfo(): String {
        return "Name: $name\nAge: $age"
    }

    init {                                                  // Primary Constructor 中不能有复杂逻辑,可以把初始化逻辑代码放入 init 块中
        println("First initializer block that prints $name")
    }
}

fun main() {
    val person1 = Person("Alice", 25)                       // Kotlin 不使用 new 创建对象

    println(person1.name)
    println(person1.age)

    person1.age += 1

    println(person1.getInfo())
}

Secondary Constructors 是名为 constructor 的成员函数,可以有多个,比如:

class Person(val name: String, var age: Int) {                              // 这是 primary constructor
    val children: MutableList<Person> = mutableListOf()

    constructor(name: String, age: Int, parent: Person) : this(name, age) { // 第一个 secondary constructor
        parent.children.add(this)
    }

    constructor(name: String, parent: Person) : this(name, 0) {             // 第二个 secondary constructor
        parent.children.add(this)
    }

    fun getInfo(): String {
        return "Name: $name\nAge: $age\nChildren number: ${children.size}"
    }
}

fun main() {
    val parent = Person("Alice", 32)                        // Kotlin 不使用 new 创建对象
    val child1 = Person("Joe", 3, parent)                   // Kotlin 不使用 new 创建对象
    val child2 = Person("Winny", parent)                    // Kotlin 不使用 new 创建对象

    println(parent.getInfo())
    println(child1.getInfo())
    println(child2.getInfo())
}

2.5.2. 类的属性

类的属性的语法为:

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

下面是类属性的例子:

class Rectangle(val width: Int, val height: Int) {    // 这个类有三个属性 width,height,area

    val area: Int
        get() = this.width * this.height
}

fun main() {
    val r = Rectangle(10, 20)
    println("width = ${r.width}")       // width = 10
    println("height = ${r.height}")     // height = 20
    println("area = ${r.area}")         // area = 200
}
2.5.2.1. 后备字段(field)

在介绍后备字段(Backing fields)前,先看一段代码:

// 这是一段有问题的代码,不要使用
class Person {
    var name: String = ""

    var age: Int = 0
        set(value) {
            if (value >= 0) {
                age = value             // 这里有问题!不能在 setter 中对当前属性赋值,这会无限调用 setter,导致程序崩溃
            }
        }
}

fun main() {
    var p = Person()
    p.age = 1
    println(p.age)
}

上面代码中,属性 age 的 setter 中有语句 age = value ,这会造成无限调用 age 的 setter,从而导致程序崩溃。

解决办法是: 把 setter 中对当前属性的引用改为 field 即:

class Person {
    var name: String = ""

    var age: Int = 0
        set(value) {
            if (value >= 0) {
                field = value           // setter 中如果要对当前属性赋值,请使用 field
            }
        }
}

fun main() {
    var p = Person()
    p.age = 1
    println(p.age)
}

同样的道理, 不能在 getter 中读取当前属性,如果有这样的需求,则应该修改为 field

2.5.3. 类的继承

Kotlin 中所有类都有一个共同的父类: AnyAny 有三个方法 equals(), hashCode(), toString() 。也就是说,Kotlin 所有类都有这三个方法。

默认,Kotlin 中的类都是 final 的,不能被继承。要让一个类可以被其它类继承,则需要使用 open 关键字,比如:

open class Base(p: Int)                     // 使用了 open 关键字,这样其它类可以继承 Base 了
class Derived(p: Int) : Base(p)             // 类 Derived 继承于类 Base
2.5.3.1. 重载(Overriding)

类中的方法和属性可以被重载(Overriding)。但需要两个操作:

  1. 父类中的方法或属性必须增加 open 关键字;
  2. 子类中的方法或属性必须增加 override 关键字。

下面是重载类中方法的例子:

open class Shape {
    open fun draw() { /*...*/ }             // open 修饰父类的 draw
    fun fill() { /*...*/ }
}

class Circle() : Shape() {
    override fun draw() { /*...*/ }         // override 修饰子类的 draw
}

open class Rectangle() : Shape() {
    final override fun draw() { /*...*/ }   // 如果有 final,则表示 draw 不能被 Rectangle 的子类重载了
}

2.5.4. 可见性

Classes, objects, interfaces, and functions 等都可以控制其可见性。Kotlin 有表 4 所示机制来控制可见性。

Table 4: public/internal/protected/private 的作用
Modifier 说明
public(默认) 任何位置都可见。不用明确指定,这是默认值
internal 同一个 module(module 由多个文件组成)内部可见
protected 修改类成员表示当前类及其子类可见
private 修改类成员表示当前类可见。修饰其它时表示当前文件可见

下面是使用 private 修饰函数,internal 修饰全局变量的例子:

// file name: example.kt
package foo

private fun foo() { ... }     // visible inside example.kt

internal val baz = 6          // visible inside the same module

下面是修饰类成员的例子:

open class Outer {
    private val a = 1
    protected open val b = 2
    internal open val c = 3
    val d = 4             // public by default

    protected class Nested {
        public val e: Int = 5
    }
}

class Subclass : Outer() {
    // a is not visible
    // b, c and d are visible
    // Nested and e are visible

    override val b = 5   // 'b' is protected
    override val c = 7   // 'c' is internal
}

2.5.5. 抽象类(Abstract classes)

一个类可以被声明为 abstract ,这就是抽象类。不能直接实例化抽象类。

下面是抽象类的例子:

abstract class Person(name: String) {             // Person 是抽象类,不能直接实例化

    init {
        println("My name is $name.")
    }

    fun displaySSN(ssn: Int) {
        println("My SSN is $ssn.")
    }

    abstract fun displayJob(description: String)  // 这个方法是 abstract 的,它没有实现。Person 子类中必须实现这个方法
}

class Teacher(name: String): Person(name) {

    override fun displayJob(description: String) {
        println("I'm a $description teacher.")
    }
}

fun main() {
    val teacher = Teacher("Alice")
    teacher.displayJob("mathematics")
    teacher.displaySSN(12345)
}

2.5.6. 接口(Interfaces)

使用 interface 关键字可以声明接口。和抽象类类似,我们不能直接实例化接口。

下面是接口的例子:

interface Person {                                    // Person 是接口,不能直接实例化
    val name: String                                  // abstract property

    fun displaySSN(ssn: Int) {                        // 这个方法有默认实现
        println("My SSN is $ssn.")
    }

    fun displayJob(description: String)               // abstract method
}

class Teacher(override val name: String) : Person {   // override 了接口的抽象属性 name

    override fun displayJob(description: String) {    // override 了接口的抽象方法 displayJob
        println("I'm a $description teacher.")
    }
}

fun main() {
    val teacher = Teacher("Alice")
    teacher.displayJob("mathematics")
    teacher.displaySSN(12345)
}

抽象类和接口的主要区别在于: 抽象类可以有状态,而接口不保存状态。类只能继承一个抽象类,但可以实现多个接口。

2.5.7. Extension

Kotlin 提供了一种在不修改类的情况下,可以增强类功能的机制,即 Extension。它包含 Extension functions 和 Extension properties 两种机制。

下面是 Extension functions 的例子:

fun String.removeFirstLastChar(): String = this.substring(1, this.length - 1)

fun main() {
    val myString= "abcde"
    val result = myString.removeFirstLastChar()
    println("Result is: $result")                 // bcd
}

如果 Extension functions 是类中已经存在的函数,那么类中的函数总是有更高优先级。比如:

class Example {
    fun printFunctionType() { println("Class method") }
}

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

fun main() {
    Example().printFunctionType()                 // Class method
}

2.5.8. Data classes

如果类仅仅是用于保存数据,则可以使用 Data classes,编译器会自动为它生成一些函数。

比如有 Data class:

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

则编译器自动生成:

  1. equals()/hashCode() pair.
  2. toString() of the form "User(name=John, age=42)".
  3. componentN() functions corresponding to the properties in their order of declaration.
  4. copy() function

其中生成的 copy() 是很有用的,比如:

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

fun main() {
    val jack = User(name = "Jack", age = 1)
    println(jack)             // User(name=Jack, age=1)

    val olderJack = jack.copy(age = 2)
    println(olderJack)        // User(name=Jack, age=2)
}

2.5.9. Enum classes

在类前使用 enum 可以声明为枚举,比如:

enum class RGB { RED, GREEN, BLUE }

fun main() {
    for (color in RGB.entries) println(color.toString())  // prints RED, GREEN, BLUE

    val color = RGB.valueOf("RED")
    println("The first color is: $color")                 // prints "The first color is: RED"
}

2.5.10. Sealed classes

在讨论 Sealed classes 前,先看看 Sealed classes 能解决什么问题。

考虑下面程序:

open class Expr
class Const(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()

fun eval(e: Expr): Int =
    when (e) {
        is Const -> e.value
        is Sum -> eval(e.right) + eval(e.left)
        else ->
            throw IllegalArgumentException("Unknown expression")
    }

fun main() {
    val expr = Const(1)
    println(eval(expr))
}

上面程序运行良好。Expr 有两个子类 Const 和 Sum, 但如果哪一天我们给它增加了另外一个子类,那么 eval 函数中的 when 表达式也需要增加相应的分支,但这很容易忘记。由于 when 表达式有 else 语句,当我们忘记为新子类增加 when 分支时编译器并不能帮我们发现问题。

Sealed classes 就是为了解决这个问题的:

sealed class Expr                                 // 把 class 设置为 sealed 可以解决上面提到的问题
class Const(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()

fun eval(e: Expr): Int =
    when (e) {
        is Const -> e.value
        is Sum -> eval(e.right) + eval(e.left)
    }

fun main() {
    val expr = Const(1)
    println(eval(expr))
}

eval 函数中的 when 表达式中不再需要 else 语句了,这样一旦我们给 Expr 增加了新子类,编译会强制要求我们在 when 表达式中增加相应的分支,否则编译器报错。

2.5.11. 泛型(Generics)

下面是类泛型的例子:

class ParameterizedClass<T>(private val value: T) {

    fun getValue(): T {
        return value
    }
}

fun main() {
    val parameterizedClass1 = ParameterizedClass<String>("string-value")
    val res1 = parameterizedClass1.getValue()
    println(res1 is String)            // true

    val parameterizedClass2 = ParameterizedClass<Int>(100)
    val res2 = parameterizedClass2.getValue()
    println(res2 is Int)               // true
}

下面是函数泛型的例子:

fun <T> middle(list: List<T>): T {
    return list[list.size / 2]
}

fun main() {
    val middleString: String = middle(listOf("ab", "cd", "ef"))
    println(middleString)

    val middleInteger: Int = middle(listOf(1, 2, 3, 4, 5))
    println(middleInteger)
}

当我们不关心泛型类型时,可以使用 * ,比如:

fun printArray(array: Array<*>) {
    array.forEach { println(it) }
}

fun main() {
    val a = arrayOf(1,2,3)
    printArray(a)

    val b = arrayOf("ab", "cd", "ef")
    printArray(b)
}
2.5.11.1. in, out

可以使用 in 声明类的泛型参数,它表示对象可以赋值给子类。比如:

class InClass<in T> {                  // in 表示可以赋值给 subtype
    fun toString(value: T): String {
        return value.toString()
    }
}

fun main() {
    val obj: InClass<Number> = InClass()
    val ref: InClass<Int> = obj        // Int 是 Number 的 subtype,没有 in 关键字这行会报错

    println(obj.toString(1.2))
    println(ref.toString(100))
}

可以使用 out 声明类的泛型参数,它表示对象可以赋值给超类。比如:

class OutClass<out T>(val value: T) {   // out 表示可以赋值给 supertype
    fun get(): T {
        return value
    }
}

fun main() {
    val obj = OutClass("ABC")
    val ref: OutClass<Any> = obj        // Any 是 String 的 supertype,没有 out 关键字这行会报错

    println(obj.value)   // ABC
    println(ref.value)   // ABC
}

2.5.12. Object declarations(单例模式)

单例模式是指一个类只有一个实例(对象)。在 Kotlin 中使用 Object declarations 来表达单例模式。

下面是 Object declaration 的例子:

object MySingleton {
    fun doSomething() {
        // ...
    }
}

fun main() {
    MySingleton.doSomething()
}
2.5.12.1. Companion objects

一个类可以有多个 Object declarations,我们可以把其中的一个标记为 companion (一个类最多有一个 Companion objects),这样在调用 Companion object 中的方法时可以直接省略 Companion object 名称了。

下面是 Companion object 的例子:

class Person(val name: String) {
    // A member function of the class
    fun sayHello() {
        println("Hello, my name is $name")
    }

    companion object MyFactory1 {                                 // 名称 MyFactory1 可以简略,即可以直接写为 companion object {
        // A property inside the companion object
        val defaultName = "DefaultName1"

        // A function inside the companion object
        fun createWithDefaultName(): Person {
            return Person(defaultName)
        }
    }

    object MyFactory2 {
        // A property inside the companion object
        val defaultName = "DefaultName2"

        // A function inside the companion object
        fun createWithDefaultName(): Person {
            return Person(defaultName)
        }
    }
}
fun main() {
    // Accessing a property and function from the companion object
    val instance1 = Person.createWithDefaultName()                // 类中companion object 只能有一个,调用时可以省略名称 MyFactory1
    val instance2 = Person.MyFactory2.createWithDefaultName()     // 类中普通的 Object declaration,必须使用 MyFactory2
    val instance3 = Person("Alice")
    instance1.sayHello()  // Output: Hello, my name is DefaultName1
    instance2.sayHello()  // Output: Hello, my name is DefaultName2
    instance3.sayHello()  // Output: Hello, my name is Alice
}

2.5.13. Object expressions(对类进行小修改但不声明子类)

有时我们想对类进行小修改,但懒得声明子类。这时可以使用 Object expressions

下面是 Object expression 的例子:

window.addMouseListener(object : MouseAdapter() {   // object 是关键字,这里表示 Object expression
    override fun mouseClicked(e: MouseEvent) {
        // ...
    }

    override fun mouseEntered(e: MouseEvent) {
        // ...
    }
})

上面例子相当于创建了 MouseAdapter 的子类,而且重载了方法 mouseClicked/mouseEntered ,再把这个子类的对象传递给 addMouseListener 函数。也可以写为:

val obj = object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {
        // ...
    }

    override fun mouseEntered(e: MouseEvent) {
        // ...
    }
}

window.addMouseListener(obj)

下面是 Object expression 的完整例子:

// From https://www.programiz.com/kotlin-programming/object-singleton

open class Person(name: String, age: Int) {
    init {
        println("name: $name, age: $age")
    }

    fun eat() = println("Eating food.")
    fun talk() = println("Talking with people.")
    open fun pray() = println("Praying god.")
}

fun main() {
    val atheist = object : Person("Jack", 29) {
        override fun pray() = println("I don't pray. I am an atheist.")
    }

    atheist.eat()
    atheist.talk()
    atheist.pray()
}

2.5.14. Type aliases

使用 typealias 可以给类型创建别名。比如:

typealias Predicate<T> = (T) -> Boolean           // Predicate<T> 是函数类型 (T) -> Boolean 的别名

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

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

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

2.6. 函数

Kotlin 中使用关键字 fun 来定义函数,参数定义使用风格 name: type ,比如:

fun sum(a: Int, b: Int): Int {
    return a + b
}

fun main() {
    sum(1, 2)                 // 调用函数
    sum(a = 1, b = 2)         // 调用函数时,可以指定参数名称,这时参数的顺序就不重要了
    sum(b = 2, a = 1)         // 调用函数时,可以指定参数名称,这时参数的顺序就不重要了
}

2.6.1. 默认参数

Kotlin 函数支持默认参数,比如:

fun foo(
    bar: Int,
    baz: Int = 200,           // 这是默认参数
    qux: Int = 300,           // 这也是默认参数。这里有个多余的逗号是没有关系的
) {
    println("bar is $bar")
    println("baz is $baz")
    println("qux is $qux")
}

fun main() {
    foo(10, 20, 30)           // 指定了所有参数
    foo(10, 20)               // 省略了一个默认参数
    foo(10)                   // 省略了两个默认参数
}

默认参数后面还可以有普通的参数,比如:

fun foo(
    bar: Int = 100,
    baz: Int,                 // 默认参数 bar 后面还有普通参数 baz
) {
    println("bar is $bar")
    println("baz is $baz")
}

fun main() {
    foo(10, 20)               // 指定了所有参数
    foo(baz = 200)            // 省略了默认参数,普通参数则必须指定名称
}

2.6.2. 变长参数(vararg)

变长参数使用 vararg 修饰,如:

fun <T> asList(vararg ts: T): List<T> {   // 参数 ts 是变长参数
    val result = ArrayList<T>()
    for (t in ts)  // ts is an Array
        result.add(t)
    return result
}

fun main() {
    val list1 = asList(1, 2, 3)
    println("$list1")                     // [1, 2, 3]

    val a = arrayOf(1, 2, 3)
    val list2 = asList(-1, 0, *a, 4)      // 数组前面的星号 * 是 spread operator,表示把内容展开作为变长参数的一部分
    println("$list2")                     // [-1, 0, 1, 2, 3, 4]
}

2.6.3. 返回 Unit

当函数不需要返回值时,可以声明返回类型是 Unit。这种类型的值,只有一个就是 Uint。如:

fun printHello(name: String?): Unit {
    if (name != null)
        println("Hello $name")
    else
        println("Hi there!")

    // `return Unit` or `return` is optional
}

当然,也可以直接不声明返回类型,上面代码等价于:

fun printHello(name: String?) {
    if (name != null)
        println("Hello $name")
    else
        println("Hi there!")
}

2.6.4. Single-expression 函数

如果函数体只有一个表达式,则可以省略函数体的大括号,把表达式直接写在等号后面。如:

fun double(x: Int): Int = x * 2        // Single-expression 函数
fun double(x: Int) = x * 2             // 同上。由于编译器可以自动推导出类型,所以返回类型也可以省略

2.6.5. Infix 函数

infix 关键字标记的函数也可以使用中缀表示法调用(即省略调用的点和圆括号)。Infix functions 必须满足以下要求:

  1. 必须是成员函数(member functions)或扩展函数(extension functions);
  2. 它们必须有一个参数;
  3. 形参不能接受可变数量的参数,也不能有默认值。

下面是中缀函数的例子:

infix fun Int.add(number: Int): Int {     // 使用 infix 声明中缀函数
    return this + number                  // 中缀函数总是有 receiver,关键字 this 表示 receiver
}

fun main() {
    val num = 10
    val newNum1 = num.add(5)
    println("$newNum1")

    val newNum2 = num add 5               // 使用中缀函数
    println("$newNum2")
}

2.6.6. Lambda 表达式

如果某个函数的参数是函数,或者返回值是函数;那么这个函数就是高阶函数(Higher-order functions)。

Lambda 表达式就是“函数字面量”。比如:

fun main() {
    val square = { number: Int -> number * number }     // Lambda 表达式

    val nine = square(3)
    println("$nine")
}
2.6.6.1. 隐式的单参数名(it)

如果 Lambda 表达式只有单个参数,则可以使用 it 进行简化。比如:

fun main() {
    val array = arrayOf(1,2,3)

    array.forEach { item -> println(item * 4) }
    array.forEach { println(it * 4) }             // 是上一行的简写形式
}

2.7. Null safety

Kotlin 中严格区分“可为 null”和“不可为 null”的类型。对于“可以为 null”的类型,在类型名后面有个问号 ? ,比如:

fun main() {
    var a: String = "abc"    // String 类型不能为 null
    // a = null              // 这行会报错,不能给 a 赋值为 null
    println(a)

    var b: String? = "abc"   // String? 类型可以为 null
    b = null                 // ok

    println(b)
}

不能直接在“可为 null”的类型上调用方法或属性,比如:

fun main() {
    var b: String? = "abc"        // String? 类型可以为 null
    // val len: Int = b.length    // 这是错误的,因为 b 是可为 null 类型,不能直接调用其方法或属性
    val len: Int? = b?.length     // 要使用 safe call ?. 调用其方法或属性,如果 b 为 null,则 b?.length 返回 null;如果 b 不为 null,则 b?.length 返回其长度
    println(len)
}

2.7.1. Automatic casts

编译器会进行 Automatic casts,以方便使用,下面是例子:

fun parseInt(str: String): Int? {
    return str.toIntOrNull()
}

fun printProduct(arg1: String, arg2: String) {
    val x = parseInt(arg1)
    val y = parseInt(arg2)

    if (x == null) {                    // if 中 x 为 null 会退出,编译器会把 if 外部后续代码中的 x 自动会变为 Int 类型,方便使用
        println("Wrong number format in arg1: '$arg1'")
        return
    }
    if (y == null) {                    // if 中 y 为 null 会退出,编译器会把 if 外部后续代码中的 y 自动会变为 Int 类型,方便使用
        println("Wrong number format in arg2: '$arg2'")
        return
    }

    // x and y are automatically cast to non-nullable after null check
    println(x * y)
}

fun main() {
    printProduct("6", "7")
    printProduct("a", "7")
    printProduct("99", "b")
}

上面的 printProduct 也可以为:

fun printProduct(arg1: String, arg2: String) {
    val x = parseInt(arg1)
    val y = parseInt(arg2)

    // Using `x * y` yields error because they may hold nulls.
    if (x != null && y != null) {       // 由于 if 中检查了 x,y 的值,在这个 if 分支内,x 和 y 会自动变为 Int 类型,方便使用
        // x and y are automatically cast to non-nullable after null check
        println(x * y)
    }
    else {
        println("'$arg1' or '$arg2' is not a number")
    }
}

3. Asynchronous programming (Coroutines)

各种编程语言中,有很多异步编程的方案,比如:

  1. Threading
  2. Callbacks
  3. Futures, promises, and others
  4. Reactive Extensions
  5. Coroutines

在 Kotlin 中采用的方案是 Coroutines,不过它没有直接在语言层面进行全面的支持,而通过库 kotlinx.coroutines 来实现大部分逻辑。

3.1. 第一个 Coroutine 程序

下面是一个简单的 Coroutine 程序(在运行下面程序前,你需要把 kotlinx-coroutines-core 加入到你项目的依赖中,具体可参考:https://github.com/Kotlin/kotlinx.coroutines/blob/master/README.md#using-in-your-projects ):

import kotlinx.coroutines.*

fun main() = runBlocking {    // this: CoroutineScope
    launch {                  // launch a new coroutine and continue
        delay(1000L)          // non-blocking delay for 1 second (default time unit is ms)
        println("World!")     // print after delay
    }
    println("Hello")          // main coroutine continues while a previous one is delayed
}

运行上面程序会得到下面输出:

Hello
World!

下面是对上面程序的一些简单说明:

  1. launch 是一个 Coroutine Builder,它用于启动一个新 Coroutine。
  2. delay 用于中止当前 Coroutine 运行,直到指定的时间到达;不过 delay 并不会阻塞底层的线程执行,底层线程可以执行其它的 Coroutine。
  3. runBlocking 也是一个 Coroutine Builder,不过它往往用于“连接”常规的非协程世界代码(如 main 函数)和协程世界代码(如 runBlocking 大括号内的代码),而且 阻塞当前线程直到大括号内的所有协程都结束, 所以上面的例子在输出 Hello 后,程序不会直接退出而是等待 World! 也输出后才退出。如果省略 runBlocking ,则会报编译错误,因为 launch 只能在协程世界代码中执行。

3.1.1. Structured concurrency

Kotlin 中的 Coroutine 遵守了 Structured concurrency 的原则,也就是说 新的 Coroutine 只能在特定的 CoroutineScope 中启动。 前一节的例子中,runBlocking 就是建立了这样的 Scope。

在现实的应用程序中,往往会启动大量的 Coroutines,使用 Structured concurrency 可以确保 Coroutines 不会丢失、不会泄漏。

3.2. 第一个 Suspending 函数

我们也可以把节 3.1launch 块中代码封装到一个独立函数 doWorld 中,即:

import kotlinx.coroutines.*

fun main() = runBlocking {    // this: CoroutineScope
    launch { doWorld() }      // launch 可启动新 Coroutine。如果去掉 launch,则会先输出 World! 再输出 Hello
    println("Hello")
}

// this is your first suspending function
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

为什么函数 doWorld 前面需要增加 suspend 关键字呢?这是因为它调用的 delay 是一个 suspend 函数。 我们要在 doWorld 函数中要调用另一个 suspend 函数(即这个例子中的 deplay )的话,它自己(即这个例子中的 doWorld )必须是 suspend 函数。

3.3. runBlocking VS. coroutineScope

除了使用 runBlocking 创建 Coroutine scope 外,还可以使用 coroutineScope 创建 Coroutine scope。

它们的相同点是都会等待它们的代码及其子 Coroutine 执行结束;而它们的主要区别是 runBlocking 会阻塞当前线程;而 coroutineScope 不会阻塞当前线程,当 Coroutine 暂停时会把线程资源给其它的 Coroutine。

下面是 coroutineScope 的例子:

import kotlinx.coroutines.*

fun main() = runBlocking {
    doWorld()
}

suspend fun doWorld() = coroutineScope {  // 如果这里省略 coroutineScope,会报错 Unresolved reference: launch. Suspension functions can be called only within coroutine body
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello")
}

下面是用多个 launch 启动多个 Coroutines 的例子:

import kotlinx.coroutines.*

// Sequentially executes doWorld followed by "Done"
fun main() = runBlocking {
    doWorld()
    println("Done")
}

// Concurrently executes both sections
suspend fun doWorld() = coroutineScope {  // this: CoroutineScope
    launch {                              // 启动 Coroutine
        delay(2000L)
        println("World 2")
    }
    launch {                              // 启动 Coroutine
        delay(1000L)
        println("World 1")
    }
    println("Hello")
}

上面代码的执行时间约 2 秒钟,且输出下面内容:

Hello
World 1
World 2
Done

3.4. 使用 async/await

除使用 launch 启动协程外,我们也可以使用 async 启动协程,它们的主要区别是:

  1. launch 函数的返回类型是 Job ,它代表一个可以取消的协程任务, 通常用于执行一些不需要返回结果的操作。
  2. async 函数的返回类型是 Deferred,它代表一个可以异步获取结果的任务,可以通过 await 获取最终结果, 通常用于执行需要返回结果的操作。

在错误处理方面,它们也不同:

  1. 在使用 launch 启动的任务中,需要在协程内部进行错误处理,否则可能会导致未捕获的异常。
  2. 在使用 async 启动的任务中,并且可以使用 try/catch 来捕获可能的异常。

下面是 async/await 的使用例子:

import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking {
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }    // async 启动协程
        val two = async { doSomethingUsefulTwo() }    // async 启动协程
        val result1 = one.await()                     // await 等待协程执行完成,获得结果
        val result2 = two.await()                     // await 等待协程执行完成,获得结果
        println("The answer is ${result1 + result2}")
    }
    println("Completed in $time ms")
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

运行上面代码,得到下面输出:

The answer is 42
Completed in 1031 ms

4. 参考

Author: cig01

Created: <2024-04-06 Sat>

Last updated: <2024-04-07 Sun>

Creator: Emacs 27.1 (Org mode 9.4)