5.7.1 函数

原文链接: https://kotlinlang.org/docs/functions.html

5.7.1 函数

在 Kotlin 中声明函数时:

  • 使用 fun 关键字。
  • 在圆括号 () 中指定参数。
  • 如有需要,包含返回类型。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 'double' 是函数名
// 'x' 是一个 Int 类型的参数
// 期望的返回值也是 Int 类型
fun double(x: Int): Int {
    return 2 * x
}

fun main() {
    println(double(5))
    // 10
}

函数的使用

函数的调用方式与其他语言一致:

1
val result = double(2)

要调用成员函数或扩展函数,使用点号 .:

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

参数

函数参数使用 Pascal 记法声明:name: Type。必须用逗号分隔参数,并显式地为每个参数指定类型:

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

当你把对象传给函数时,编译器传递的是该对象引用的副本。这个副本引用指向同一个对象,因此函数可以修改该对象的可变状态。

函数参数在函数体内是只读的(隐式声明为 val),所以你不能再给它们重新赋值:

1
2
3
4
5
6
class Counter(var value: Int)

fun reset(counter: Counter) {
    counter.value = 0 // 允许:修改对象
    counter = Counter(0) // 错误:'val' 不能被重新赋值
}

声明函数参数时可以使用尾随逗号:

1
2
3
4
fun powerOf(
    number: Int,
    exponent: Int, // 尾随逗号
) { /*...*/ }

尾随逗号有助于重构和维护代码:你可以在声明中移动参数,而不必担心哪个参数是最后一个。

注意: Kotlin 函数可以接收其他函数作为参数,也可以作为实参传递。更多信息请参阅 。

带默认值的参数

通过为参数指定默认值,可以把它变成可选参数。当调用函数时没有提供与该参数对应的实参时,Kotlin 就会使用默认值。带默认值的参数也称为可选参数。

可选参数减少了对多个重载的需求,因为你不必仅仅为了跳过某个有合理默认值的参数,就声明函数的不同版本。

在参数声明后追加 = 即可设置默认值:

1
2
3
4
5
6
7
8
fun read(
    b: ByteArray,
    // 'off' 的默认值是 0
    off: Int = 0,
    // 'len' 的默认值根据
    // 'b' 数组的大小计算得出
    len: Int = b.size,
) { /*...*/ }

当你在没有默认值的参数之前声明带默认值的参数时,只能通过命名实参来使用该默认值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fun greeting(
    userId: Int = 0,
    message: String,
) { /*...*/ }

fun main() {
    // 把 0 用作 'userId' 的默认值
    greeting(message = "Hello!")

    // 错误:没有为参数 'message' 传值
    greeting("Hello!")
}

尾随 lambda 是这条规则的例外,因为最后一个参数必须与传入的函数对应:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fun main () {
fun greeting(
    userId: Int = 0,
    message: () -> Unit,
)
{ println(userId)
  message() }

// 使用 'userId' 的默认值
greeting() { println ("Hello!") }
// 0
// Hello!
}

重写方法总是使用基方法的默认参数值。当你重写带有默认参数值的方法时,必须从签名中省略默认参数值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
open class Shape {
    open fun draw(width: Int = 10, height: Int = 5) { /*...*/ }
}

class Rectangle : Shape() {
    // 这里不允许指定默认值,
    // 但这个函数默认也会对 'width' 使用 10,
    // 对 'height' 使用 5。
    override fun draw(width: Int, height: Int) { /*...*/ }
}

非恒定表达式作为默认值

你可以给参数指定非常量的默认值。例如,默认值可以是函数调用的结果,也可以是使用其他实参值的计算结果,如下例中的 len 参数:

1
2
3
4
5
fun read(
    b: ByteArray,
    off: Int = 0,
    len: Int = b.size,
) { /*...*/ }

引用其他参数值的参数必须在顺序上声明得更靠后。在这个例子中,len 必须声明在 b 之后。

一般来说,你可以把任何表达式指定为参数的默认值。不过,只有当调用函数时没有传入对应参数、因而需要使用默认值时,默认值才会被求值。例如,下面这个函数只有在调用时没有传入 print 参数的情况下才会打印一行内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fun main() {
    fun read(
        b: Int,
        print: Unit? = println("No argument passed for 'print'")
    ) { println(b) }

    // 打印 "No argument passed for 'print'",然后打印 "1"
    read(1)
    // 只打印 "1"
    read(1, null)
}

如果函数声明中的最后一个参数是函数类型,那么对应的 lambda 实参既可以作为命名实参传入,也可以放在圆括号之外传入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
fun main() {
    fun log(
        level: Int = 0,
        code:  Int = 1,
        action: () -> Unit,
    ) { println (level)
        println (code)
        action() }

    // 为 'level' 传入 1,并为 'code' 使用默认值 1
    log(1) { println("Connection established") }

    // 两个参数都使用默认值:'level' 为 0,'code' 为 1
    log(action = { println("Connection established") })

    // 与上一次调用等价,两个参数都使用默认值
    log { println("Connection established") }
}

命名参数

调用函数时,你可以为一个或多个实参命名。当函数调用有很多实参时,这会很有帮助。在这类情况下,很难把某个值与某个参数对应起来,尤其是当它是 null 或布尔值时。

在函数调用中使用命名实参时,你可以按任意顺序列出它们。

来看 reformat() 函数,它有 4 个带默认值的参数:

1
2
3
4
5
6
7
fun reformat(
    str: String,
    normalizeCase: Boolean = true,
    upperCaseFirstLetter: Boolean = true,
    divideByCamelHumps: Boolean = false,
    wordSeparator: Char = ' ',
) { /*...*/ }

调用这个函数时,你可以为其中一些实参命名:

1
2
3
4
5
6
7
reformat(
    "String!",
    normalizeCase = false,
    upperCaseFirstLetter = false,
    divideByCamelHumps = true,
    '_'
)

你可以跳过所有带默认值的实参:

1
reformat("This is a long String!")

你也可以只跳过部分带默认值的实参,而不是全部省略。不过,从第一个被跳过的实参开始,之后的所有实参都必须命名:

1
2
3
4
5
reformat(
    "This is a short String!",
    upperCaseFirstLetter = false,
    wordSeparator = '_'
)

你可以通过为对应参数命名来传递可变数量的实参(vararg)。在这个例子中,它是一个数组:

1
2
3
fun mergeStrings(vararg strings: String) { /*...*/ }

mergeStrings(strings = arrayOf("a", "b", "c"))

注意: 在 JVM 上调用 Java 函数时,你不能使用命名实参语法,因为 Java 字节码并不总会保留函数参数名。

返回类型

当你声明带有块体(把语句放在花括号 {} 中)的函数时,必须始终显式指定返回类型。唯一的例外是函数返回 Unit,此时可以省略返回类型。

Kotlin 不会为带有块体的函数推断返回类型。它们的控制流可能很复杂,这会让阅读者甚至编译器都难以确定返回类型。不过,如果你不指定返回类型,Kotlin 可以为单表达式函数推断返回类型。

Kotlin 函数只返回一个值,但这个值可以包含多项数据。关于表示这些值的方式,请参阅返回多个值。

返回多个值

当你需要返回多个彼此相关但含义不同的值时,请声明一个数据类,即使它只被一个函数使用也值得这样做:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
data class OrderSummary(
    val subtotal: Double,
    val tax: Double,
)

fun calculateOrderSummary(prices: List<Double>): OrderSummary {
    val subtotal = prices.sum()
    val tax = subtotal * 0.2
    return OrderSummary(subtotal, tax)
}

fun main() {
    val summary = calculateOrderSummary(listOf(12.50, 8.00, 4.50))

    println(summary.subtotal)
    // 25.0
    println(summary.tax)
    // 5.0
}

当需要返回多个含义不同的值时,数据类很合适。如果返回的值属于同一类事物、并且你想把它们作为一个整体来处理,那么可以考虑返回集合:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
data class Person(val name: String)

val friendGroups = listOf(
    listOf(Person("Alice"), Person("Bob")),
    listOf(Person("Charlie"), Person("Diana"), Person("Eve")),
    listOf(Person("Frank"))
)

fun findLargestGroupOfFriends(): List<Person> {
    return friendGroups.maxByOrNull { it.size } ?: emptyList()
}

fun main() {
    val largestGroup = findLargestGroupOfFriends()

    println(largestGroup.map { it.name })
    // [Charlie, Diana, Eve]
}

如果需要返回固定数量的值,你也可以使用标准库中的 Pair 或 Triple 数据类。但它们的属性名是 first、second、third 这类通用名称,会让结果难以理解。

例如,虽然 calculateOrderTotals() 函数返回一个 Pair,但每个 Double 表示什么并不清楚:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fun calculateOrderTotals(prices: List<Double>): Pair<Double, Double> {
    val subtotal = prices.sum()
    val tax = subtotal * 0.2
    return Pair(subtotal, tax)
}

