前语

前段时间一直在面试,某次面试,面试官看着我的简历说:“看你写的你很了解 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 高阶函数

本文正在参与「金石方案」