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
数组中,然后遍历这个数组开始挨个执行,并且每次执行时都要判断这个请求是否需要执行,以及当前是否被中断请求。
得益于高阶函数的特性,我们可以方便的控制每个函数的执行时机。
总结
因为我讲的比较浅显,读者可能看起来会一头雾水,所以这里推荐结合下面列出的参考资料的文章一起看,同时自己上手敲一敲,就能很好的理解了。
参考资料
链接:https://juejin.cn/post/7208129482095280189
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。