高阶函数与内联优化

定义高阶函数

在 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 保留了内联带来的性能优势。

相关推荐
移动开发者1号2 小时前
Android中Activity、Task与Process的关系
android·kotlin
移动开发者1号3 小时前
Activity onCreate解析
android·kotlin
alexhilton12 小时前
在Android应用中实战Repository模式
android·kotlin·android jetpack
&岁月不待人&1 天前
实现弹窗随键盘上移居中
java·kotlin
移动开发者1号1 天前
Android Activity状态保存方法
android·kotlin
移动开发者1号1 天前
Volley源码深度分析与设计亮点
android·kotlin
移动开发者1号2 天前
App主界面点击与跳转启动方式区别
android·kotlin
移动开发者1号2 天前
我用Intent传大图片时竟然崩了,怎么回事啊
android·kotlin
androidwork3 天前
Android LinearLayout、FrameLayout、RelativeLayout、ConstraintLayout大混战
android·java·kotlin·androidx