Kotlin 中的高阶函数及其应用

1,966 阅读4分钟

前言

前段时间一直在面试,某次面试,面试官看着我的简历说:“看你写的你很了解 kotlin 哦?那你说一说,为什么 kotlin 可以将函数作为参数和返回值,而 java 不行?”

我:“……”。

这次面试我连水都没喝一口就灰溜溜的走了。

回小黑屋的路上,突然想到,这玩意儿好像是叫 “高阶函数” 吧?好像我自己也经常用来着,咋就会啥也说不出来了呢?痛定思痛,赶紧恶补了一下相关的内容。

所以为什么 Kotlin 支持函数作为参数呢?

其实翻看 Kotlin 官方文档 《High-order functions and lambdas》 一节,就会发现它的第一段话就解释了为什么:

Kotlin functions are first-class, which means they can be stored in variables and data structures, and can be passed as arguments to and returned from other higher-order functions. You can perform any operations on functions that are possible for other non-function values.

To facilitate this, Kotlin, as a statically typed programming language, uses a family of function types to represent functions, and provides a set of specialized language constructs, such as lambda expressions.

因为在 Kotlin 中函数是头等公民,所以它们可以被储存在变量中、作为参数传递给其他函数或作为返回值从其他函数返回,就像操作其他普通变量一样操作高阶函数。

而 Kotlin 为了支持这个特性,定义了一系列的函数类型并且提供一些特定的语言结构支持(例如 lambdas 表达式)。

那么要怎么用呢?

高阶函数

首先,先看一段简单的代码:

fun getDataFromNet(onSuccess: (data: String) -> Unit) {
    val requestResult = "我是从网络请求拿到的数据"

    onSuccess(requestResult)
}

fun main() {
    getDataFromNet(
        onSuccess = {data: String ->
            println("获取到数据:$data")
        }
    )
}

运行代码,输出:

获取到数据:我是从网络请求拿到的数据

下面我们来解释一下这段代码是什么意思。

首先看 getDataFromNet 函数的参数 onSuccess ,嗯?这是个什么东西?

哈哈,看起来可能会觉得有点奇怪,其实这里的 onSuccess 也是一个函数,且带有参数 data: String

大致可以理解成:

fun onSuccess(data: String) {
    // TODO
}

这么一个函数,不过实际上这个函数是并不叫 onSuccess ,我们是只把这个函数赋值给了变量 onSuccess

从上面简单例子,我们可以看出,如果要声明一个个高阶函数,那么我们需要使用形如:

(arg1: String, arg2: Int) -> Unit

的函数类型来声明高阶函数。

基本形式就是一个括号 () + -> + Unit

其中,() 内可以像普通函数一样声明接收的参数,如果不接收任何参数则可以只写括号:

() -> Unit

箭头则是固定表达式,不可省略。

最后的 Unit 表示这个函数返回的类型,不同于普通函数,返回类型不可省略,即使不返回任何数据也必须明确声明 Unit

当一个普通函数接收到一个作为参数的高阶函数时,可以通过 变量名()变量名.invoke() 调用:

fun getDataFromNet(onSuccess: (data: String) -> Unit, onFail: () -> Unit) {
    val requestResult = "我是从网络请求拿到的数据"

    // 调用名为 onSuccess 的高阶函数
    onSuccess.invoke(requestResult)
    // 也可以直接通过括号调用
    onSuccess(requestResult)

    // 调用名为 onFail 的高阶函数
    onFail.invoke()
    // 也可以直接通过括号调用
    onFail()
}

下面再看一个有返回值的高阶函数的例子:

fun getDataFromNet(getUrl: (type: Int) -> String) {
    val url = getUrl(1)
    println(url)
}

fun main() {
    getDataFromNet(
        getUrl = {type: Int ->
            when (type) {
                0 -> "Url0"
                1 -> "Url1"
                else -> "Err"
            }
        }
    )
}

上面的代码会输出:

Url1

将高阶函数作为函数返回值或者赋值给变量其实和上面大差不差,只要把一般用法中的返回值和赋值内容换成 函数类型 表示的高阶函数即可:

