函数

函数声明

Kotlin 使用关键词 fun 声明一个函数:

  1. fun double(x: Int): Int {
  2. return 2 * x
  3. }

函数使用

普通的函数调用方式:

  1. val result = double(2)

使用点号调用成员函数:

  1. Sample().foo() // create instance of class Sample and call foo

参数

函数参数的定义使用帕斯卡符号——name: type。参数之间通过逗号分隔。每个参数都必须指明类型。

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

默认参数

函数参数可以有默认值,因此调用时可以省略对应的参数。与其他语言相比,这样可以减少函数重载。

  1. fun read(b: Array<Byte>, off: Int = 0, len: Int = b.size) {
  2. // ...
  3. }

默认值定义在类型右侧的 = 之后。

覆写的方法总是与父类方法使用相同的默认参数。所以覆写一个带默认参数值的方法时,函数签名中必须要去掉默认值:

  1. open class A {
  2. open fun foo(i: Int = 10) { /* ... */ }
  3. }
  4. class B : A {
  5. // no default value allowed
  6. override fun foo(i: Int) { /* ... */ }
  7. }

如果一个默认参数位于没有默认值的参数之前,默认值值能通过命名参数的形式使用。

  1. fun foo(bar: Int = 0, baz: Int) { /* ... */ }
  2. // The default value bar = 0 is used
  3. foo(baz = 1)

如果 lambda 在圆括号之外作为最后一个参数传递给函数,默认参数无需传值:

  1. fun foo(bar: Int = 0, baz: Int = 1, qux: () -> Unit) { /* ... */ }
  2. foo(1) { println("hello") } // Uses the default value baz = 1
  3. foo() { println("hello") } // Uses the default value bar = 0 and baz = 1

命名参数

函数调用时可以使用参数名。当函数有很多参数或者带默认参数时,这个特性很方便。

例如下面的函数:

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

可以用默认参数调用:

  1. reformat(str)

如果不使用参数默认值:

  1. reformat(str, true, true, false, '_')

如果使用命名参数,代码会变得更可读:

  1. reformat(str,
  2. normalizeCase = true,
  3. upperCaseFirstLetter = true,
  4. dividebyCamelHumps = false,
  5. wordSeparator = '_'
  6. )

如果不需要所有的参数:

  1. reformat(str, wordSeparator = '_')

如果同时使用位置参数和命名参数来调用函数,那么位置参数不能位于命名参数之后。例如,f(1, y = 2) 合法,f(x = 1, 2) 不合法。

可变数量参数(vararg)可以利用 spread 操作符通过命名的形式传递:

  1. fun foo(vararg strings: String) { /* ... */ }
  2. foo(strings = *arrayOf("a", "b", "c"))

注意,命名参数的语法无法用于 java 函数,因为 Java 字节码不一定保有参数名。

返回 Unit 的函数

如果函数的返回值无用,返回值类型可以为 UnitUnit 类型只有一个值 - Unit。无需显示返回这个值。

  1. fun printHello(name: String?): Unit {
  2. if (name != null)
  3. println("Hello ${name}")
  4. else
  5. println("Hi there!")
  6. // `return Unit` or `return` is optional
  7. }

Unit 的返回值可以不用声明。上面的代码等价于:

  1. fun printHello(name: String?) {
  2. // ...
  3. }

单表达式函数

如果函数只返回一个单表达式的值,那么可以省略花括号,函数体放在等号右侧。

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

如果编译器能够推导出返回值类型,无需显示声明。

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

显示的返回类型

有块体(block body)的函数必须显示指明返回值类型,除非返回 Unit。Kotlin 不会推导带块体的函数返回值类型,因为这类函数的控制逻辑可能比较复杂,返回值类型对(甚至是编译器)来说不明显。

可变参数(Varargs)

函数参数(通常是最后一个)可用 vararg 修饰:

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

可以传递任意数量的参数:

  1. val list = asList(1, 2, 3)

在函数内,类型为 Tvararg 参数被视作一个 T 的数组,也就是说,变量 ts 的类型是 Array<out T>

vararg 只能标记于一个参数,如果这个参数不是参数列表的最后一个,那么它之后的参数可以通过命名参数的语法传值,或者,如果参数是函数类型,可以在括号之外传递一个 lambda。

