高阶函数和lambda

高阶函数

高阶函数可以接收函数类型的参数,也可以返回一个函数。lock() 是一个很好的例子,它接收一个 lock 对象和一个函数,获取锁之后执行函数,然后释放锁:

  1. fun <T> lock(lock: Lock, body: () -> T): T {
  2. lock.lock()
  3. try {
  4. return body()
  5. }
  6. finally {
  7. lock.unlock()
  8. }
  9. }

上面的代码中,body 是一个函数类型:() -> T,无参并且返回值类型是 T 。在 try 块中发起调用,被 lock 保护,由 lock() 提供返回值。

lock() 可接收另一个函数作为它的参数:

  1. fun toBeSynchronized() = sharedResource.operation()
  2. val result = lock(lock, ::toBeSynchronized)

传入一个 lambda 表达式会更简单:

  1. lock(lock, { sharedResource.operation() })

下文有关于 lambda 表达式更详细的描述,先看一下概览:

  • lambda 表达式由花括号包裹
  • 参数在 -> 之前声明(参数类型可省略)
  • 函数体在 -> 之后(如果有的话)

如果一个函数的最后一个参数是一个函数,我们可以在函数括号之外传入一个 lambda 表达式:

  1. lock (lock) {
  2. sharedResource.operation()
  3. }

另一个高阶函数的例子是 map()

  1. fun <T, R> List<T>.map(transform: (T) -> R): List<R> {
  2. val result = arrayListOf<R>()
  3. for (item in this)
  4. result.add(transform(item))
  5. return result
  6. }

用法如下:

  1. val doubled = inis.map { value -> value * 2 }

如果 lambda 是函数唯一的参数,括号可以省略。

it: 单个参数的隐式名字

另外一个比较有用的约定是:如果一个函数只有一个参数,那么参数的声明连带 -> 可以省略,参数名用 it 指代:

  1. ints.map { it * 2 }

这些约束允许使用 LINQ 风格的代码:

  1. strings.filter { it.length == 5 }.sortedBy { it }.map { it.toUpperCase() }

下划线指代无用变量(1.1开始)

如果 lambda 的参数无用,可以用下划线代替:

  1. map.forEach { _, value -> println("$value!") }

lambda 解构

参考解构声明。

内联函数

使用内联函数可以提升高阶函数性能。

lambda 表达式和匿名函数

一个 lambda 表达式或匿名函数是一个 “函数字面量(function literal)”,例如,一个函数没有声明,但是直接作为表达式传递。如下:

  1. max(strings, { a, b -> a.length < b.length })

max 是高阶函数,它的第二个参数是一个函数。第二个参数是一个表达式,这个表达式本身是一个函数(函数字面量)。作为函数,它等价于:

  1. fun compare(a: String, b: String): Boolean = a.length < b.length

函数类型

对于接收函数作为参数的函数,我们需要为那个参数指定函数类型。例如,上述提到的 max,其定义如下:

  1. fun <T> max(collection: Collection<T>, less: (T, T) -> Boolean): T? {
  2. var max: T? = null
  3. for (it in collection)
  4. if (max == null || less(max, it))
  5. max = it
  6. return max
  7. }

参数 less 的类型是 (T, T) -> Boolean,两个 T 类型参数,返回值是 Boolean:如果第一个参数小于第二个,返回 true。

函数体的第四行,less 是一个函数调用,传入了两个类型为 T 的参数。

函数类型的写法如上,也可以用命名参数来增加可读性:

  1. val compare: (x: T, y: T) -> Int = ...

如果要声明一个可空函数类型的变量,可以用括号包裹整个函数类型,然后在后面加一个问号:

  1. var sum: ((Int, Int) -> Int)? = null

lambda 表达式的语法

lambda 表达式,即函数类型的字面量,其完整的语法形式如下:

  1. val sum = { x: Int, y: Int -> x + y }

lambda 表达式由大括号包裹,在完整的语法格式下,参数声明在括号内,并且可以带有可选的类型标记,函数体在 -> 符号之后。如果编译器推测出来返回值类型不是 Unit,函数 体内的最后一个表达式(可能只有一个表达式)会被当做返回值。

如果去掉所有的可选标记,剩下的代码如下所示:

  1. val sum: (Int, Int) -> Int = { x, y -> x + y }

lambda 只有一个参数是很常见的。如果 Kotlin 本身能够识别出 lambda 的函数签名,我们就不需要手动去声明这个唯一的参数,因为 Kotlin 会隐式地帮我们把这个参数声明为 it

  1. ints.filter { it > 0 }

