函数
Kotlin 函数使用 fun
关键字声明:
fun double(x: Int): Int {
return 2 * x
}
函数用法
函数使用标准方式调用:
val result = double(2)
调用成员函数使用点表示法:
Stream().read() // create instance of class Stream and call read()
形参
函数形参使用帕斯卡表示法定义 - 名称: 类型。形参之间用逗号分隔,每个形参都必须显式指定类型:
fun powerOf(number: Int, exponent: Int): Int { /*...*/ }
声明函数形参时,你可以使用尾部逗号:
fun powerOf(
number: Int,
exponent: Int, // trailing comma
) { /*...*/ }
带有默认值的形参
函数形参可以有默认值,当你省略对应的实参时会使用这些默认值。这减少了重载的数量:
fun read(
b: ByteArray,
off: Int = 0,
len: Int = b.size,
) { /*...*/ }
这类形参也称为 可选形参。
通过在类型后追加 =
来设置默认值。
覆盖方法总是使用基方法的默认形参值。当覆盖一个具有默认形参值的方法时,必须从签名中省略默认形参值:
open class A {
open fun foo(i: Int = 10) { /*...*/ }
}
class B : A() {
override fun foo(i: Int) { /*...*/ } // No default value is allowed.
}
如果一个带有默认值的形参出现在一个没有默认值的形参之前,那么该默认值只能通过具名实参调用函数来使用:
fun foo(
bar: Int = 0,
baz: Int,
) { /*...*/ }
foo(baz = 1) // The default value bar = 0 is used
如果所有带默认值的形参后的最后一个形参是函数类型,那么你可以将对应的 lambda 表达式实参作为具名实参传递,或者在圆括号外传递:
fun foo(
bar: Int = 0,
baz: Int = 1,
qux: () -> Unit,
) { /*...*/ }
foo(1) { println("hello") } // Uses the default value baz = 1
foo(qux = { println("hello") }) // Uses both default values bar = 0 and baz = 1
foo { println("hello") } // Uses both default values bar = 0 and baz = 1
具名实参
调用函数时,你可以为函数的一个或多个实参命名。当函数有许多实参并且难以将值与实参关联时,这会很有帮助,尤其当它是布尔值或 null
值时。
当你使用具名实参调用函数时,你可以自由更改它们的顺序。如果你想使用它们的默认值,你可以完全省略这些实参。
考虑 reformat()
函数,它有 4 个带默认值的实参。
fun reformat(
str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ',
) { /*...*/ }
调用此函数时,你不必命名所有实参:
reformat(
"String!",
false,
upperCaseFirstLetter = false,
divideByCamelHumps = true,
'_'
)
你可以跳过所有带有默认值的实参:
reformat("This is a long String!")
你也可以跳过特定的带有默认值的实参,而不是全部省略。但是,在第一个被跳过的实参之后,你必须命名所有后续实参:
reformat("This is a short String!", upperCaseFirstLetter = false, wordSeparator = '_')
你可以使用_展开操作符_(在数组前加上 *
)传递可变数量实参 (vararg
)并命名:
fun foo(vararg strings: String) { /*...*/ }
foo(strings = *arrayOf("a", "b", "c"))
在 JVM 上调用 Java 函数时,你不能使用具名实参语法,因为 Java 字节码并不总是保留函数形参的名称。
返回 Unit 的函数
如果函数不返回任何有用的值,其返回类型为 Unit
。Unit
是一种只有一个值 Unit
的类型。此值不必显式返回:
fun printHello(name: String?): Unit {
if (name != null)
println("Hello $name")
else
println("Hi there!")
// `return Unit` or `return` is optional
}
Unit
返回类型声明也是可选的。上面的代码等同于:
fun printHello(name: String?) { ... }
单表达式函数
当函数体由单个表达式组成时,可以省略花括号,并在 =
符号后指定函数体:
fun double(x: Int): Int = x * 2
当编译器可以推断出返回类型时,显式声明返回类型是可选的:
fun double(x: Int) = x * 2
显式返回类型
带有代码块体的函数必须始终显式指定返回类型,除非它们旨在返回 Unit
,在这种情况下指定返回类型是可选的。
Kotlin 不会推断带有代码块体的函数的返回类型,因为这类函数在函数体中可能具有复杂的控制流,返回类型对读者(有时甚至对编译器)来说并非显而易见。
可变数量实参 (vararg)
你可以用 vararg
修饰符标记函数的形参(通常是最后一个):
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
}
在这种情况下,你可以向函数传递可变数量的实参:
val list = asList(1, 2, 3)
在函数内部,T
类型的 vararg
形参表现为 T
的数组,如上例所示,其中 ts
变量的类型为 Array<out T>
。
只能有一个形参标记为 vararg
。如果 vararg
形参不是列表中的最后一个,则后续形参的值必须使用具名实参语法传递,或者,如果形参是函数类型,则通过在圆括号外传递 lambda 表达式。
当你调用 vararg
函数时,你可以单独传递实参,例如 asList(1, 2, 3)
。如果你已经有一个数组并想将其内容传递给函数,请使用展开操作符(在数组前加上 *
):
val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)
如果你想将原生类型数组传递到 vararg
中,你需要使用 toTypedArray()
函数将其转换为常规(类型化)数组:
val a = intArrayOf(1, 2, 3) // IntArray is a primitive type array
val list = asList(-1, 0, *a.toTypedArray(), 4)
中缀表示法
标记有 infix
关键字的函数也可以使用中缀表示法调用(省略点和调用圆括号)。中缀函数必须满足以下要求:
infix fun Int.shl(x: Int): Int { ... }
// calling the function using the infix notation
1 shl 2
// is the same as
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
。这是为了确保清晰的解析。
class MyStringCollection {
infix fun add(s: String) { /*...*/ }
fun build() {
this add "abc" // Correct
add("abc") // Correct
//add "abc" // Incorrect: the receiver must be specified
}
}
函数作用域
Kotlin 函数可以在文件的顶层声明,这意味着你不需要像 Java、C# 和 Scala 等语言那样创建类来包含函数(Scala 3 以后支持顶层定义)。除了顶层函数,Kotlin 函数还可以声明为局部函数、成员函数和扩展函数。
局部函数
Kotlin 支持局部函数,它们是函数内部的函数:
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
可以是局部变量:
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])
}
成员函数
成员函数是定义在类或对象内部的函数:
class Sample {
fun foo() { print("Foo") }
}
成员函数使用点表示法调用:
Sample().foo() // creates instance of class Sample and calls foo
泛型函数
函数可以有泛型形参,它们在函数名之前使用尖括号指定:
fun <T> singletonList(item: T): List<T> { /*...*/ }
有关泛型函数的更多信息,请参阅泛型。
尾递归函数
Kotlin 支持一种称为尾递归的函数式编程风格。对于某些通常使用循环的算法,你可以改用递归函数,而没有栈溢出的风险。当函数标记有 tailrec
修饰符并满足所需的正式条件时,编译器会优化掉递归,转而生成一个快速高效的基于循环的版本:
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))
此代码计算余弦函数的不动点,这是一个数学常数。它简单地从 1.0
开始重复调用 Math.cos
,直到结果不再变化,为指定的 eps
精度产生 0.7390851332151611
的结果。结果代码等同于这种更传统的风格:
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 支持尾递归。
另请参阅: