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

原文地址:https://www.cnblogs.com/baiqiantao/p/6133527.html