我们可以用限定 return 的语法从 lambda 中显示地返回一个值。否则,最后一个表达式的值会被隐式返回。因此,下面两个代码片段是等价的:

  1. ints.filter {
  2. val shouldFilter = it > 0
  3. shouldFilter
  4. }
  5. ints.filter {
  6. val shouldFilter = it > 0
  7. return@filter shoudFilter
  8. }

注意:如果一个函数的最后一个参数是函数类型,那么使用 lambda 表达式的实参可以位于实参列表的括号之外。这个语法叫 callSuffix

匿名函数

上面所示的 lambda 表达式的语法缺少了一项能力:无法指定返回值类型。多数情况下,类型可以自动推导出,所以也没有必要指定。但是,如果一定要指定类型的话,可以利用另一种语法:匿名函数

  1. fun(x: Int, y: Int): Int = x + y

除了没有函数名之外,匿名函数的声明和常规函数并无二致。函数体既可以是上例的表达式也可以是区块:

  1. fun(x: Int, y: Int): Int {
  2. return x + y
  3. }

参数和返回值类型的指定方式与常规函数一样,唯一不同的是,能够根据上下文推导出的参数类型可以直接省略掉。

  1. ints.filter(fun(item) = item > 0)

匿名函数的返回值类型推导和常规函数一样:

  • 如果函数体是一个表达式,返回类型可以自动推导
  • 如果函数体是一个区块,返回必须显示指定(或者假定为 Unit

注意:匿名函数作为参数时必须在圆括号内。在圆括号外部传参的简化语法只适用于 lambda 表达式。

lambda 表达式和匿名函数的另一个不同体现在 非局部返回 的行为。不带标签的 return 语句返回的是带有 fun 关键字的函数。这就意味着 lambda 表达式内的 return 返回的是它的包围函数。那么匿名函数内的 return 返回的是这个匿名函数本身(因为匿名函数也需要 fun 关键字修饰)。

闭包

lambda 表达式或者匿名函数(以及局部函数和对象表达式)可以访问它的闭包,即外围区域定义的变量。与 Java 不同的是,在闭包内捕获(captured)的变量可以被修改:

  1. var sum = 0
  2. ints.filter { it > 0 }.forEach {
  3. sum += it
  4. }

带接收者的 function literal

Kotlin 可以为函数字面量的调用指定一个 接收者对象。在函数字面量内部,可以不带任何限定符地访问接收者对象的方法。这点和扩展函数类似,在函数体也可以访问接收者对象的成员。最重要的用途示例之一是类型安全的 Groovy 风格构建器

这种函数字面量的类型是带接收者的函数类型:

  1. sum: Int.(other: Int) -> Int

这种函数字面量的调用就好像它是接收者对象的一个方法:

  1. 1.sum(2)

匿名函数的语法允许我们直接指定函数字面量的接收者类型。因此,我们可以事先定义一个带接收者的函数变量,以备后用。

  1. val sum = fun Int.(other: Int): Int = this + other

带接收者函数的非字面值能够和普通函数兼容,前提是普通函数的第一个参数需要是接收者的类型,它们可以互相赋值,也可以作为参数来传递。例如,String.(Int) -> Boolean(String, Int) -> Boolean 是兼容的。

  1. val represents: String.(Int) -> Boolean = { other -> toIntOrNull() == other }
  2. println("123".represents(123)) // true
  3. fun testOperation(op: (String, Int) -> Boolean, a: String, b: Int, c: Boolean) =
  4. assert(op(a, b) == c)
  5. testOperation(represents, "100", 100, true) // OK

上例中,represents 是一个类型为 String.(Int) -> Boolean 的 lambda 变量。

它的值是 { other -> toIntOrNull() == other }

  • 大括号表示 lambda 的定义
  • lambda 的 receiver object 的类型是 String
  • toIntOrNull() 是 String 的方法,调用对象是 receiver object。

lambda 表达式可用作带接收者的函数字面量,前提是接收者的类型可以通过上下文推导出。

  1. class HTML {
  2. fun body() { ... }
  3. }
  4. fun html(init: HTML.() -> Unit): HTML {
  5. val html = HTML() // Create the receiver object
  6. html.init() // pass the receiver object to the lambda
  7. return html
  8. }
  9. html { // lambda with receiver begins here
  10. body() // calling a method on the receiver object
  11. }

关于 function literal 的解释

把“function literal”翻译成“函数字面量”,我也不知道准不准确,这里稍微解释一下。

如果我们直接把一个函数写在那,就叫 function literal;反之,如果我们用一个变量来表示函数,那么这个变量就不是 function literal,而是一个 non-literal value。