函数
Kotlin 函数使用 fun
关键字声明:
fun double(x: Int): Int {
return 2 * x
}
函数用法
函数通过标准方式调用:
val result = double(2)
调用成员函数使用点表示法:
Stream().read() // 创建 Stream 类的实例并调用 read()
参数
函数参数使用 Pascal 表示法定义 - 名称: 类型。参数之间用逗号分隔,每个参数都必须显式指定类型:
fun powerOf(number: Int, exponent: Int): Int { /*...*/ }
声明函数参数时,可以使用尾随逗号:
fun powerOf(
number: Int,
exponent: Int, // 尾随逗号
) { /*...*/ }
默认参数
函数参数可以拥有默认值,当您省略相应参数时将使用这些默认值。这减少了重载的数量:
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) { /*...*/ } // 不允许有默认值。
}
如果默认参数位于没有默认值的参数之前,则只能通过使用命名参数调用函数来使用默认值:
fun foo(
bar: Int = 0,
baz: Int,
) { /*...*/ }
foo(baz = 1) // 使用了默认值 bar = 0
如果默认参数后的最后一个参数是lambda 表达式,您可以将其作为命名参数传递,或者放在圆括号外传递:
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
命名参数
调用函数时,您可以命名函数的一个或多个参数。当函数有许多参数,并且难以将值与参数关联起来时,这会很有帮助,尤其是在参数是布尔值或 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"))
NOTE
在 JVM 上调用 Java 函数时,您不能使用命名参数语法,因为 Java 字节码并不总是保留函数参数的名称。
Unit 返回函数
如果一个函数不返回有用的值,其返回类型为 Unit
。Unit
是一种只有一个值——Unit
——的类型。此值不必显式返回:
fun printHello(name: String?): Unit {
if (name != null)
println("Hello $name")
else
println("Hi there!")
// `return Unit` 或 `return` 是可选的
}
Unit
返回类型声明也是可选的。上述代码等同于:
fun printHello(name: String?) { ... }
单表达式函数
当函数体由单个表达式组成时,可以省略花括号,并在 =
符号后指定函数体:
fun double(x: Int): Int = x * 2
当编译器可以推断出返回类型时,显式声明返回类型是可选的:
fun double(x: Int) = x * 2
显式返回类型
具有块体(block body)的函数必须始终显式指定返回类型,除非它们旨在返回 Unit
,在这种情况下指定返回类型是可选的。
Kotlin 不会推断具有块体函数的返回类型,因为这类函数在函数体内可能包含复杂的控制流,并且返回类型对读者(有时甚至是编译器)来说并不明显。
可变数量参数 (varargs)
您可以将函数的一个参数(通常是最后一个)标记为 vararg
修饰符:
fun <T> asList(vararg ts: T): List<T> {
val result = ArrayList<T>()
for (t in ts) // ts 是一个 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 是一个基本类型数组
val list = asList(-1, 0, *a.toTypedArray(), 4)
中缀表示法
用 infix
关键字标记的函数也可以使用中缀表示法(省略点和调用时的括号)进行调用。中缀函数必须满足以下要求:
infix fun Int.shl(x: Int): Int { ... }
// 使用中缀表示法调用函数
1 shl 2
// 与以下代码相同
1.shl(2)
NOTE
中缀函数调用的优先级低于算术运算符、类型转换和 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" // 正确
add("abc") // 正确
//add "abc" // 不正确:必须指定接收者
}
}
函数作用域
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() // 创建 Sample 类的实例并调用 foo
泛型函数
函数可以拥有泛型参数,这些参数在函数名称前使用尖括号指定:
fun <T> singletonList(item: T): List<T> { /*...*/ }
有关泛型函数的更多信息,请参阅泛型。
尾递归函数
Kotlin 支持一种称为尾递归的函数式编程风格。对于一些通常使用循环的算法,您可以改用递归函数,而没有栈溢出的风险。当函数被 tailrec
修饰符标记并满足所需的形式条件时,编译器会优化掉递归,转而生成一个快速高效的基于循环的版本:
val eps = 1E-10 // “足够好”,可以是 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 // “足够好”,可以是 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 在 JVM 和 Kotlin/Native 上支持尾递归。
另请参阅: