Kotlin高阶函数详解

高阶函数是Kotlin函数式编程的基石,各种开源框架的关键元素,掌握了高阶函数对一些框架的源代码更容易理解,对学习Jetpack Compose也变得得心应手。

了解高阶函数

可以先看View.java中点击事件的代码,分析下:

  • 首先,为了设置点击事件的监听,代码里特地定义了一个 OnClickListener 接口;
  • 接着,为了设置鼠标点击事件的监听,又专门定义了一个 OnContextClickListener 接口。

这里如果借助高阶函数,可以一个接口都不定义。

//View.java
//成员变量
private OnClickListenermOnClickListener;
private OnContextClickListenermOnContextClickListener;
//监听手指点击事件
public void setOnClickListener(OnClickListenerl){
  mOnClickListener=l;
}
//为传递这个点击事件,专门定义了一个接口
public interface OnClickListener{
voidon Click(Viewv);
}
//监听鼠标点击事件
public void setOnContextClickListener(OnContextClickListenerl){
  getListenerInfo().mOnContextClickListener=l;
}
//为传递这个鼠标点击事件,专门定义了一个接口
public interface OnContextClickListener{
  booleanonContextClick(Viewv);
}

看上段代码的Kotlin等价代码:

//View.kt
//                (View)->Unit就是「函数类型」         
//                      ↑     ↑    
var mOnClickListener:((View)->Unit)? = null
var mOnContextClickListener:((View)->Unit)? = null
//高阶函数
fun setOnClickListener(l:(View)->Unit){
    mOnClickListener=l;
}
//高阶函数
fun setOnContextClickListener(l:(View)->Unit){
    mOnContextClickListener=l;
}

通过对比,我们可以发现Kotlin引入高阶函数,实际分为两个部分:

  • 用函数类型替代接口定义;
  • 用 Lambda 表达式作为函数参数。

使用高阶函数好处

也带来了几个好处:一个是针对定义方,代码中减少了两个接口类的定义;另一个是对于调用方来说,代码也会更加简洁。就大大减少了代码量,提高了代码可读性,并通过减少类的数量,提高了代码的性能。


使用高阶函数对比

高阶函数的相关概念

函数类型

函数类型(Function Type)就是函数的类型
Kotlin中,函数是一等公民(first class),这意味着函数可以被存储在变量或者数据结构中,它是有类型的。Kotlin使用函数类型来描述一个函数的具体类型。一个完整语法的函数类型如下:

//(x:Int, y:Int) -> Int  这个就是 sum 函数的类型
//    ↑      ↑       ↑
fun sum(a: Int, b: Int):Int{
  return (a+b)
}

(Int, Int) ->Int 就代表了参数类型是两个 Int,返回值类型为 Int 的函数类型。

函数引用

函数的引用,普通的变量也有引用的概念,我们可以将一个变量赋值给另一个变量。而这一点,在函数上也是同样适用的,函数也有引用,并且也可以赋值给变量。如:作为参数传递给高阶函数。

//函数赋值给变量                  函数引用
//     ↑                            ↑
val function: (Int, Int) ->Int = ::sum

高阶函数

定义:高阶函数是将函数用作参数或返回值的函数。
其实Android里的点击事件监听用Kotlin来实现的话,它就是一个高阶函数。

//                    函数作为参数的高阶函数
//                              ↓
fun setOnClickListener(l: (View) -> Unit) { ... }

//                   返回值是函数类型的高阶函数
//                              ↓
fun higherFunction(): (Int, Int) -> Int { ... }

综上可以理解一个函数的参数或是返回值,它们当中有一个是函数的情况下,这个函数就是高阶函数。

Lambda

上面讲到函数的引用这种方法可以给函数类型赋值,也可以通过Lambda表达式对一个函数类型的变量进行赋值(大多数情况都是使用Lambda表达式来调用高阶函数)如下所示:

val function: (Int, Int) ->Int = {num1: Int, num2: Int -> num1 + num2}

Lambda 表达式语法结构:{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体} 函数体中可以编写任意行代码,最后一行代码会自动作为 Lambda 表达式的返回值

高阶函数的调用示例

