高阶函数

高阶函数

高阶函数定义

高阶函数就是以另一个函数作为参数或者返回值的函数。

在Kotlin中,函数可以用lambda或者函数引用来表示。因此,Kotlin高阶函数是以lambda或者函数引用作为参数的函数,或者返回值为lambda或者函数引用的函数,或者两者都满足的函数。

函数类型

函数类型的声明语法:(参数类型列表) -> 返回类型。例如,(Int, String) -> Unit

// 函数类型在lambda中定义
val sum = { x: Int, y: Int -> x + y }
1
2
// 函数类型在变量声明定义
val sum: (Int, Int) -> Int = { x, y -> x + y }
1
2

在声明一个普通函数时,Unit类型的返回值是可以省略的,但声明一个函数类型时总是需要一个显式的返回类型,所以此时的Unit不能省略。

像其他方法一样,函数类型的返回值可以标记为可空类型

var canReturnNull: (Int, Int) -> Int? = { null }
1

也可以定义一个函数类型的可空变量。

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

调用作为参数的函数

调用作为参数的函数和调用普通函数的语法是一样的:把括号放在函数名后,并把参数放在括号内。

fun twoAndThree(operation: (Int, Int) -> Int) {
    val result = operation(2, 3)
    println("The result is $result")
}
1
2
3
4

函数类型作为参数时的默认值

函数类型作为参数时可以指定它的默认值,格式为参数名: 函数类型 = lambda

fun <T> Collection<T>.joinToString(
    separator: String = ", ",
    prefix: String = "",
    postfix: String = "",
    transform: (T) -> String = { it.toString() }
): String {...}
1
2
3
4
5
6

函数类型作为参数时的null值

声明一个参数为可空的函数类型,允许传入null值。

fun foo(callback: (() -> Unit)?) {
    if (callback != null) {
        callback()
    }
}
1
2
3
4
5

其实,函数类型是一个包含invoke方法的接口的具体实现。上面的例子可简写为:callback?.invoke()

通过lambda去除重复代码

可以用一个通用的函数类型来描述策略,然后传递不同的lambda表达式作为不同的策略。

// 计算指定员工群体的平均工资
fun List<Employee>.averageSalaryFor(strategy: (Employee) -> Boolean) =
    filter(strategy).map(Employee::salary).average()
1
2
3
// 计算30岁以下的员工群体的平均工资
list.averageSalaryFor { it.age <= 30 }
1
2

内联函数

当一个函数被声明为inline时,它的函数体是内联的。即函数体会被直接替换到函数被调用的地方,而不是被正常地调用。

内联函数消除lambda的运行时开销

每个lambda表达式会被编译成一个匿名类。默认情况下,这个生成的匿名类只有一个单例会被创建。但如果lambda捕捉了某个变量,那么每次调用都会创建一个新的对象。这会带来运行时的额外开销,导致使用lambda比使用一个直接执行相同代码的函数效率更低。

内联函数体中的lambda会随着内联函数体在函数被调用的地方替换代码,而不会生成匿名类及其实例。

/* 内联函数 */
inline fun <T> synchronizedFunc(lock: Lock, action() -> T): T {
    lock.lock()
    try {
        return action()
    } finally {
        lock.unlock()
    }
}
1
2
3
4
5
6
7
8
9
/* 调用内联函数 */
fun foo(l: Lock) {
    println("Before sync")
    synchronizedFunc(l) {
        println("Action")
    }
    println("After sync")
}
1
2
3
4
5
6
7
8
/* 内联函数在调用处替换代码 */
fun __foo__(l: Lock) {
    println("Before sync")
    l.lock()
    try {
        println("Action")
    } finally {
        l.unlock()
    }
    println("After sync")
}
1
2
3
4
5
6
7
8
9
10
11

如果在两个不同的位置使用同一个内联函数,但是用的是不同的lambda,那么内联函数会在每一个被调用的位置被分别内联

内联函数的限制

如果内联函数的lambda参数在某个地方被保存起来,以便后面可以继续使用,那么该lambda的代码将不能被内联。

lambda参数如果被直接调用或者作为参数传递给另外一个inline函数,它是可以被内联的。

如果内联函数期望两个或更多的lambda参数,可以选择只内联其中一些参数,用noinline修饰符来标记不进行内联的某个lambda参数。

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
    // ...
}
1
2
3

内联集合操作

集合的filter、map等函数都被声明为内联函数,所以它们的函数体会被内联,即传入的lambda参数也会被内联。

注意,用来处理序列的lambda是没有被内联的,这是因为每一个中间序列会把lambda保存在它的某个字段上。

何时将函数声明为内联

inline关键字声明的内联函数只能提高带有lambda参数的函数的性能。 不仅节约了函数调用的开销,而且节约了为lambda创建匿名类以及创建其实例对象的开销。

对于普通的函数调用,JVM已经提供了强大的内联支持。 JVM会分析代码的执行,并在任何通过内联能够带来好处的时候将函数调用内联。这是在将字节码转换成机器代码时自动完成的。 在字节码中,每一个函数的实现只会出现一次,并不会跟内联函数那样在每个调用的地方都拷贝一次。 如果函数是被直接调用,那么调用栈会更加清晰。

使用inline声明内联函数时,应该注意代码的长度。如果要内联的函数很大,将它的字节码拷贝到每一个调用点将会极大地增加字节码的长度。 在Kotlin标准库中的内联函数总是很小的。

使用内联lambda管理资源

lambda去除重复代码的一个常见模式是资源管理:先获取一个资源,然后完成一个操作,最后释放资源。

实现这个模式的标准做法是try/finally语句,资源在try代码块之前被获取,在finally代码块中被释放。

inline fun <T> synchronizedFunc(lock: Lock, action() -> T): T {
    lock.lock()
    try {
        return action()
    } finally {
        lock.unlock()
    }
}
1
2
3
4
5
6
7
8
val l: Lock = ...
synchronizedFunc(l) {
    // ...
}
1
2
3
4

Kotlin标准库为Lock接口定义了一个名为withLock的扩展函数,可使用它来代替上面的做法。

/* Kotlin库中withLock函数的定义 */
fun <T> Lock.withLock(action() -> T): T {
    lock()
    try {
        return action()
    } finally {
        unlock()
    }
}
1
2
3
4
5
6
7
8
9
val l: Lock = ...
l.withLock {
    // ...
}
1
2
3
4

高阶函数中的控制流

lambda中的返回语句

在lambda中使用return关键字,它会从调用lambda的函数中返回,并不只是从lambda中返回。类似于在Java中的for循环或synchronized代码块中使用return关键字。

fun lookForDaking(people: List<Person>) {
    people.forEach {
        if (person.name == "daking") {
            println("Found!")
            return
        }
    }
    println("Daking is not found.")
}
1
2
3
4
5
6
7
8
9

注意,只有在以lambda作为参数的函数是内联函数的时候,才能从更外层的函数返回。在一个非内联函数的lambda中使用return是不允许的

使用标签返回

使用标签返回来实现lambda的局部返回。

list.forEach 标签名@{
    if(it.name == "daking") return@标签名
}
1
2
3

使用lambda作为参数的函数的函数名可以作为标签。

list.forEach {
    if(it.name == "daking") return@forEach
}
1
2
3

若显式地指定了lambda表达式的标签,再使用函数名作为标签是没有任何效果的。一个lambda表达式的标签数量不能多于一个。

匿名函数默认使用局部返回

return从最近使用fun关键字声明的函数返回

  • lambda表达式没有使用fun关键字,所以lambda中的return默认从外层的函数返回。

  • 匿名函数使用了fun,因此其中的return从匿名函数返回。

fun lookForDaking(people: List<Person>) {
    people.forEach(fun (person) {
        if (person.name == "daking") return
        println("${person.name} is not the target.")
    })
}
1
2
3
4
5
6