美文网首页Android KotlinKotlin
Kotlin高阶函数详解

Kotlin高阶函数详解

作者: 光锥外 | 来源:发表于2022-07-27 14:28 被阅读0次

    高阶函数是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 高阶函数详解

    相关文章

      网友评论

        本文标题:Kotlin高阶函数详解

        本文链接:https://www.haomeiwen.com/subject/aaaairtx.html