这里用forEach函数来举例,forEach函数也是一个高阶函数。源码如下:

public inline fun IntArray.forEach(action: (Int) -> Unit): Unit {
    for (element in this) action(element)

intArray.forEach(?) //此处? 是个函数类型的参数,函数类型是 (Int) -> Unit
//函数类型的参数是可以定义相同的函数类型的变量传给forEach,如下:
val action: (Int) -> Unit = ??
fun main() {
    intArray.forEach(action)
}

上述我们讲到了函数赋值可以使用函数引用也可以使用 Lambda 表达式,使用函数引用代码如下:

val action: (Int) -> Unit = ::printValue

fun main() {
    intArray.forEach(action)
}

fun printValue(value: Int): Unit {
    println(value)
}

函数引用的方式调用高阶函数太过麻烦,还需要特地写一个函数来调用高阶函数。所以我们绝对多数情况都是使用Lambda 表达式来调用高阶函数的,且Lambda 表达式有很多简洁的写法。
使用Lambda 表达式来改写上述代码:

val action: (Int) -> Unit = {value: Int -> println(value)}

fun main() {
    intArray.forEach(action)
}

1、Kotlin 有类型推到机制,所以 Int 可以去掉

val action: (Int) -> Unit = {value -> println(value)}

2、Lambda 表达式如果只有一个参数,可以直接用 it 来代替,并且不需要声明参数名

val action: (Int) -> Unit = {println(it)}
//将简化后的代码代入,现在上述的代码就变成如下这样
fun main() {
    intArray.forEach({println(it)})
}

3、当Lambda 参数是函数的最后一个参数时,可以将 Lambda 表达式移到函数括号的外面。

fun main() {
    intArray.forEach(){
        println(it)
    }
}

4、Lambda 表达式是函数的唯一一个参数的话,还可以将函数的括号省略

fun main() {
    intArray.forEach{
        println(it)
    }
}

带接收者的函数类型

这里拿 Kotlin 的标准函数 apply函数和also函数来分析:

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

仔细⽐较这两个扩展函数会发现它们⾮常相似:它们都接收⼀个 block 函数并返回 this 值。
唯⼀的差别就在于apply的 block 函数类型为:T.() -> Unit,⽽also的 block 函数类型为:(T) -> Unit)

T.() -> Unit和(T) -> Unit) 区别

看下图apply和also使用可以得出:
also方法中接收到的是it,而在apply方法中接收到的是this

  • applythis值作为接收者调用指定的函数[block],并返回this值。

  • alsothis值作为参数调用指定的函数[block],并返回this值。

  • T.()->Unit 的函数体中可以直接使用T代表的对象,即用this代表对象
    (T) -> Unit 将T表示的对象作为实参通过函数参数传递进来,供函数体使用
    ()->Unit与T表示的对象没有直接联系,只能通过外部T实例的变量来访问对象

    apply和also使用区别

总结

  • 将函数的参数类型和返回值类型抽象出来后,我们就得到了函数类型。比如(View) ->Unit 就代表了参数类型是 View,返回值类型为 Unit 的函数类型。
  • 如果一个函数的“参数”或者“返回值”的类型是函数类型,那这个函数就是高阶函数。
  • Lambda 就是函数的一种简写,但存在运行时带来的额外内存开销,可以使用inline关键字来修饰函数,使其变为内联函数,可提升性能。

Kotlin官方的源代码Standard.Kt可以去分析其中的 with、let、also、takeIf、repeat、apply,来进一步加深对高阶函数的理解。还有Collections.Kt,可以去分析其中的 map、flatMap、fold、groupBy 等操作符,从而对高阶函数的应用场景有一个更具体的认知。

关键字 inline「内联函数」

Kotlin 中新增了「内联函数」,内联函数起初是在 C++ 里面的。当一个函数被内联 inline 标注后,在调用它的地方,会把这个函数方法体中的所以代码移动到调用的地方,而不是通过方法间压栈进栈的方式。

内联函数可以消除Lambda表达式运行时带来的额外内存开销


参考

朱涛 · Kotlin 编程第一课
Kotlin 高阶函数详解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容