fun funWithFunReturn(): () -> Unit {
    val returnValue: () -> Unit = { }
    
    return returnValue
}

在实例化高阶函数时,高阶函数的参数需要使用形如 arg1: String , arg2: Int -> 的形式表示,例如:

fun getDataFromNet(onSuccess: (arg1: String, arg2: Int) -> Unit) {
    // do something
}

fun main() {
    getDataFromNet(
        onSuccess = { arg1: String, arg2: Int ->  
            println(arg1)
            println(arg2)
        }
    )
}

注意,这里的参数名不一定要和函数中定义的一样,可以自己写。

如果参数类型可以推导出来,则可以不用声明类型:

fun getDataFromNet(onSuccess: (arg1: String, arg2: Int) -> Unit) {
    // do something
}

fun main() {
    getDataFromNet(
        onSuccess = { a1, a2 ->
            println(a1)
            println(a2)
        }
    )
}

同时,如果某些参数没有使用到的话,可以使用 _ 下划线代替:

fun getDataFromNet(onSuccess: (arg1: String, arg2: Int) -> Unit) {
    // do something
}

fun main() {
    getDataFromNet(
        onSuccess = { a1, _ ->
            println(a1)
        }
    )
}

用 lambda 表达式简化一下

在上面我们举例的代码中,为了更好理解,我们没有使用 lambda 表达式简化代码。

在实际使用过程中,我们可以大量的使用 lambda 表达式来大大减少代码量,例如:

fun getDataFromNet(onSuccess: (data: String) -> Unit, onFail: () -> Unit) {
    val requestResult = "我是从网络请求拿到的数据"
    
    if (requestResult.isNotBlank()) {
        onSuccess(requestResult)
    }
    else {
        onFail()
    }

}

fun main() {
    getDataFromNet(
        onSuccess = {data: String ->
            println("获取到数据:$data")
        },
        onFail = {
            println("获取失败")
        }
    )
}

可以简化成:

fun main() {
    getDataFromNet(
        {
            println("获取到数据:$it")
        },
        {
            println("获取失败")
        }
    )
}

可以看到,如果高阶函数的参数只有一个的话,可以不用显式声明,默认使用 it 表示。

同时,如果普通函数的参数只有一个高阶函数,且位于最右边,则可以直接提出来,不用写在括号内,并将括号省略:

fun getDataFromNet(onSuccess: (data: String) -> Unit) {
    // do something
}

fun main() {
    // 这里调用时省略了 () 
    getDataFromNet { 
        println(it)
    }
}

即使同时有多个参数也不影响把最右边的提出来,只是此时 () 不能省略:

fun getDataFromNet(arg: String, onSuccess: (data: String) -> Unit) {
    // do something
}

fun main() {
    getDataFromNet("123") {
        println(it)
    }
}

关于使用 lambda 后能把代码简化到什么程度,可以看看这篇文章举的安卓中的点击事件监听的例子

从最初的

image.setOnClickListener(object: View.OnClickListener {
    override fun onClick(v: View?) {
        gotoPreview(v)
    }
})

简化到只有一行:

image.setOnClickListener { gotoPreview(it) }

所以它有什么用?

更简洁的回调

在上文中,我们举了使用 lambda 表达式后可以把点击事件监听省略到只有一行的程度,但是这里仅仅只是使用。

众所周知,安卓中写事件监听的代码需要一大串:

public interface OnClickListener {
    void onClick(View v);
}

public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

如果我们使用高阶函数配合 lambda 则只需要:

var mOnClickListener: ((View) -> Unit)? = null

fun setOnClickListener(l: (View) -> Unit) {
    mOnClickListener = l;
}

调用时也只需要:

setOnClickListener {
    // do something 
}

其实,我们最开始举的封装网络请求的例子就是一个很好的事例,如果不使用高阶函数,那么我们为了实现网络请求成功后的回调,还得额外多写一些接口类,得益于高阶函数,我们只需要这样即可:

fun getDataFromNet(onSuccess: (data: String) -> Unit) {
    val requestResult = "我是从网络请求拿到的数据"

    onSuccess(requestResult)
}

fun main() {
    getDataFromNet {
         println("获取到数据:$it")
    }
}

让函数更加多样

有时候,我们可能会有一些比较特殊的需要多重校验的嵌套场景,如果不使用高阶函数的话,可能需要这样写:

fun checkName(data: String): Boolean {
    return true
}

fun checkAge(data: String): Boolean {
    return true
}

fun checkGender(data: String): Boolean {
    return true
}

fun checkId(data: String): Boolean {
    return true
}

fun postData(data: String) {

}

fun main() {
    val mockData = ""

    if (checkName(mockData)) {
        if (checkAge(mockData)) {
            if (checkGender(mockData)) {
                if (checkId(mockData)) {
                    postData(mockData)
                }
            }
        }
    }
}

如果使用高阶函数,则可以这么写:

fun checkName(data: String, block: (data: String) -> Unit) {
    // if true
    block(data)
}

fun checkAge(data: String, block: (data: String) -> Unit) {
    // if true
    block(data)
}

fun checkGender(data: String, block: (data: String) -> Unit) {
    // if true
    block(data)
}

fun checkId(data: String, block: (data: String) -> Unit) {
    // if true
    block(data)
}

fun postData(data: String) {

}

fun main() {
    val mockData = ""

    checkName(mockData) {
        checkAge(it) {
            checkGender(it) {
                checkId(it) {
                    postData(it)
                }
            }
        }
    }
}

额……好像举的这个例子不太恰当,但是大概就是这么个意思。

更好的控制函数执行

在我写的项目中还有一个比上面一个更加奇怪的需求。

这个程序有个后台进程一直在分别请求多个状态,且每个状态返回的数据类型都不同,我们需要分别将这些状态全部请求完成后打包成一个单独的数据,而且这些状态可能并不需要全部都请求,需要根据情况实时调整请求哪些状态,更烦的是,会有一个停止状态,如果收到停止的指令,我们必须立即停止请求,所以不能等待所有请求完成后再停止,必须要立即停止当前所在的请求。如果直接写你会怎么写?

听见都头大了是吧,但是这个就是我之前写工控程序时经常会遇到的问题,需要有一个后台进程实时轮询不同从站的不同数据,并且由于串口通信的特性,如果此时有新的指令需要下发,必须立即停止轮训,优先下发新指令。

所以我是这样写的:

fun getAllStatus(needRequestList: List<Any>, isFromPoll: Boolean = false): StatusData {

    val fun0: () -> ResponseData.Status1 = { syncGetStatus1() }
    val fun1: () -> ResponseData.Status2 = { syncGetStatus2() }
    val fun2: () -> ResponseData.Status3 = { syncGetStatus3() }
    val fun3: () -> ResponseData.Status4 = { syncGetStatus4() }
    val fun4: () -> ResponseData.Status5 = { syncGetStatus5() }
    val fun5: () -> ResponseData.Status6 = { syncGetStatus6() }
    val fun6: () -> ResponseData.Status7 = { syncGetStatus7() }
    val fun7: () -> Int = { syncGetStatus8() }
    val fun8: () -> Int = { syncGetStatus9() }

    val funArray = arrayOf(
        fun0, fun1, fun2, fun3, fun4, fun5, fun6, fun7, fun8
    )

    val resultArray = arrayListOf<Any>()

    for (funItem in funArray) {
        if (isFromPoll && (isPauseNwPoll() || !KeepPolling)) throw IllegalStateException("轮询被中断")
        if (funItem in needRequestList) resultArray.add(funItem.invoke())
    }
    
    // 后面的省略
}

可以看到,我们把需要请求的函数全部作为高阶函数存进 funArray 数组中,然后遍历这个数组开始挨个执行,并且每次执行时都要判断这个请求是否需要执行,以及当前是否被中断请求。

得益于高阶函数的特性,我们可以方便的控制每个函数的执行时机。

总结

因为我讲的比较浅显,读者可能看起来会一头雾水,所以这里推荐结合下面列出的参考资料的文章一起看,同时自己上手敲一敲,就能很好的理解了。

参考资料

  1. High-order functions and lambdas
  2. 头等函数
  3. Kotlin Jetpack 实战 | 04. Kotlin 高阶函数

本文正在参加「金石计划」