type: doc layout: reference category: "Syntax"

title: "Higher-Order Functions and Lambdas"

高阶函数和lambda表达式

高阶函数

高阶函数是一种能用函数作为参数或者返回值为函数的一种函数。 lock()是高阶函数中一个比较好的例子,它接收一个lock对象和一个函数,获得锁,运行传入的函数,并释放锁:

fun lock<T>(lock: Lock, body: () -> T): T {
  lock.lock()
  try {
    return body()
  }
  finally {
    lock.unlock()
  }
}

我们分析一下上面的代码:函数body拥有函数类型:() -> T 所以body应该是一个不带参数并且返回T类型的值的函数。 它在try{: .keyword }代码块中调用,被lock保护的,当lock()函数被调用时返回他的值。

如果我们想调用lock()函数,我们可以把另一个函数传递给它作为参数(详见 函数引用):

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

另外一种更为便捷的方式是传入一个 函数字面量 (通常被称为 lambd 表达式):

val result = lock(lock, { sharedResource.operation() })

函数字面量 这里有更详细的描述, 但是为了继续这一段,让我们看到一个简短的概述:

  • 一个函数字面值总是被大括号包围着。
  • 其参数(如果有的话)被声明在->之前(参数类型可以省略)
  • 函数体在 -> 后面 (如果存在的话).

在Kotlin中, 如果函数的最后一个参数是一个函数,那么我们可以省略括号

lock (lock) {
  sharedResource.operation()
}

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

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

这个函数可以如下调用:

val doubled = ints.map {it -> it * 2}

还有一个有用的公约是:如果函数字面量有一个参数,那它的声明可以省略,用 it 表示。

ints map {it * 2} // Infix call + Implicit 'it'

这些约定可以写成 LINQ-风格 的代码:

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

内联函数

使用内联函数有时能提高高阶函数的性能。

函数字面量和函数表达式

一个函数字面量和函数表达式是一个“匿名函数”, 即 一个未声明的函数,但却立即写为表达式。思考下面的例子:

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

max函数是一个高阶函数, 也就是说 他的第二个参数是一个函数. 这个参数是一个表达式,但它本身也是一个函数, 也就是函数字面量.写成一个函数的话,它相当于

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

函数类型

对于一个接收一个函数作为参数的函数,我们必须为该参数指定一个函数类型。譬如上述max函数定义如下:

fun max<T>(collection: Collection<out T>, less: (T, T) -> Boolean): T? {
  var max: T? = null
  for (it in collection)
    if (max == null || less(max!!, it))
      max = it
  return max
}

参数 less 是一个 (T, T) -> Boolean类型的函数, 也就是说less函数接收两个T类型的参数并返回一个Boolean值: 如果第一个比第二个小就返回True.

在第四行代码里, less 被用作为一个函数: 它传入两个T类型的参数.

如上所写的是就函数类型, 或者还有命名参数, 目的就是能通过命名参数调用.

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

函数字面量语法

函数字面量的全部语法形式, 也就是函数类型的字面量, 譬如下面的代码:

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

一个函数字面值总是被大括号包围着,在括号内有全部语法形式中的参数声明并且有可选的参数类型。 函数体后面有一个 -> 符号。如果我们把所有的可选注释都留了出来,那剩下的是什么样子的:

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

这是非常常见的,一个函数字面量只有一个参数。 如果Kotlin能自己计算出自己的数字签名,我们就可以不去声明这个唯一的参数。并且用it进行隐式声明。

ints.filter {it > 0} // this literal is of type '(it: Int) -> Boolean'

请注意,如果函数取另一个函数作为最后一个参数,该函数字面量参数可以放在括号外的参数列表。 语法细则详见 call-Suffix.

函数表达式

上述函数字面量的语法还少了一个东西: 能够指定函数的返回类型。在大多数情况下, 这是不必要的。因为返回类型可以被自动推断出来. 然而,如果你需要要明确的指定。你需要一个替代语法: 函数表达式.

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

函数表达式看起来很像一个正则函声明, 只是名字被省略了. 内容也是一个表达式(如上面的代码)或者代码块:

fun(x: Int, y: Int): Int {
  return x + y
}

指定的参数和返回类型与指定一个正则函数方式相同,只是如果参数类型能沟通过上下文推断出来,那么该参数类型是可以省略的:

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

函数表达式的返回类型推断法只适用于常规函数:具有表达式体并且必须明确指定函数体有代码(或者假定Unit)块的返回类型能够被自动推断出来。

请注意,函数表达式参数始终在圆括号内传递. 允许在函数括号外使用的速记语法只针对于函数字面量。

另一个函数字面量和函数表达式区别是 non-local returns的行为.一个带标签的return{:. keyword} 语句,总是在用fun{: .keyword } 关键词声明的函数中返回。这意味着函数字面中的return{: .keyword }将在函数闭包中返回 。然而函数表达式return*{: .keyword}的就是在函数表达式中返回。

闭包

一个函数字面量或表达式(以及一个本地函数本地函数和一个 对象表达式)可以访问他的闭包,即声明在外范围内的变量。与java不同,在闭包中捕获的变量可以被修改:

var sum = 0
ints filter {it > 0} forEach {
  sum += it
}
print(sum)

扩展函数表达式

除了常规函数,kotlin支持扩展函数。拓展函数字面量是很有用处的,并且支持表达式。 他们的一个最重要的例子是Type-safe Groovy-style builders的使用。

扩展函数表达式不同于一般的函数表达式,它有一个接收器类型规范。

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

接收器类型可仅在函数表达式中指定,而不是在函数字面量中。函数字面量能作为拓展函数表达式,但是只能在接收器类型能够通过上下文推断出来的时候。

扩展函数表达式的类型是接收器函数类型:

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

该函数可以被称为一个点或中缀形式(因为它只有一个参数):

1.sum(2)
1 sum 2

翻译By Airoyee