调用 vararg 函数时,可以一个一个传参,例如,asList<1, 2, 3>,或者,如果要传递一个数组的内容,可以用 spread 操作符(数组前面加个 *):

  1. val a = arrayOf(1, 2, 3)
  2. val list = asList(-1, 0, *a, 4)

中缀符号(infix notation)

infix 关键字标记的函数可以使用中缀符号调用(调用可省略点号和参数)。中缀函数需要满足如下条件:

  • 函数是成员函数或扩展函数;
  • 函数只有一个参数;
  • 参数不能接受可变数量的参数并且不能拥有默认值。
  1. infix fun Int.shl(x: Int): Int {
  2. // ...
  3. }
  4. // call extension function using infix notation
  5. 1 shl 2
  6. // is the same
  7. 1.shl(2)

中缀函数调用的优先级低于算术运算符、类型转换和 rangeTo 操作符。例如下面的等价表达式:

  • 1 shl 2 + 3 and 1 shl (2 + 3)
  • 0 until n 2 and 0 until (n 2)
  • xs union ys as Set<> and xs union (ys as Set<>)

另一方面,中缀函数调用的优先级要高于布尔运算 && 和 ||,is 和 in 检查,以及其他操作符。例如下面的等价表达式:

  • a && b xor c and a && (b xor c)
  • a xor b in c and (a xor b) in c

注意,使用中缀函数必须指明接收者和参数。如果在当前接收者上使用中缀符号,需要显示使用 this;与正常函数不同,它没法省略掉。这么做是为了保证无歧义解析。

  1. class MyStringCollection {
  2. infix fun add(s: String) { /* ... */ }
  3. fun build() {
  4. this add "abc" // Correct
  5. add("abc") // Correct
  6. add "abc" // Incorrect: the receiver must be specified
  7. }
  8. }

函数范围

Kotlin 的函数可以直接定义在代码文件的最外层,也就是说,无需像 Java、C# 或 Scala 那样必须把函数定义在类里面。除此之外,Kotlin 的函数可以声明为本地函数、成员函数和扩展函数。

局部函数

Kotlin 支持本地函数,也就是说,函数可以嵌套。

  1. fun dfs(graph: Graph) {
  2. fun dfs(current: Vertex, visited: Set<Vertex>) {
  3. if (!visisted.add(current)) return
  4. for (v in current.neighbors)
  5. dfs(v, visited)
  6. }
  7. dfs(graph.vertices[0], HashSet())
  8. }

局部函数可以访问外部函数(即:闭包)的局部变量,所以,上例中的 visited 可用局部变量表示:

  1. fun dfs(graph: Graph) {
  2. val visited = HashSet<Vertext>()
  3. fun dfs(current: Vertex) {
  4. if (!visited.add(current)) return
  5. for (v in current.neighbors)
  6. dfs(v)
  7. }
  8. }
  9. dfs(graph.vertices[0])
  10. }

成员函数

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

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

成员函数通过点符号来调用:

  1. Sample().foo() // creates instance of class Sample and calls foo

更多关于类和重写的姿势可参考类和继承

泛型函数

函数可以有泛型参数,用尖括号指定,位于函数名之前:

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

内联函数

可见内联函数章节。

扩展函数

可见扩展章节。

高阶函数和 lambda

可见高阶函数和lambda章节。

尾递归函数(Tail recursive functions)

Kotlin 支持函数式编程的尾递归,尾递归可以代替循环来实现某些算法,而且不会导致栈溢出。当一个函数用 tailrec 来修饰并且满足所需的形式时,编译器会把递归调用优化成一个快速且高效的循环。

  1. tailrec fun findFixPoint(x: Double = 1.0): Double
  2. = if (x == Math.cos(x)) x else findFixPoint(Math.cos(x))

上面的代码用于计算一个数学常量 —— cosine 的 fixpoint。实现方式是一直调用 Math.cos 直到计算结果不再改变,最后产生的值是 0.7390851332151607。以上代码的传统写法如下:

  1. private fun findFixPoint(): Double {
  2. var x = 1.0
  3. while (true) {
  4. val y = Math.cos(x)
  5. if (x == y) return x
  6. x = y
  7. }
  8. }

tailrec 修饰符要求函数的最后一个操作必须是调用自己。如果递归调用之后还有其他执行代码,或者是 try/catch/finally 的代码块,都不能使用尾递归。当前只有 JVM 的后端(backend)支持尾递归。