zoukankan      html  css  js  c++  java
  • Kotlin 朱涛7 高阶函数 函数类型 Lambda SAM

    本文地址


    目录

    07 | 高阶函数:为什么说函数是一等公民

    • 原文
    • StandardKt:分析其中的 with、let、also、takeIf、repeat、apply 等操作符
    • CollectionsKt:分析其中的 map、flatMap、fold、groupBy 等操作符

    对 C/Java 开发者来首,高阶函数是一个全新的概念,很难从经典的 C/Java 里找到同等的概念迁移过来。

    高阶函数

    高阶函数是 Kotlin 函数式编程的基石,是理解协程源码的基础,对学习 Jetpack Compose 也大有帮助,同时也是各种开源框架的关键元素。在特定业务场景下,我们也可以用它来实现自己的 DSL (Domain Specific Language)。

    • 高阶函数:High-order functions,是指使用函数作为参数或返回值的函数
    • 函数类型:Function Type,是对函数的参数类型返回值类型的抽象

    函数类型

    在 Kotlin 的世界里,函数是一等公民。类、变量有类型,一等公民函数当然也有类型。

    // 函数类型 (Int,  Int) -> Float
    //          ↑      ↑       ↑
    fun add(a: Int, b: Int): Float { return (a+b).toFloat() }
    

    上面的代码中,(Int, Int) -> Float 就是 add 函数的类型,它代表了参数类型是两个 Int、返回值类型为 Float 的函数类型。

    变量有引用、赋值的概念,一等公民函数当然也有。

    val function: (Int, Int) -> Float = ::add // 将函数赋值给变量
    

    上面的代码中,::add 就代表函数 add 的引用。

    SAM 与 Lambda

    • SAM:Single Abstract Method,代表只有一个抽象方法的接口
    • Lambda:可以理解为是函数的简写。符合一定条件的函数,就可以使用 Lambda 表达式来简写

    符合 SAM 要求的接口,就能被编译器进行 SAM 转换,也就可以使用 Lambda 表达式来简写。

    注意,Java 8 中的 SAM 叫做函数式接口(FunctionalInterface),限制条件如下:

    • 必须是接口,抽象类不行
    • 该接口有且仅有一个抽象的方法(有默认实现的方法不考虑)
    public void setOnClickListener(OnClickListener l)   // 方法声明
    
    fun mOnClick(v: View): Unit = println(v.toString()) // 自定义的一个函数
    view.setOnClickListener(::mOnClick) // 引用 mOnClick 函数,当做函数的参数
    
    // 接口 OnClickListener 符合 SAM 转换的条件,所以可以用 Lambda 表达式替代函数引用
    view.setOnClickListener { v: View -> println(v.toString()) }
    

    这里有一个问题:Android 并没有提供 View.java 的 Kotlin 实现,为什么我们可以用 Lambda 来简化事件监听呢?

    因为,虽然 View.java 是 Java 代码,但 Kotlin 编译器知道它的参数 OnClickListener 符合 SAM 转换的条件,所以会自动做以下转换。

    public void setOnClickListener(OnClickListener l) // 转换前的 Java 代码
    fun setOnClickListener(l: ((View!) -> Unit)?)     // 转换后的 Kotlin 代码
    

    高阶函数实现原理

    Kotlin 引入的高阶函数,在编译后,其实被转换成了 匿名内部类

    class View {
        fun setOnClickListener(onlick: (View) -> Unit) = onlick(this) // 调用传入的方法
    }
    
    fun main() {
        fun mOnClick(v: View): Unit = println(v.toString()) // 定义一个方法
        View().setOnClickListener(::mOnClick)               // 引用一个方法
    }
    

    反编译成 Java 后的代码如下:

    public final class View {
       public final void setOnClickListener(Function1 onlick) { // 函数类型被转换成了一个接口
          onlick.invoke(this); // 调用接口实例的方法
       }
    }
    
    // 和之前遇到过的情况一样,反编译成 Java 后代码语法错误
    public static final void main() {
       <undefinedtype> $fun$mOnClick$1 = null.INSTANCE;           // 创建了某个匿名内部类的实例
       (new View()).setOnClickListener((Function1)null.INSTANCE); // 传入的也是匿名内部类的实例
    }
    
    // 我们只能从字节码中寻找蛛丝马迹,和之前遇到过的情况一样,这里也是定义了一些匿名内部类
    // final class MainKt$main$1 extends Lambda implements Function1
    // final class MainKt$main$2 extends FunctionReferenceImpl implements Function1
    
    // A function that takes 1 argument,预先定义的一系列接口,有 x 个参数就用 Functionx
    public interface Function1<in P1, out R> : Function<R> {
        public operator fun invoke(p1: P1): R // Invokes the function with the specified argument
    }
    
    // Represents a value of a functional type,代表函数类型,接口未定义任何方法
    public interface Function<out R> // R represent return type of the function
    

    Kotlin 弄了个这么高端的高阶函数,最终还是以匿名内部类的形式在运行,那它的性能如何?

    Kotlin 高阶函数的性能,在某些情况下可以达到匿名内部类的 100 倍!具体原理后面再详细探讨。

    带接收者的函数类型

    使用 apply 函数

    使用 Kotlin 的标准函数 apply 可以简化代码。

    data class User(var name: String, var text: String)
    
    if (user != null) {
        println(user.name + user.blog) // 显示调用 user
        image.setOnClickListener { gotoPreview(user) }
    }
    
    // 使用 apply 简化上面的代码
    user?.apply {
        println(this.name + blog) // 可以省略 user 的调用
        image.setOnClickListener { gotoPreview(this) } // 用 this 代替
    }
    
    • apply 肯定是个函数,只是 () 被省略了,完整格式应该是:user?.apply() {...}
    • apply 的参数中肯定有一个 Lambda 表达式,其中参数名是 this,其代表了 user

    自定义 apply 函数

    现在,我们尝试自己实现这个 apply 方法:

    // 形参不允许被命名为 this (Kotlin 的语言设计者可以),因此我这里用的是 self
    fun User.apply2(self: User, block: (self: User) -> Unit): User{
        block(self) // 调用这个函数
        return this // 返回值
    }
    
    user?.apply2(self = user) { self: User -> // 选用传入 self
        println(self.name + self.blog)        // 必须通过 self 访问成员
        image.setOnClickListener { gotoPreview(this) }
    }
    

    可以看到,和 Kotlin 的标准函数 apply 相比,我们反推实现的 apply2 用起来比较繁琐。

    简化 apply 函数

    使用 带接收者的函数类型,可以简化上面 apply 函数的定义:

    // fun User.apply2(self: User, block: (self: User) -> Unit): User{}
    //  User.apply2(block: (self: User) -> Unit): User
    fun User.apply3(block: User.() -> Unit): User{ // 【(self: User)】【User.()】
        block() //  不用再传this
        return this
    }
    
    user?.apply3 {
        println(this.name + blog) // 可以省略 user 的调用
        image.setOnClickListener { gotoPreview(this) } // 用 this 代替
    }
    

    定义为成员方法

    上面的 apply3 方法,看起来就像是在 User 里,增加了一个成员方法一样。

    data class User(var name: String, var blog: String) {
        fun apply4() {
            println(this.name + blog) // 成员方法可以通过 this 访问成员变量
            image.setOnClickListener { gotoPreview(this) }
        }
    }
    
    user?.apply4()
    

    总结

    从外表上看,带接收者的函数类型,就 等价于成员方法。但从本质上讲,它仍是通过编译器注入 this 来实现的。

    带接收者的函数类型也算是一种扩展函数,从语法层面讲,他们都相当于成员方法

    Lambda 表达式引发的 8 种写法

    ① object 匿名内部类

    这是原始代码,它的本质是用 object 关键字定义了一个匿名内部类

    image.setOnClickListener(object: View.OnClickListener { // 匿名内部类
        override fun onClick(v: View?) {
            gotoPreview(v)
        }
    })
    

    ② Lambda 表达式

    在这种情况下,object 关键字可以被省略。这时候它在语法层面就不再是匿名内部类了,它其实已经变成 Lambda 表达式了,因此它里面 override 的方法也要跟着删掉:

    image.setOnClickListener(View.OnClickListener { v: View? -> // Lambda 表达式
        gotoPreview(v)
    })
    

    上面的 View.OnClickListener 被称为 SAM Constructor,它是编译器为我们生成的。

    ③ 省略 SAM 构造器

    由于 Kotlin 的 Lambda 表达式是不需要 SAM Constructor 的,所以它也可以被删掉:

    image.setOnClickListener({ v: View? -> // 省略 SAM Constructor
        gotoPreview(v)
    })
    

    ④ 自动类型推导

    由于 Kotlin 支持类型推导,所以 View 可以被删掉:

    image.setOnClickListener({ v -> // 省略类型 View
        gotoPreview(v)
    })
    

    ⑤ 单一参数写成 it

    当 Kotlin Lambda 表达式只有一个参数的时候,它可以被写成 it:

    image.setOnClickListener({ it -> // 参数写成 it
        gotoPreview(it)
    })
    

    ⑥ 省略 Lambda 的 it

    Kotlin Lambda 的 it 是可以被省略的:

    image.setOnClickListener({ // 省略 it
        gotoPreview(it)
    })
    

    ⑦ 移动 Lambda 位置

    当 Kotlin Lambda 作为函数的 最后一个参数 时,Lambda 可以被挪到外面:

    image.setOnClickListener() { // 移动 Lambda 位置
        gotoPreview(it)
    }
    

    ⑧ 省略小括号

    当 Kotlin 只有一个 Lambda 作为函数参数时,() 可以被省略:

    image.setOnClickListener { // 省略小括号
        gotoPreview(it)
    }
    

    演进过程

    这 8 种写法的演进过程:

    小结

    • 为什么引入高阶函数:为了简化
    • 高阶函数是什么:函数作为参数 or 返回值
    • 函数类型是什么:函数的类型
    • 函数引用是什么:类比变量的引用
    • Lambda 是什么:可以简单理解为函数的简写
    • 带接收者的函数类型是什么:可以简单理解为成员函数的类型

    2016-12-05

  • 相关阅读:
    从程序员到项目经理(14):项目管理三大目标
    从程序员到项目经理(13):项目经理必须懂一点“章法”
    从程序员到项目经理(12):如何管理自己的时间(下)
    从程序员到项目经理(11):如何管理自己的时间(上)
    从程序员到项目经理(10):每个人都是管理者
    从程序员到项目经理(9):程序员加油站 --要执着但不要固执
    从程序员到项目经理(8):程序员加油站 -- 再牛也要合群
    从程序员到项目经理(6):程序员加油站 -- 完美主义也是一种错
    从程序员到项目经理(7):程序员加油站 -- 不要死于直率
    [SQL]不知道
  • 原文地址:https://www.cnblogs.com/baiqiantao/p/6133527.html
Copyright © 2011-2022 走看看