fun main() {
    val totals = calculateOrderTotals(listOf(12.50, 8.00, 4.50))

    // 'first' 是什么意思?
    println(totals.first)
    // 25.0

    // 'second' 是什么意思?
    println(totals.second)
    // 5.0
}

对于含义不同的结果,请优先使用带描述性属性名的数据类,如 OrderSummary 数据类示例中所演示的那样。

单表达式函数

当函数体只包含单个表达式时,可以省略花括号,并在 = 符号之后指定函数体:

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

大多数情况下你不必显式声明返回类型:

1
2
// 编译器推断出该函数返回 Int
fun double(x: Int) = x * 2

从单个表达式推断返回类型时,编译器有时会遇到问题。这时应当显式添加返回类型。例如,递归函数或相互递归(彼此调用)的函数,以及像 fun empty() = null 这样表达式没有类型的函数,都必须要有返回类型。

当你使用推断出的返回类型时,一定要检查实际结果,因为编译器推断出的类型可能对你并不好用。在上面的例子中,如果你希望 double() 函数返回 Number 而不是 Int,就必须显式声明。

如果在表达式函数体中使用了 return 语句,就必须显式指定返回类型:

1
2
fun getDisplayNameOrDefault(userId: String?): String =
    getDisplayName(userId ?: return "default")

返回 Unit 的函数

如果函数带有块体(花括号 {} 中的语句)并且不返回有用的值,编译器会认为它的返回类型是 Unit。Unit 是一种只有一个值的类型,该值也称为 Unit。

除函数类型参数外,你不必把 Unit 指定为返回类型。你也永远不必显式返回 Unit。

例如,你可以声明一个不返回 Unit 的 printHello() 函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 函数类型参数('action')的声明
// 仍然需要显式的返回类型
fun printHello(name: String?, action: () -> Unit) {
    if (name != null)
        println("Hello $name")
    else
        println("Hi there!")

    action()
}

fun main() {
    printHello("Kodee") {
        println("This runs after the greeting.")
    }
    // Hello Kodee
    // 这在问候语之后运行。

    printHello(null) {
        println("No name provided, but action still runs.")
    }
    // 没有提供名字,但 action 仍会运行
}

它等价于下面这种更冗长的声明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
fun printHello(name: String?, action: () -> Unit): Unit {
    if (name != null)
        println("Hello $name")
    else
        println("Hi there!")

    action()
    return Unit
}
fun main() {
    printHello("Kodee") {
        println("This runs after the greeting.")
    }
    // Hello Kodee
    // 这在问候语之后运行。

    printHello(null) {
        println("No name provided, but action still runs.")
    }
    // 没有提供名字,但 action 仍会运行
}

可变数量参数(varargs)

要给函数传递可变数量的实参,可以用 vararg 修饰符标记其中一个参数(通常是最后一个)。在函数内部,你可以把类型为 T 的 vararg 参数当作 T 的数组使用:

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

然后就可以向该函数传递可变数量的实参:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fun <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for (t in ts) // ts 是数组
        result.add(t)
    return result
}

fun main() {
    val list = asList(1, 2, 3)
    println(list)
    // [1, 2, 3]
}

只有一个参数可以标记为 vararg。如果你把 vararg 参数声明在参数列表的非末尾位置,就必须使用命名实参为后续参数传值。如果某个参数是函数类型,你也可以把它的值作为 lambda 放在圆括号之外传入。

调用 vararg 函数时,你可以像 asList(1, 2, 3) 的例子那样逐个传入实参。如果你已经有一个数组,想把它作为 vararg 参数(或其中一部分)传给函数,请使用展开运算符,在数组名前加上 *:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fun <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for (t in ts)
        result.add(t)
    return result
}

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

    // 函数收到数组 [-1, 0, 1, 2, 3, 4]
    list = asList(-1, 0, *a, 4)

    println(list)
    // [-1, 0, 1, 2, 3, 4]
}

如果你想传递基本类型数组作为 vararg,需要用 .toTypedArray() 函数把它转换为普通(有类型)的数组:

1
2
3
// 'a' 是 IntArray,属于基本类型数组
val a = intArrayOf(1, 2, 3)
val list = asList(-1, 0, *a.toTypedArray(), 4)

中缀表示法

使用 infix 关键字,你可以声明无需圆括号和点号即可调用的函数。这有助于让代码中简单的函数调用更易读。

1
2
3
4
5
6
7
infix fun Int.shl(x: Int): Int { /*...*/ }

// 使用一般记法调用函数
1.shl(2)

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

中缀函数必须满足以下要求:

