Kotlin中的高阶函数、内置高阶函数和内联

1,377 阅读7分钟

简介

高阶函数是将函数用作参数或返回值的函数。在Kotlin函数都是头等的,可以将存储在变量与数据结构中、作为参数传递给其他函数或者将它们作为返回值从其他函数里返回。我们不仅可以自己定义高阶函数。

同时Kotlin还为我们提供了大量的内联高阶函数如let、run、also、with等。这些函数都是基于高阶函数实现的,掌握了高阶函数后,才能真正的了解和明白这些内置函数。

下面就是一个高阶函数的简单的使用案例(大家在实际开发中一定要注意代码规范,不要像我这样偷懒而去随便命名):

fun main() {
    val fun1 = fun(name: String) {
        println(name)
    }
    //高阶函数使用
    func2("白瑞德", fun1)
    //lambda表达式写法
    func2("白瑞德") {
        println(it)
    }
}


fun func2(name: String, func: (String) -> Unit) {

    func("名字叫做$name")
}

因为在Kotlin中,如果在默认参数之后的最后一个参数是lambda表达式,那么它既可以作为具名参数在括号内传入,也可以在括号外传入。所以才有了第二种写法。接下来我们都会使用lambda的方式书写代码。

高阶函数形参以及返回值的定义

Kotlin中的函数语法一般如下:

修饰符 函数名(参数名:参数类型) : 返回值类型 {
    ...
    方法体
    ...
    return 返回值;
}

按照简介里的定义,如果想定义一个高阶函数,只需要将参数类型返回值类型中的任意一个定义为函数类型就可以了。我们都知道函数的三个重要的构成:函数名、形参和返回值。要将函数作为参数,那么形参就可以指代它了。所以无需在函数名上耗费太多精力。只需要关注形参和返回值就好了。例如上文中的func2函数的形参func: (String) -> Unit。它的意思就是接收一个入参为String类型,没有返回值(void)的函数。

有了这个理解,我们就可以很明确的确定形参和返回值的定义格式了:func:(T...)->R。其中T和R就是你想要的具体类型(在Kotlin内置的高阶函数如let,run,also等就是使用T、R来表示泛型)。T...表示可以有多个形参。而R也可以是Unit, Unit类型实现了与Java中的void一样的功能,表示函数无需返回值。

下面我们将分类展示一些它们的使用以及一些区别:

  • ()->R
  • (T...)->R
  • T.()->R
  • 组合类T.(Any)->R

我们首先创建一个User类供我们演示使用:

class User {

    var name: String = ""
    var age: Int = 0

    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }

    fun printMessage() {
         println("姓名:$name+年龄:$age")
    }
}

代码很简单,两个成员变量和一个打印成员变量的函数。

首先看第一组:

()->R

这种形式的参数接收一个返回值为R的无参函数,如果R是Unit类型,则没有返回值。

首先定义一个高阶函数:

fun uFun1(funcUnit:()->Unit,funcUser:()->User){
    funcUnit()
    funcUser().printMessage()
}

uFun1接受两个无参的函数,唯一的区别是后者会返回一个User实例。接下来我们看一下使用方式:

fun main() {
    val u1 = fun() {
        println("要准备打印个人信息了")
    }
    val u2 = fun(): User {
        return User("白瑞德", 19)
    }
    uFun1(u1, u2)
    uFun1(u1) {
        User("白瑞德", 19)
    }
}

输出结果如下:

要准备打印个人信息了
姓名:白瑞德+年龄:19
要准备打印个人信息了
姓名:白瑞德+年龄:19

注意看lambda的实现方式,连return关键字都省略了。

我们通过编译器将Kotlin的字节码实现转换成Java代码,看一下它的运行机制:

image.png

不难看出,在Kotlin中原本为函数的参数在Java中被一个Function0对象替代了。

而Function0是一个接口,具体实现如下:

image.png

看到这里恍然大悟:Kotlin中的高阶函数中,作为参数的函数,实际上再JVM中就是使用匿名内部类实现的。

(T...)->R

这种形式的参数接收一个返回值为R且有参数T(数量不固定,类型也不固定)函数,如果R是Unit类型,则没有返回值。

首先定义一个高阶函数:

fun uFun2(funcUnit: (user: User) -> Unit, funcUser: (user: User, afterYears: Int) -> String) {
    val user = User("白瑞德", 18)
    funcUnit(user)
    val funcUser1 = funcUser(user, 10)
    println(funcUser1)
}

它接收两个函数:第一个形参为User无返回值;第二个形参有两个User和int,返回值为String。

使用方法如下:

fun main() {
    val u3 = fun(user: User) {
        user.printMessage()
    }

    val u4 = fun(user: User, afterYears: Int): String {
        user.age += afterYears
        return "${user.name}今年${user.age - afterYears}岁"
    }

    val u5 = fun User.() {
        printMessage()
    }
    uFun2(u3, u4)
    uFun2(u5) { u, y -> "${u.name}$y 年后就是${u.age + y}岁了" }

    uFun3 {
        this.name
    }
}

和()->R一样,lambda的实现方式明显更加简介高效。

代码里的u5处所定义的函数需要注意,它是一个User的扩展函数。

fun User.() 和 fun(user: User)的使用方法相同,他们都需要传入一个User对象。区别在于函数体内对User对象的调用方式不同。前者既然作为扩展函数,可以直接向在User类内部调用函数一样,直接使用类所定义的成员变量和函数(实际是通过this的方式访问的,但是this可省略)。后者就是一个普通的函数了,如果想要访问User类内的任意函数和变量,都必须使用user.的方式显式的调用。另外它们还有一点共同点,就是他们都不支持对User的私有成员进行访问。

T.()->R

这种方式和 (T...)->R的要求基本类似,只是对类的成员访问的方式不同。

看一下具体实现:

fun uFun3(funcUnit: User.() -> Unit, funcUser: User.(afterYears: Int) -> String) {
    val user = User("白瑞德", 18)
    val funcUnit = funcUnit(user)
    val funcString = user.funcUser(10)
    println(funcUnit)
    println(funcString)
}

对于User.()类型的形参,不仅可以使用funcUnit(user)的方式访问,也可以使用 user. funcUnit()的形式访问。 而在实参函数体里的情形,已经在 (T...)->R小节里讲述过了。这里不再累述。

返回值为函数的情况

返回值为函数的情况下,对于返回值的定义规范和形参一样:

fun uFun4():() -> Unit{
    return fun(){
        println(User("白瑞德", 18).name)
    }
}

使用方法如下:

uFun4()()
uFun4().invoke()

将字节码实现转换为Java后代码如下:

image.png

也是使用匿名内部类的方式实现的。

Kotlin内置高阶函数

kotlin为我们提供了很多内置高阶函数,如let、run、also、with等。都是以内联函数的形式定义在Standard.kt文件中。

image.png

相信大家看一下这些方法接收的参数,不难发现它们的参数类型基本上就是我们在上文中提到的。由此我们变不难掌握let、run、also、with的区别和用法。

  1. () -> R:形参要求是无参数的方法,可返回任意类型。而且无需显式的返回,默认返回值为最后一行的变量或表达式;如:

    val r: String = run { "" }
    val b: Boolean = run { 1==2 }
    
  2. (T)->R:形参要求是一个形参为T的方法,返回值为R类型。同样无需显式返回,默认返回最会一行的变量或表达式。并且在函数体内需要使用it.的方式来访问参数T实例的成员;其中(T)->Unit、(T)->Int和(T)->Boolean属于(T)->R,只是它们指明了具体的返回值类型而不是一个泛型。如:

    val letInt:Int = str.let {
           it.length
    }
    val takeString:String? = str.takeIf {
     		it.contains("l")
    }
    
  3. T.()->R:形参要求是一个形参为T的方法,返回值为R类型。同样无需显式返回,默认返回最会一行的变量或表达式。如果R为Unit类型,则返回的为调用者T。否则和(T)->R一样,无需显式返回,默认返回最会一行的变量或表达式。但是(T)->R不同的是它无需显式的指明T,即可直接访问T的成员,就像在T的类里面访问一样(不包括私有成员),和如:

    val strAlso:String = str.apply {
       	println(hashCode())
    }
    val with:Int = with(str) {
    	println(length)
    	0
    }
    

另外,with和report都的参数是(T,T.() -> R)的样式。意思是接收一个类型为 T 的参数和一个代码块。但实际使用除了调动方式不同外,基本没有什么异样。

最后,关于这些内置函数的返回值,它们的返回值类型取决于内联函数的返回值。这里仅看函数定义的返回值还不够。

public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

以apply和let为例:apply的返回值一个为this,也就是调用者本身;let的返回值会传入的函数的返回值。也就是block返回什么(代码块最后一行),它就返回什么:这就是这些函数有的返回调用者本身,有的返回传入的代码块的最后一行的原因。

总结这些内置的函数异同如下:

函数名函数块内的对象返回值
letit最后一行
withthis(无需显式调用)最后一行
runthis(无需显式调用)最后一行
alsoit调用者本身
applythis调用者本身

无需死记硬背它们的区别。如果弄懂了函数作为参数的,你很快就会理解这些内置函数的特点和作用。即使一时想不起,看一下它们的定义就一切都明了了。如果实在还是弄不懂,可以看更入门的文章# kotlin Standard中的内置内联高阶函数

inline和noinline

不知道你有没有发现我一直称let、run、also、with为内联高阶函数。为什么会加个内联呢?因为它们的函数定义如下:

public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

多了一个inline关键字。它有什么用呢?不妨让我们对比下下面的代码:

run {
   println("-----")
}
val func = fun() {
   println("-----")
}()

它们运行的效果一样。实现的功能也一样,但是,它们的字节码实现却大不相同。转换为Java实现如下:

image.png

明明上文中才说,高阶函数是通过匿名内部类实现的吗?这里怎么只有一个匿名内部类?

没错,这就是inline的作用。它不再是使用匿名内部类的方式,而是相当于直接把代码块"粘贴"到当前位置。这样可以通过内联的形式进行编译,减少对象的创建提高性能。

理解了inline,自然就能才到noinline的含义了:不要内联。可是,函数不都是默认不内联的吗?和inline不同的是,noinline不是作用于函数的,而是作用在函数的参数上的。那么它有什么效果呢——就是字面意思,关闭内联。???那直接不用inline不就好了吗?但是,思考一个问题,如果我有一个高阶函数,它的形参和返回值都是函数类型:

fun lineFun(num:Int,func: () -> Int): () -> Int {
    println("--------$num")
    return func
}

而此时,要将lineFun定义为内联函数:

image.png

很不幸,编译器报错了:func是非法的内联参数,需要添加nolinline去修饰它。这又是为什么呢?

还记得我们提过,内联函数并不是借助匿名内部类实现的。它更像是直接把代码粘贴过的,而且它内部的参数也不再是对象了。假如lineFun是个内联函数,我们这样调用它:

println("-----1")
val func = fun(): Int {
    println("-----2")
    return 0
}
lineFun(1, func)()

那么按照内联规则,编译后应该是下面的样子:

println("-----1")
println("-----2")
println("--------$1")
return ?????

没错,函数实参func也被内联了,那么它所要做就是将自己的函数体展开,也就是只需要贡献代码块并将它粘贴到调用出即可,那么它也不再是个对象了,那么返回值怎么办呢?这时候就要请出noinline了。当func被noinline标记后,它就不再参与内联,而是使用匿名内部类的方式参与其中。自然也就结局返回的问题了。