高阶函数和 lambda 表达式
Kotlin 函数是一等公民 ,这意味着它们可以存储在变量和数据结构中,并且可以作为参数传递给其他高阶函数 ,也可以从其他高阶函数中返回。 你可以对函数执行任何操作,就像对其他非函数值进行的操作一样。
为了方便这一点,Kotlin 作为一种静态类型的编程语言,使用一系列函数类型来表示函数,并提供一套专门的语言构造,比如lambda 表达式。
高阶函数
高阶函数是一种接受函数作为参数或返回函数的函数。
一个非常好的高阶函数例子是集合操作中的函数式编程风格的fold
。 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)
。
你还可以使用 类型别名 给函数类型取一个别名:
实例化函数类型
有几种方法可以获得函数类型的实例:
在函数字面值中使用代码块,有以下几种形式之一:
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
是一个高阶函数,因为它将一个函数值作为其第二个参数。 这第二个参数是一个表达式,它本身就是一个函数,称为函数字面值,相当于以下命名函数:
Lambda 表达式语法
Lambda 表达式的完整语法形式如下:
Lambda 表达式始终用花括号括起来。
完整语法形式中的参数声明位于花括号内,并且具有可选的类型注解。
箭头
->
后面是函数体。如果 lambda 的推断返回类型不是
Unit
,那么 lambda 主体内最后的(或可能是单个的)表达式被视为返回值。
如果省略所有可选的注解,剩下的形式如下:
传递尾随 Lambda
根据 Kotlin 的约定,如果函数的最后一个参数是一个函数,那么作为相应参数传递的 lambda 表达式可以放在括号外面:
这样的语法也被称为 尾随 lambda。
如果 lambda 是该调用中的唯一参数,那么括号可以完全省略:
it
: 单个参数的隐式名称
一个 lambda 表达式只有一个参数是非常常见的。
如果编译器可以在没有任何参数的情况下解析签名,那么参数就不需要声明, ->
可以省略。该参数将隐式地以名称 it
声明:
从 lambda 表达式返回值
你可以使用 qualified return 语法来显式地从 lambda 返回值。 否则,最后一个表达式的值将隐式返回。
因此,以下两个代码片段是等价的:
这个约定,以及 将 lambda 表达式放在括号外传递 ,使得可以编写LINQ-style代码:
下划线用于未使用的变量
如果 lambda 参数未使用,你可以用下划线代替其名称:
lambda 中的解构
lambda 中的解构被描述为 解构声明 的一部分。
匿名函数
上面的 lambda 表达式语法缺少一样东西 - 指定函数返回类型的能力。在大多数情况下,这是不必要的,因为返回类型可以自动推断。 但是,如果你确实需要明确指定它,可以使用另一种语法: 匿名函数。
匿名函数看起来非常像常规函数声明,只是省略了它的名称。它的主体可以是一个表达式(如上所示)或一个块:
参数和返回类型的指定方式与常规函数相同,只是如果它们可以从上下文中推断出,则参数类型可以省略:
对于匿名函数,返回类型的推断与常规函数一样工作:对于具有表达式主体的匿名函数,返回类型会自动推断,但对于具有块主体的匿名函数,必须明确指定返回类型(或假定为 Unit
)。
lambda 表达式和匿名函数之间的另一个区别是非局部返回的行为。 没有标签的 return
语句总是从使用 fun
关键字声明的函数中返回。 这意味着 lambda 表达式中的 return
将从包含它的函数返回,而匿名函数中的 return
将从匿名函数本身返回。
闭包
lambda 表达式或匿名函数(以及 局部函数 和 对象表达式 )可以访问其闭包 ,其中包括外部作用域中声明的变量。在闭包中捕获的变量可以在 lambda 中进行修改:
带接收者的函数字面值
带接收者的 函数类型 ,如 A.(B) -> C
,可以使用特殊形式的函数字面值——带接收者的函数字面值来实例化。
如上所述,Kotlin 提供了在提供 接收者对象 的同时调用函数类型实例的能力。
在函数字面值的主体内,传递给调用的接收者对象成为 隐式 的 this
,因此您可以在函数体内访问该接收者对象的成员,而无需任何额外的限定符,或者使用 this
表达式 访问接收者对象。
这种行为类似于 扩展函数 的行为,后者也允许您在函数体内访问接收者对象的成员。
下面是一个带接收者的函数字面值及其类型的示例,其中在接收者对象上调用 plus
:
匿名函数语法允许您直接指定函数字面值的接收者类型。 如果需要声明带接收者的函数类型的变量,然后稍后使用它,这可能很有用。
当接收者类型可以从上下文中推断出时,lambda 表达式可以用作带接收者的函数字面值。 它们的使用中最重要的例子之一是 类型安全的构建器: