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
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 的,就不能使用尾递归。
另请参阅: