函数和 Lambda 表达式
函数
用 fun
关键字声明函数,函数的参数必须有显式类型
默认参数
函数参数可以有默认值,当省略相应的参数时使用默认参数值
覆盖方法总是使用与基类类型方法相同的默认参数值,当覆盖一个带有默认参数值的方法时,必须从签名中省略默认参数值
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)
如果在默认参数之后的最后一个参数是 Lambda 表达式,那么它作为命名参数既可以在括号内传入,也可以在括号外传入
fun foo(bar: Int = 0, baz: Int = 1, qux: () -> Unit) {}
foo(1) { println("") }
foo(qux = { println("") })
foo { println("") }
变长参数
函数的参数(通常是最后一个)可以用 vararg
修饰符修饰
fun <T> asList(vararg ts: T): List<T> {
val result = ArrayList<T>()
for (t in ts) // ts 是一个数组
result.add(t)
return result
}
val a = arrayOf(1, 2, 3)
// 用 * 将数组展开
val list = asList(-1, 0, *a, 4)
返回值类型
如果一个函数不返回任何有用的值,那么它的返回类型是 Unit
,Unit 返回类型声明也是可选的
当函数返回单个表达式时,可以省略花括号并且在 =
之后指定代码体
fun trible(x: Int) = x * 3
当返回值类型可由编译器推断时,显式声明返回类型是可选的
中缀函数
标有 infix
关键字的函数也可以使用中缀表示法(忽略该函数的点与圆括号)调用。中缀函数必须满足以下要求
- 它们必须是成员函数或扩展函数
- 它们只能有一个参数
- 其参数不得接收可变数量的参数且不能有默认值
中缀函数的优先级低于算术操作符、类型转换及 rangeTo
操作符。
局部函数
一个函数在另一个函数内部,局部函数可以访问外部函数(即闭包)的局部变量
data class Vertex(val neighbors: List<Vertex>)
data class Graph(val vertices: List<Vertex>)
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])
}
尾递归函数
Kotlin 支持一种称为尾递归的函数式编程风格,允许将一些通常用循环写的算法改用递归函数来写,从而避免堆栈溢出的风险。
当一个函数用 tailrec
修饰符修饰并满足所需的形式时,编译器会优化该递归代码,留下一个快速而高效的基于循环的版本。
val eps = 1E-10
tailrec fun findFixPoint(x: Double = 1.0): Double =
if (Math.abs(x - Math.cos(x)) < eps)
x
else
findFixPoint(Math.cos(x))
内联函数
内联函数可以提高运行时效率,用 inline
修饰。inline
修饰符影响函数本身和传给它的 Lambda 表达式,所有这些都将内联到调用处
inline fun <T> lock(lock: Lock, body: () -> T): T {
// ...
}
扩展函数
对一个现有类扩展定义一个成员函数,不过该定义在类的外面。一般我们如果想对一个类封装一个 API 方法,但又不能直接修改该类时,就可以用到扩展函数
fun <类名>.<方法名>([参数1, 参数2, ...]): <返回类型> {
<方法体>
}
fun View.isVisible(): Boolean =
visibility == View.VISIBLE
fun View.setVisible(visible: Boolean) {
visibility = if (visible) View.VISIBLE else View.GONE
}
同样的,还可以扩展属性,但是这个属性并不会保存在对象里,定义是需要实现 get()
和 set()
函数
val <类名>.<属性名>: <类型>
get() {
<方法体>
}
var <类名>.<属性名>: <类型>
get() {
<方法体>
}
set(value) {
<方法体>
}
var View.isVisible
get() = visibility == View.VISIBLE
set(value) {
visibility = if (value) View.VISIBLE else View.GONE
}
Lambda 表达式
Kotlin 中的函数是一等公民,这意味着它们可以存储在变量与数据结构中、作为参数传递给其他高阶函数及从其他高阶函数返回,可以像操作任何其他非函数值一样操作函数
高阶函数是将函数用作参数或返回值的函数
fold
函数接收一个初始累积值与一个接合函数,并通过将当前累积值与每个集合元素连续接合起来代入累积值来构建返回值
为了调用 fold
,需要传给它一个函数类型的实例作为参数,而在高阶函数调用处,广泛使用 Lambda 表达式
val items = listOf(1, 2, 3, 4, 5)
items.fold(0, { acc: Int, i: Int ->
val result = acc + i
println("acc = $acc, i = $i, result = $result")
// Lambda 表达式中最后一个表达式是返回值
result
})
// Lambda 表达式的参数类型是可选的,如果能够将其推断出来的话,不需要声明
val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })
// 函数引用也可以用于高阶函数调用
val product = item.fold(1, Int::times)
Kotlin 使用类似 (Int) -> String
的一系列函数类型来处理函数的声明,这些类型具有与函数签名相对应的特殊表示法,即它们的参数和返回值
有几种方法可以获得函数类型的实例
- 使用函数字面值的代码块
- Lambda 表达式:
{ a, b -> a + b}
- 匿名函数:
fun (s: String): Int = s.toIntOrNull() ?: 0
- Lambda 表达式:
- 使用已有声明的可调用引用
- 顶层、局部、成员、扩展函数:
::isOdd
,String::toInt
- 顶层、成员、扩展属性:
List<Int>::size
- 构造函数:
::Regex
- 顶层、局部、成员、扩展函数:
函数类型的值可以通过其 invoke()
操作符调用,f.invoke(x)
或 f(x)
val stringPlus: (String, String) -> String = String::plus
val intPlus: Int.(Int) -> Int::plus
// <-->
println(stringPlus.invoke("<-", "->"))
println(intPlus(1, 2))
println(2.intPlus(3))
若一个 Lambda 表达式只有一个参数,编译器可以识别出签名,可以不声明唯一的参数并忽略 ->
,该参数会被隐式声明为 it
val list = listOf(1, 2, 3)
list.filter { it > 0 } // (it: Int) -> Boolean
若 Lambda 表达式的参数未使用,那么可以用下划线取代其名称
map.forEach { _, value -> println("$value") }