Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
282 lines (195 sloc) 9.27 KB

高阶函数和lambda

高阶函数

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

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

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

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

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

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

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

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

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

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

lock (lock) {
    sharedResource.operation()
}

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

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 = inis.map { value -> value * 2 }

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

it: 单个参数的隐式名字

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

ints.map { it * 2 }

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

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

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

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

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

lambda 解构

参考解构声明。

内联函数

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

lambda 表达式和匿名函数

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

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

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

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

函数类型

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

fun <T> max(collection: Collection<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,两个 T 类型参数,返回值是 Boolean:如果第一个参数小于第二个,返回 true。

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

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

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

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

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

lambda 表达式的语法

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

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

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

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

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

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

ints.filter { it > 0 }

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

ints.filter {
    val shouldFilter = it > 0
    shouldFilter
}

ints.filter {
    val shouldFilter = it > 0
    return@filter shoudFilter
    }

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

匿名函数

上面所示的 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 返回的是这个匿名函数本身(因为匿名函数也需要 fun 关键字修饰)。

闭包

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

var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}

带接收者的 function literal

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

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

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

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

1.sum(2)

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

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

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

val represents: String.(Int) -> Boolean = { other -> toIntOrNull() == other }
println("123".represents(123)) // true

fun testOperation(op: (String, Int) -> Boolean, a: String, b: Int, c: Boolean) = 
    assert(op(a, b) == c)

testOperation(represents, "100", 100, true) // OK

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

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

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

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

class HTML {
    fun body() { ... }
}

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()   // Create the receiver object
    html.init()         // pass the receiver object to the lambda
    return html
}

html {      // lambda with receiver begins here
    body()  // calling a method on the receiver object
}

关于 function literal 的解释

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

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