5.7.2 高阶函数与 lambda
8 分钟阅读
5.7.2 高阶函数与 lambda
Kotlin 函数是一等公民,也就是说它们可以存储在变量和数据结构中,也可以作为实参传给其他高阶函数,或由它们返回。凡是能对其他非函数值进行的操作,都能对函数进行。
为了实现这一点,作为静态类型编程语言的 Kotlin 使用一组函数类型来表示函数,并提供了一整套专门的语言结构,例如 lambda 表达式。
高阶函数
高阶函数是指接收函数作为参数,或返回函数的函数。
集合上的函数式编程惯用法 fold 就是高阶函数的一个好例子。它接收一个初始累加值和一个组合函数,通过将当前累加值与每个集合元素依次组合来构造返回值,并在每次组合后替换累加值:
| |
在上面的代码中,combine 参数的类型是函数类型 (R, T) -> R,因此它接收一个带有 R 和 T 两个参数、返回 R 类型值的函数。它在 for 循环中被调用,返回值随后赋给 accumulator。
要调用 fold,需要向它传入一个函数类型的实例作为实参;在高阶函数的调用处,lambda 表达式(详见下文)被广泛用于这一目的:
| |
函数类型
Kotlin 使用函数类型(例如 (Int) -> String)来声明与函数相关的内容:val onClick: () -> Unit = ...。
这些类型有一种与函数签名(即参数和返回值)相对应的特殊记法:
所有函数类型都有带圆括号的参数类型列表和返回类型:
(A, B) -> C表示接收A和B两个参数、返回C类型值的函数类型。参数类型列表可以为空,例如() -> A。Unit返回类型不能省略。函数类型还可以额外带有一个接收者类型,写在记法中的点号之前:类型
A.(B) -> C表示可以在接收者对象A上调用、接收参数B并返回C类型值的函数。带接收者的函数字面量常常与这类类型一起使用。挂起函数属于一类特殊的函数类型,其记法中带有 suspend 修饰符,例如
suspend () -> Unit或suspend A.(B) -> C。
函数类型记法中还可以选择性地包含函数参数名:(x: Int, y: Int) -> Point。这些名称可用于说明参数的含义。
要表示函数类型是可空的,请按如下方式使用圆括号:((Int, Int) -> Int)?。
函数类型也可以用圆括号组合起来:(Int) -> ((Int) -> Unit)。
注意: 箭头记法是右结合的,
(Int) -> (Int) -> Unit等价于上一个例子,但不等价于((Int) -> (Int)) -> Unit。
你也可以使用类型别名为函数类型指定替代名称:
| |
实例化函数类型
有几种方式可以获得函数类型的实例:
- 在函数字面量中使用代码块,形式如下之一:
- lambda 表达式:
{ a, b -> a + b }, - 匿名函数:
fun(s: String): Int { return s.toIntOrNull() ?: 0 }
带接收者的函数字面量可以用作带接收者的函数类型的值。
其中还包括指向某个特定实例成员的绑定可调用引用:foo::toString。
- 使用把函数类型作为接口实现的自定义类的实例:
| |
如果有足够的信息,编译器可以为变量推断函数类型:
| |
带接收者和不带接收者的函数类型,其非字面量值可以互换使用,因此接收者可以充当第一个参数,反之亦然。例如,类型为 (A, B) -> C 的值可以在期望 A.(B) -> C 类型值的地方传入或赋值,反过来也一样:
| |
注意: 默认情况下会推断为不带接收者的函数类型,即使变量是用某个扩展函数的引用初始化的。要改变这一点,请显式指定变量类型。
调用函数类型的实例
函数类型的值可以使用它的 invoke(...) 运算符来调用:f.invoke(x),或者直接写成 f(x)。
如果该值带接收者类型,则应把接收者对象作为第一个参数传入。调用带接收者的函数类型值的另一种方式,是把接收者对象写在它前面,就像该值是一个扩展函数一样:1.foo(2)。
示例:
| |
内联函数
对于高阶函数,有时使用能提供灵活控制流的内联函数会更有好处。
Lambda 表达式与匿名函数
Lambda 表达式和匿名函数都是函数字面量。函数字面量是没有被声明、而是作为表达式立即传递的函数。请看下面的例子:
| |
函数 max 是高阶函数,因为它把函数值作为第二个参数。第二个参数本身就是一个函数表达式,称为函数字面量,它等价于下面这个具名函数:
| |
你也可以使用 suspend 关键字创建挂起 lambda 表达式。挂起 lambda 的函数类型是 suspend () -> Unit,并且可以调用其他挂起函数:
| |
Lambda 表达式语法
Lambda 表达式的完整语法形式如下:
| |
- lambda 表达式始终用花括号括起来。
- 完整语法形式中的参数声明写在花括号内,类型标注是可选的。
- 函数体写在
->之后。 - 如果推断出的 lambda 返回类型不是
Unit,则 lambda 体中的最后一个(也可能是唯一的)表达式会被当作返回值。
如果把所有可选的标注都去掉,剩下的形式是这样的:
| |
传递尾随 lambda
按照 Kotlin 约定,如果函数的最后一个参数是函数,那么作为对应实参传入的 lambda 表达式可以放在圆括号之外:
| |
这种语法也称为尾随 lambda。
如果 lambda 是该调用中唯一的实参,圆括号可以完全省略:
| |
it:单个参数的隐式名称
lambda 表达式只有一个参数的情况非常常见。
如果编译器能够在不带任何参数的情况下解析签名,就不需要声明该参数,-> 也可以省略。此时参数会被隐式声明为 it:
| |
从 lambda 表达式返回值
你可以使用带限定符的 return语法从 lambda 中显式返回值。否则,最后一个表达式的值会被隐式返回。
因此,下面两段代码是等价的:
| |
这一约定再加上把 lambda 表达式放在圆括号之外的写法,使得 LINQ 风格的代码成为可能:
| |
用下划线表示未使用的变量
如果 lambda 参数未被使用,可以用下划线代替它的名字:
| |
lambda 中的解构
lambda 中的解构作为解构声明的一部分来介绍。
匿名函数
上面的 lambda 表达式语法缺少一样东西——无法指定函数的返回类型。大多数情况下并不需要这样做,因为返回类型可以自动推断。但如果你确实需要显式指定,可以使用另一种语法:匿名函数。
| |
匿名函数看起来与普通函数声明非常相似,只是省略了名称。它的函数体可以是表达式(如上所示),也可以是代码块:
| |
参数和返回类型的指定方式与普通函数相同,只是如果能从上下文推断出参数类型,就可以省略它们:
| |
匿名函数的返回类型推断与普通函数完全一样:带表达式函数体的匿名函数会自动推断返回类型,而带块体的匿名函数则必须显式指定返回类型(否则会被假定为 Unit)。
注意: 当把匿名函数作为参数传递时,请把它们放在圆括号内。允许把函数放在圆括号之外的简写语法只适用于 lambda 表达式。
lambda 表达式与匿名函数的另一个区别在于非局部返回的行为。不带标签的 return 语句总是从用 fun 关键字声明的函数返回。这意味着 lambda 表达式中的 return 会从外层函数返回,而匿名函数中的 return 则从匿名函数本身返回。
闭包
lambda 表达式或匿名函数(以及局部函数和对象表达式)可以访问其闭包,其中包含在外层作用域中声明的变量。闭包中捕获的变量可以在 lambda 中被修改:
| |
带接收者的函数字面量
带接收者的函数类型,例如 A.(B) -> C,可以用一种特殊形式的函数字面量——带接收者的函数字面量——来实例化。
如前所述,Kotlin 支持在提供接收者对象的同时调用带接收者的函数类型实例。
在函数字面量内部,调用时传入的接收者对象会成为隐式的 this,这样你无需任何额外限定符就能访问该接收者对象的成员,也可以用 this 表达式访问该接收者对象。
这种行为与扩展函数类似,后者同样允许你在函数体内访问接收者对象的成员。
下面是一个带接收者的函数字面量示例及其类型,其中 plus 是在接收者对象上调用的:
| |
匿名函数语法允许你直接指定函数字面量的接收者类型。当你先声明一个带接收者的函数类型变量、之后再使用它时,这会很有用。
| |
当接收者类型可以从上下文推断出来时,lambda 表达式可以用作带接收者的函数字面量。它们最重要的用例之一是类型安全的构建器:
| |