注意: 中缀函数调用的优先级低于算术运算符、类型转换和 rangeTo 运算符。以下表达式是等价的:* 1 shl 2 + 3 等价于 1 shl (2 + 3) * 0 until n * 2 等价于 0 until (n * 2) * xs union ys as Set<*> 等价于 xs union (ys as Set<*>) 另一方面,中缀函数调用的优先级高于布尔运算符 && 和 ||、is 与 in 检查,以及某些其他运算符。因此下面这些表达式也是等价的:* a && b xor c 等价于 a && (b xor c) * a xor b in c 等价于 (a xor b) in c

注意,中缀函数总是要求同时指定接收者和参数。用中缀表示法调用当前接收者上的方法时,请显式使用 this。这样可以保证解析无歧义。

 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
class MyStringCollection {
    val items = mutableListOf<String>()

    infix fun add(s: String) {
        println("Adding: $s")
        items += s
    }

    fun build() {
        add("first") // 正确:普通函数调用
        this add "second" // 正确:带显式接收者的中缀调用
        // add "third"    // 编译错误:需要显式接收者
    }

    fun printAll() = println("Items = $items")
}

fun main() {
    val myStrings = MyStringCollection()
    // 把 "first" 和 "second" 添加到列表中
    myStrings.build()

    myStrings.printAll()
    // Adding: first
    // Adding: second
    // Items = [first, second]
}

函数作用域

你可以在文件的顶层声明 Kotlin 函数,也就是说不需要为了放置函数而创建类。函数也可以作为成员函数或扩展函数声明。

局部函数

Kotlin 支持局部函数,即在其他函数内部声明的函数。例如,下面的代码为给定图实现了深度优先搜索算法。外层 dfs() 函数内部的局部 dfs() 函数用于隐藏实现并处理递归调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person(val name: String) {
    val friends = mutableListOf<Person>()
}
class SocialGraph(val people: List<Person>)
fun dfs(graph: SocialGraph) {
    fun dfs(current: Person, visited: MutableSet<Person>) {
        if (!visited.add(current)) return
        println("Visited ${current.name}")
        for (friend in current.friends)
            dfs(friend, visited)
    }
    dfs(graph.people[0], HashSet())
}
fun main() {
    val alice = Person("Alice")
    val bob = Person("Bob")
    val charlie = Person("Charlie")
    alice.friends += bob
    bob.friends += charlie
    charlie.friends += alice
    val network = SocialGraph(listOf(alice, bob, charlie))
    dfs(network)
}

局部函数可以访问外层函数的局部变量(闭包)。在上述情况中,visited 这个函数参数可以改为局部变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person(val name: String) {
    val friends = mutableListOf<Person>()
}
class SocialGraph(val people: List<Person>)
fun dfs(graph: SocialGraph) {
    val visited = HashSet<Person>()
    fun dfs(current: Person) {
        if (!visited.add(current)) return
        println("Visited ${current.name}")
        for (friend in current.friends)
            dfs(friend)
    }
    dfs(graph.people[0])
}
fun main() {
    val alice = Person("Alice")
    val bob = Person("Bob")
    val charlie = Person("Charlie")
    alice.friends += bob
    bob.friends += charlie
    charlie.friends += alice
    val network = SocialGraph(listOf(alice, bob, charlie))
    dfs(network)
}

成员函数

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

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

要调用成员函数,写出实例或对象名,然后加上 . 和函数名:

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

关于类和重写成员的更多信息,请参阅类和继承。

泛型函数

你可以在函数名之前使用尖括号 <> 为函数指定泛型参数:

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

关于泛型函数的更多信息,请参阅泛型。

尾递归函数

Kotlin 支持一种称为尾递归的函数式编程风格。对于一些通常使用循环的算法,你可以改用递归函数而无需担心栈溢出。当函数用 tailrec 修饰符标记并满足所需的形式条件时,编译器会优化掉递归,留下一个快速高效的基于循环的版本:

1
2
3
4
5
6
7
8
import kotlin.math.cos
import kotlin.math.abs

// 一个随意取定的"足够好"的精度
val eps = 1E-10

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

这段代码计算余弦的不动点(一个数学常量)。函数从 1.0 开始反复调用 cos(),直到结果不再变化,在指定的 eps 精度下得到结果 0.7390851332151611。这段代码等价于下面这种更传统的写法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import kotlin.math.cos
import kotlin.math.abs

// 一个随意取定的"足够好"的精度
val eps = 1E-10

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

只有当函数把自身调用作为最后一个操作时,才能对它应用 tailrec 修饰符。如果递归调用之后还有其他代码、递归调用位于 try/catch/finally 块中,或者函数是 open 的,就不能使用尾递归。

另请参阅: