注册

Kotlin 中的高阶函数及其应用

前言


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

作者:equationl
链接:https://juejin.cn/post/7208129482095280189
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册