Kotlin 2.1.0 入门教程(八)
Lambda 表达式和匿名函数
Lambda 表达式和匿名函数是函数字面量。
函数字面量是未声明但立即作为表达式传递的函数。考虑以下示例:
max(strings, { a, b -> a.length < b.length })
函数 max
是一个高阶函数,因为它将函数值作为其第二个参数。这第二个参数是一个本身为函数的表达式,称为函数字面量,它等效于以下命名函数:
fun compare(a: String, b: String): Boolean = a.length < b.length
Lambda 表达式语法
完整语法形式如下:
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
-
Lambda 表达式总是用花括号括起来。
-
完整语法形式中的参数声明放在花括号内,并且可以选择性地包含类型注解。
-
主体放在
->
之后。 -
如果推断的 Lambda 返回类型不是
Unit
,则 Lambda 主体中的最后一个(或可能是唯一的)表达式被视为返回值。
如果省略所有可选的注解,剩下的内容如下所示:
val sum = { x: Int, y: Int -> x + y }
传递尾随 Lambda
如果函数的最后一个参数是函数,则可以将作为相应参数传递的 Lambda 表达式放在括号外:
val product = items.fold(1) { acc, e -> acc * e }
如果 Lambda 是该调用中的唯一参数,则可以完全省略括号:
run { println("...") }
it
单个参数的隐式名称
Lambda 表达式只有一个参数的情况非常常见。
如果编译器可以在没有任何参数的情况下解析签名,则无需声明参数,并且可以省略 ->
。
该参数将隐式声明为 it
。
ints.filter { it > 0 } // 这个字面量的类型是 (it: Int) -> Boolean
如果 Lambda 表达式只有一个参数,可以使用 it
隐式引用该参数。
fun main() {
val numbers = listOf(-1, 2, -3, 4)
val positives = numbers.filter { it > 0 }
}
从 Lambda 表达式返回值
可以使用限定的返回语法从 Lambda 中显式返回值。否则,最后一个表达式的值将被隐式返回。
因此,以下两个代码片段是等效的:
ints.filter {
val shouldFilter = it > 0
shouldFilter
}
ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}
这种约定,连同将 Lambda 表达式放在括号外传递,允许编写 LINQ 风格的代码:
strings.filter { it.length == 5 }.sortedBy { it }.map { it.uppercase() }
未使用变量的下划线
如果 Lambda 参数未使用,可以用下划线代替其名称:
map.forEach { (_, value) -> println("$value!") }
匿名函数
上面的 Lambda 表达式语法缺少一件事:指定函数返回类型的能力。
在大多数情况下,这是不必要的,因为返回类型可以自动推断。
但是,如果确实需要显式指定它,可以使用另一种语法:匿名函数。
fun (x: Int, y: Int): Int = x + y
匿名函数看起来非常像常规函数声明,只是省略了名称。其主体可以是表达式(如上所示)或代码块:
fun (x: Int, y: Int): Int {
return x + y
}
参数和返回类型的指定方式与常规函数相同,但如果可以从上下文中推断出参数类型,则可以省略参数类型:
ints.filter(fun (item) = item > 0)
匿名函数的返回类型推断与普通函数一样:对于具有表达式主体的匿名函数,返回类型会自动推断,但对于具有代码块主体的匿名函数,必须显式指定返回类型(或假定为 Unit
)。
当将匿名函数作为参数传递时,请将其放在括号内。允许将函数放在括号外的简写语法仅适用于 Lambda 表达式。
Lambda 表达式和匿名函数之间的另一个区别是非局部返回的行为。没有标签的 return
语句总是从使用 fun
关键字声明的函数返回。这意味着 Lambda 表达式中的 return
将从封闭函数返回,而匿名函数中的 return
将从匿名函数本身返回。
闭包
Lambda 表达式或匿名函数(以及局部函数和对象表达式)可以访问其闭包,闭包包括在外部作用域中声明的变量。在闭包中捕获的变量可以在 Lambda 中修改:
var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)
带有接收者的函数字面量
带有接收者的函数类型,例如 A.(B) -> C
,可以用一种特殊形式的函数字面量实例化:带有接收者的函数字面量。
如上所述,Kotlin 提供了在提供接收者对象的同时调用带有接收者的函数类型实例的能力。
在函数字面量的主体内,传递给调用的接收者对象成为隐式的 this
,因此您可以访问该接收者对象的成员而无需任何额外的限定符,或者使用 this
表达式访问接收者对象。
这种行为类似于扩展函数,扩展函数也允许您在函数体内访问接收者对象的成员。
以下是带有接收者的函数字面量及其类型的示例,其中在接收者对象上调用了 plus
:
val sum: Int.(Int) -> Int = { other -> plus(other) }
匿名函数语法允许您直接指定函数字面量的接收者类型。如果您需要声明一个带有接收者的函数类型变量,然后稍后使用它,这可能很有用。
val sum = fun Int.(other: Int): Int = this + other
当可以从上下文中推断出接收者类型时,Lambda 表达式可以用作带有接收者的函数字面量。它们最重要的用途之一是类型安全构建器:
class HTML {
fun body() { ... }
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML() // 创建接收者对象。
html.init() // 将接收者对象传递给 lambda。
return html
}
html { // 带有接收者的 lambda 从这里开始。
body() // 调用接收者对象的方法。
}
class HTML {
fun body() {
println("body")
}
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
fun main() {
html {
body() // body
this.body() // body
}
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
class HTML {
fun head(init: Head.() -> Unit) {
val head = Head()
head.init()
}
fun body(init: Body.() -> Unit) {
val body = Body()
body.init()
}
}
class Head {
fun title(text: String) {
println("<title>$text</title>")
}
}
class Body {
fun p(text: String) {
println("<p>$text</p>")
}
}
fun main() {
html {
head {
title("My Page")
}
body {
p("Hello, Kotlin!")
}
}
}