高阶函数与内联优化

定义高阶函数

在 Kotlin 中,Lambda 表达式是函数式编程风格的灵魂所在。如果你想要定义出使用 Lambda 的函数式 API,就需要通过高阶函数来实现。

我们先来看看高阶函数的定义:如果一个函数接收另一个函数作为参数,或者其返回值是一个函数,那么该函数就是高阶函数。

为了理解这个定义,我们先来了解一下函数类型,它的基本语法如下:

kotlin 复制代码
// 语法:(参数类型1,参数类型2,...) -> 返回值类型
(String, Int) -> Unit

其中:

  • -> 左侧的部分,表示函数接收的参数类型列表。如果不接收任何参数,就只要写一对空括号即可。

  • -> 右侧的部分,表示函数的返回值类型。如果没有返回值,可以声明为 Unit

当一个函数的参数类型是函数类型时,那么它就是一个高阶函数。比如:

kotlin 复制代码
fun example(action: (String, Int) -> Unit) {
    action("hello", 123)
}

在函数内部,我们可以像调用普通函数一样调用这个函数类型的参数。

那这有什么用?简单来说,高阶函数可以让函数的一部分执行逻辑由函数的调用方来决定。

举个计算器的例子。我们定义一个高阶函数 calculate(),它接收两个整数和一个"操作"函数。函数并不关心操作过程是什么,它只会调用这个操作,获取到结果并返回:

kotlin 复制代码
fun calculate(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    val result = operation(num1, num2)
    return result
}

为了调用这个高阶函数,首先得有符合其函数类型的函数,我们定义两个函数:

kotlin 复制代码
fun plus(num1: Int, num2: Int): Int {
    return num1 + num2
}

fun minus(num1: Int, num2: Int): Int {
    return num1 - num2
}

然后,在 main() 函数中测试一下:

kotlin 复制代码
fun main() {
    val num1 = 20
    val num2 = 80
    val result1 = calculate(num1, num2, ::plus)
    println("result1 is $result1")

    val result2 = calculate(num1, num2, ::minus)
    println("result2 is $result2")
}

其中我们通过 ::函数名 函数引用的方式,来将函数作为参数进行传递。

运行结果:

csharp 复制代码
result1 is 100
result1 is -60

虽然函数引用的方式可行,但每次调用高阶函数时,就需要定义一个函数,就显得有些繁琐了。所以,Kotlin 还支持通过 Lambda 表达式来调用高阶函数,我们来看看它的用法。

上述调用如果使用 Lambda 表达式来完成的话,是这样的:

kotlin 复制代码
fun main() {
    val num1 = 80
    val num2 = 20

    val result1 = calculate(num1, num2) { n1, n2 -> n1 + n2 }
    println("result1 is $result1")

    val result2 = calculate(num1, num2) { n1, n2 -> n1 - n2 }
    println("result2 is $result2")
}

这时,我们并不需要定义额外的函数,就能调用高阶函数。

带接收者的函数类型

现在,我们使用高阶函数,来模仿标准库中的 apply 函数:

kotlin 复制代码
fun StringBuilder.myApply(block: StringBuilder.() -> Unit): StringBuilder {
    block()
    return this
}

我们为 StringBuilder 定义了一个名为 myApply 的扩展函数。它接收一个函数类型的 block 参数,不过,这个类型比较特殊,是一个带接收者的函数类型

其中 StringBuilder. 这部分即是接收者(Receiver) ,这样我们在传入的 Lambda 表达式中会自动拥有 StringBuilder 的上下文,可以访问 StringBuilder 类中的公有成员。

我们来测试一下:

kotlin 复制代码
fun main() {
    val list: List<String> = listOf("Sally", "Jack", "Martin", "Tommy", "Asher", "Alan")
    val builderInstance = StringBuilder().myApply {
        append("All People:\n")
        for (people in list) {
            append(people)
            append("\n")
        }
        append("There are ${list.size} people in total.")
    }
    val result = builderInstance.toString()
    println(result)
}

运行结果:

ini 复制代码
All People:
Sally
Jack
Martin
Tommy
Asher
Alan
There are 6 people in total.

但官方的 apply 函数似乎能被任何类型的对象调用,这就需要使用到 Kotlin 的泛型了,我们来修改一下:

kotlin 复制代码
fun <T> T.myApply(block: T.() -> Unit): T {
    block()
    return this
}

这样,任何类型的对象都可以调用它了。不过,它距离官方的 apply 还差在性能优化上。

内联函数的作用

高阶函数这么好用,有没有什么代价?当然是有的。

我们先来看看它背后的实现原理。以下面的 Kotlin 代码为例:

kotlin 复制代码
fun calculate(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    val result = operation(num1, num2)
    return result
}

fun main() {
    val result = calculate(100, 80) { n1, n2 -> n1 + n2 }
}

这段 Kotlin 代码被编译成 JVM 字节码,再反编译为 Java 代码,大致会是下面这个样子(简化便于理解):

java 复制代码
public static int calculate(int num1, int num2, Function2<Integer, Integer, Integer> operation) {
    int result = operation.invoke(num1, num2);
    return result;
}

public static void main() {
    int result = calculate(100, 80, new Function2<Integer, Integer, Integer>() {
        @Override
        public Integer invoke(Integer n1, Integer n2) {
            return n1 + n2;
        }
    });
}

可以看到,operation 参数的函数类型被转换成了 Function2 接口,而我们传入的 Lambda 表达式则变为了实现了 Function2 接口的匿名类实例。

这意味着我们每次调用高阶函数并传入 Lambda 表达式时,其实都会创建一个新的匿名类对象,这会造成额外的内存和性能开销。

为了解决这个问题,Kotlin 提供了 inline(内联)关键字,将高阶函数变为内联函数,可消除因使用 Lambda 表达式带来的运行时开销。

使用它很简单,只需在高阶函数的声明前加上 inline 关键字即可,像这样:

kotlin 复制代码
inline fun calculate(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    val result = operation(num1, num2)
    return result
}

那内联又是怎么实现的?代码替换

在编译期间,编译器会将内联函数中的代码,以及传入的 Lambda 表达式中的代码,"粘贴" 到调用处。

我们来说说这个过程,以上述的 calculate 的调用为例:

  1. 首先会将 Lambda 表达式中的代码,替换掉函数类型参数调用的地方:

  2. 然后内联函数的调用,会被替换成它的函数体:

最终编译后 main 函数中的代码会变为:

kotlin 复制代码
fun main() {
    val result = 100 + 80
}

就是因为这样,内联函数才能彻底消除因使用 Lambda 表达式带来的运行时开销。

注意:如果内联函数很长,且在很多地方被调用,那么会导致生成的字节码体积增加。所以,内联函数只适合接收 Lambda 参数且函数体不多的高阶函数。

现在我们就可以来改进我们之前的 myApply 函数了,标准库中的 apply 函数是一个内联函数,可以消除因使用 Lambda 带来的性能开销,所以优化后的 myApply 函数为:

kotlin 复制代码
 inline fun <T> T.myApply(block: T.() -> Unit): T {
     block()
     return this
 }

noinline与crossinline

非局部返回

内联的 Lambda 表达式中,可以直接使用 return 关键字来退出 外层的函数,这被称为非局部返回 。而非内联函数只能进行局部返回,使用 return@label 的语法来退出 Lambda 表达式本身。

请看这个例子:

kotlin 复制代码
data class User(val name: String)

fun findUser(users: List<User>, action: (User) -> Unit) {
    println("--- Start search ---")
    for (user in users) {
        if (user.name == "Admin") {
            action(user)
        }
    }
    println("--- End search ---")
}

fun main() {
    val users = listOf(User("Alice"), User("Admin"), User("Bob"))
    findUser(users) { user ->
        println("Found: ${user.name}")
        return@findUser // 只能局部返回,退出 Lambda
    }
    println("Main function continues...")
}

我们定义了一个函数 findUser,用于查找用户列表中的用户,找到了就使用 return@find 这种写法进行局部返回,不再执行 Lambda 表达式剩余的代码。

注意:在该 Lambda 表达式中不能使用 return 关键字进行返回,否则会报错:'return' is not allowed here

运行结果:

lua 复制代码
--- Start search ---
Found: Admin
--- End search ---
Main function continues...

现在,我们将 findUser 函数声明为内联函数:

kotlin 复制代码
data class User(val name: String)


inline fun findUser(users: List<User>, action: (User) -> Unit) {
    println("--- Start search ---")
    for (user in users) {
        if (user.name == "Admin") {
            action(user)
        }
    }
    println("--- End search ---")
}

fun main() {
    val users = listOf(User("Alice"), User("Admin"), User("Bob"))
    findUser(users) { user ->
        println("Found: ${user.name}")
        return // 直接 return 并结束 main 函数
    }
    println("Main function continues...")
}

此时在 Lambda 表达式中,就可以使用 return 关键字了,并且会退出 main 函数。

运行结果:

diff 复制代码
--- Start search ---
Found: Admin

因为 Lambda 表达式中的代码最终会被放("粘贴")到 main 函数中,所以 return 自然是返回 main 函数了。

noinline

默认情况下,内联函数会将其所有的 Lambda 参数进行内联。但如果你只想内联其中一部分,该怎么办?

这时,就需要用到 noinline 关键字了。只需在不想被内联的 Lambda 参数之前加上即可,如下所示:

kotlin 复制代码
inline fun processBlocks(block1: () -> Unit, noinline block2: () -> Unit) {
    
}

内联可以消除因使用 Lambda 表达式所带来的运行时开销,那我们为什么要非内联的函数类型参数呢?

因为内联的 Lambda 参数在编译后,会被其内部代码进行替换,并不是一个函数对象。在内联函数的内部,不能传递给另一个需要函数对象的普通函数,而非内联的 Lambda 参数可以作为函数对象自由传递。

例如:

kotlin 复制代码
fun executeInRunnable(block: () -> Unit) {
    println("--- Executing runnable ---")
    Runnable {
        block()
    }.run()
}

inline fun processBlocks(block1: () -> Unit, noinline block2: () -> Unit) {
    println("Executing block1 (inlined)")
    block1()

//    // 内联 lambda 参数无法传递给普通函数
//    executeInRunnable(block1) // 报错 Illegal usage of inline-parameter 'block1' ... Add 'noinline' modifier to the parameter declaration

    println("Passing block2 as an object to another function")
    // 非内联 lambda 参数可以作为函数对象自由传递
    executeInRunnable(block2)
}

crossinline

如果内联函数的 Lambda 参数在一个新的作用域中被调用,比如在另一个线程中或者另一个匿名类中,编译会不通过,因为无法处理其中可能存在的非局部返回。比如:

kotlin 复制代码
inline fun runRunnable(block: () -> Unit) {
    val runnable = Runnable {
        block()
    }
    runnable.run()
}

其中 block() 这行代码会报错:Can't inline 'block' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration 'block'

上述代码,我们在一个匿名类的实现中,调用了内联的函数类型参数。如果在该 Lambda 表达式中使用 return 进行非局部返回,这个 return 将试图穿过一个作用域边界Runnable 匿名类),去返回外层的函数(Lambda 表达式所在的函数)。这种返回会导致混乱的控制流,所以编译器禁止了这样的使用方式。

这种情况下,我们可以使用 crossinline 关键字,使 runRunnable 函数仍然是内联函数。如下所示:

kotlin 复制代码
inline fun runRunnable(crossinline block: () -> Unit) {
    val runnable = Runnable {
        block()
    }
    runnable.run()
}

它会向编译器做出一个保证:在这个 Lambda 表达式中我不会使用非局部返回 return。

当使用了 crossinline 后,你在 Lambda 表达式中将不允许使用 return 关键字进行非局部返回,比如:

kotlin 复制代码
fun main() {
    runRunnable {
        return
    }
}

会报错:'return' is not allowed here

但还是可以使用 return@runRunnable 进行局部返回的,比如:

kotlin 复制代码
fun main() {
    runRunnable {
        return@runRunnable
    }
}

总的来说,除了 return 关键字使用上的差别,crossinline 保留了内联带来的性能优势。

相关推荐
QING61814 小时前
Jetpack Compose Brush API 简单使用实战 —— 新手指南
android·kotlin·android jetpack
QING61815 小时前
Jetpack Compose Brush API 详解 —— 新手指南
android·kotlin·android jetpack
鹿里噜哩15 小时前
Spring Authorization Server 打造认证中心(二)自定义数据库表
spring boot·后端·kotlin
用户693717500138421 小时前
23.Kotlin 继承:继承的细节:覆盖方法与属性
android·后端·kotlin
Haha_bj21 小时前
五、Kotlin——条件控制、循环控制
android·kotlin
Kapaseker1 天前
不卖课,纯干货!Android分层你知多少?
android·kotlin
urkay-2 天前
Android 切换应用语言
android·java·kotlin·iphone·androidx
杀死那个蝈坦2 天前
监听 Canal
java·前端·eclipse·kotlin·bootstrap·html·lua
Yang-Never2 天前
Open GL ES->EGL渲染环境、数据、引擎、线程的创建
android·java·开发语言·kotlin·android studio
urkay-2 天前
Android 全局修改设备的语言设置
android·xml·java·kotlin·iphone