Kotlin 教程(二)

四、高阶函数

4.1 匿名函数

高阶函数的意思是使用函数作为变量或者返回值的函数。

// 这就是一个高阶函数,它的参数是一个函数类型的对象
// (Int) -> Unit 表示这是一个参数为 Int,无返回值的函数类型的对象
fun high(function: (Int) -> Unit) {
    // 函数类型的对象通过 invoke 来调用其函数
    function.invoke(666)
}

fun main() {
    // 调用高阶函数时传入一个函数即可,传入的函数没有名字,所以又被称之为匿名函数
    high(fun(number: Int): Unit {
        println("number = $number")
    })
}

4.2 Lambda 表达式

fun high(function: (Int) -> Unit) {
    function.invoke(666)
}

fun main() {
    // 传入的匿名函数可以简化为 Lambda 表达式的形式
    high({ number: Int ->
        println("number = $number")
    })
    // 如果 Lambda 表达式是高阶函数的最后一个参数,则可以把 Lambda 表达式写在括号外面
    high() { number: Int ->
        println("number = $number")
    }
    // 如果 Lambda 表达式是高阶函数的唯一参数,则可以省略括号
    high { number: Int ->
        println("number = $number")
    }
    // 如果 传入的这个参数是单参数的,则可以省略不写这个参数,使用默认名字 it 表示此参数即可
    high {
        println("number = $it")
    }
}

Kotlin 的匿名函数和 Lambda 表达式本质上就是一个函数类型的对象,它和函数不是同一个东西,而是一个 Kotlin 帮我们自动生成的,我们看不见的一个和函数具有相同功能的对象。

4.3 双冒号 + 函数名

除了使用匿名函数和 Lambda 之外,我们还可以通过双冒号 “::” + 函数名的方式来构造一个函数类型的对象。

fun high(function: (Int) -> Unit) {
    function.invoke(666)
}

fun main() {
    // “::” + 函数名可以使函数变成函数类型的对象,使得它可以被传入高阶函数中
    high(::normal)
}

fun normal(number: Int) {
    println("number = $number")
}

五、内联函数

5.1 inline

函数类型的对象说起来比较抽象,如果用 Java 来表示的话,高阶函数的代码大致是这样的:

public static void high(Function function) {
    function.invoke(666);
}

public static void main() {
    // Kotlin 内部会帮我们建立一个和函数功能相同的对象,这个对象就被称之为函数类型的对象
    high(new Function() {
        @Override
        public void invoke(int number) {
            System.out.println("number = " + number);
        }
    });
}

也就是说,每当我们使用函数类型的对象时,都会创建一个新的对象,这就会造成额外的内存和性能开销。为了解决此问题,Kotlin 提供了内联函数。

// 添加 inline 关键字使其变成内联函数
inline fun high(function: (Int) -> Unit) {
    function.invoke(666)
}

fun main() {
    // 在编译时,Kotlin 首先会将内联函数中 Lambda 表达式的内容替换到其调用的地方,然后再将内联函数中的全部代码移到调用处
    // 也就是说,这里最终的代码会变成直接调用 println("number = 666"),所以说内联函数完全消除了 Lambda 表达式带来的运行时开销
    high {
        println("number = $it")
    }
}

如果给非高阶函数添加 inline 关键字,我们会看到一条提示:Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types,意思是 inline 对普通函数的性能提升微乎其微,inline 关键字主要用于高阶函数。这是因为 JVM 已经对普通的函数进行了内联优化。

5.2 noinline

如果一个高阶函数接收了两个或者更多的函数类型的对象作为参数,在这个高阶函数添加了 inline 关键字后,所有函数类型的对象均会被内联,如果我们想要某个函数类型的对象不被内联,则可以为此参数添加 noinline 关键字。

// 给 block1 参数添加 noinline 关键字使其不要内联
inline fun high(noinline block1: () -> Unit, block2: () -> Unit){}

既然内联函数这么好用,为什么还要提供 noinline 呢?

上文中说到,内联函数在编译时会被替换为实际运行的代码,所以说它的参数没有真正的参数属性,在函数内部需要传参时,只能传递给另一个内联函数,这就是它最大的局限性。而非内联函数的参数是真实的参数,可以自由的进行传递。

5.3 crossinline

在 Kotlin 的 Lambda 表达式中,return 只能用于局部返回,并且加上 @返回范围

thread { 
    // 这里是局部返回,终止 thread 中的代码,但 thread 之后的代码不受影响
    return@thread
}
// thread 之后的代码可以继续执行

但由于内联函数中的 Lambda 会在编译时替换为实际代码,所以内联函数的 Lambda 中,可以使用 return 全局返回:

inline fun high(block: () -> Unit) {
    block.invoke()
}

fun main() {
    high {
        println("hello")
        // 这里是全局返回,Lambda 之后的代码无法再被运行。(这里也可以用 return@high 局部返回)
        return
    }
    // 这里的代码不会被执行
    println("test")
}

如果内联函数中的 Lambda 表达式在函数内另一个 Lambda 表达式中使用,return 就可能产生歧义:

inline fun high(block: () -> Unit) {
    thread {
        // 这里是无法编译通过的,因为这里会产生歧义
        // 如果 block 的代码中使用了 return,想要进行全局返回,但由于 block 的代码是在这里的 Lambda 表达式中执行的,无法全局返回,就会导致出错。
        block.invoke()
    }
}

为了解决这个问题,我们需要给 block 参数加上 crossinline 关键字,禁止 block 的代码中使用全局返回:

// 为 block 参数添加 crossinline 关键字,表示不允许 block 中使用全局返回。
inline fun high(crossinline block: () -> Unit) {
    thread {
        block.invoke()
    }
}

fun main() {
    high {
        println("hello")
        // 由于这里的 Lambda 加上了 crossinline 关键字,所以这里无法再使用全局返回,只能局部返回
        return@high
    }
    // 这里的代码依旧能够执行
    println("test")
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章