Kotlin之lambda表达式

本文整理自:https://chiclaim.blog.csdn.net/article/details/85575213

一、什么是 lambda 表达式

我们先从 lambda 最基本的语法开始,引用一段 Kotlin in Action 中对 lambda 的定义:

总的来说,主要有 3 点:

1、lambda 总是放在一个花括号里 ({})
2、箭头左边是 lambda 参数 (lambda parameter)
3、箭头右边是 lambda 体 (lambda body)

我们再来看上面简单的 lambda 实例:
 

button.setOnClickListener{view -> //view是lambda参数
    //lambda体
    //todo something
}

二、lambda 表达式与 Java 的 functional interface

上面的 OnClickListener 接口和 Button 类是定义在 Java 中的。

该接口只有一个抽象方法,在 Java 中这样的接口被称作 functional interface 或 SAM (single abstract method)

因为我们在实际的工作中可能和 Java 定义的 API 打的交道最多了,因为 Java 这么多年的生态,我们无处不再使用 Java 库,

所以在 Kotlin 中,如果某个方法的参数是 Java 定义的 functional interface,Kotlin 支持把 lambda 当作参数进行传递的。

需要注意的是,Kotlin 这样做是指方便的和 Java 代码进行交互。但是如果在 Kotlin 中定义一个方法,它的参数类型是functional interface,是不允许直接将 lambda 当作参数进行传递的。如:
 

//在Kotlin中定义一个方法,参数类型是Java中的Runnable
//Runnable是一个functional interface
fun postDelay(runnable: Runnable) {
    runnable.run()
}

//把lambda当作参数传递是不允许的
postDelay{
   println("postDelay")
}

在 Kotlin 中调用 Java 方法,能够将 lambda 当作参数传递,需要满足两个条件:

1、该 Java 方法的参数类型是 functional interface (只有一个抽象方法)
2、该 functional interface 是 Java 定义的,如果是 Kotlin 定义的,就算该接口只有一个抽象方法,也是不行的
如果 Kotlin 定义了方法想要像上面一样,把 lambda 当做参数传递,可以使用高阶函数。这个后面会介绍。

Kotlin 允许 lambda 当作参数传递,底层也是通过构建匿名内部类来实现的:

fun main(args: Array<String>) {
    val button = Button()
    button.setOnClickListener {
        println("click 1")
    }

    button.setOnClickListener {
        println("click 2")
    }
}

//编译后对应的 Java 代码:

public final class FunctionalInterfaceTestKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      Button button = new Button();
      button.setOnClickListener((OnClickListener)null.INSTANCE);
      button.setOnClickListener((OnClickListener)null.INSTANCE);
   }
}

发现反编译后对应的 Java 代码有的地方可读性也不好,这是 Kotlin 插件的 bug,比如 (OnClickListener)null.INSTANCE

所以这个时候需要看下它的 class 字节码:

//内部类1
final class lambda/FunctionalInterfaceTestKt$main$1 implements lambda/Button$OnClickListener{
    public final static Llambda/FunctionalInterfaceTestKt$main$1; INSTANCE
    //...
}

//内部类2
final class lambda/FunctionalInterfaceTestKt$main$2 implements lambda/Button$OnClickListener{
    public final static Llambda/FunctionalInterfaceTestKt$main$2; INSTANCE
    //...
}

//main函数
  // access flags 0x19
  public final static main([Ljava/lang/String;)V
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 0
    LDC "args"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 10 L1
    NEW lambda/Button
    DUP
    INVOKESPECIAL lambda/Button.<init> ()V
    ASTORE 1
   L2
    LINENUMBER 11 L2
    ALOAD 1
    GETSTATIC lambda/FunctionalInterfaceTestKt$main$1.INSTANCE : Llambda/FunctionalInterfaceTestKt$main$1;
    CHECKCAST lambda/Button$OnClickListener
    INVOKEVIRTUAL lambda/Button.setOnClickListener (Llambda/Button$OnClickListener;)V

从中可以看出,它会新建 2 个内部类,内部类会暴露一个 INSTANCE 实例供外界使用。

也就是说传递 lambda 参数多少次,就会生成多少个内部类

但是不管这个 main 方法调用多少次,一个 setOnClickListener,都只会有一个内部类对象,因为暴露出来的 INSTANCE 是一个常量

我们再来调整一下 lambda 体内的实现方式:
 

fun main(args: Array<String>) {
    val button = Button()
    var count = 0
    button.setOnClickListener {
        println("click ${++count}")
    }

    button.setOnClickListener {
        println("click ${++count}")
    }
}

也就是 lambda 体里面使用了外部变量了,再来看下反编译后的 Java 代码:

public static final void main(@NotNull String[] args) {
  Intrinsics.checkParameterIsNotNull(args, "args");
  Button button = new Button();
  final IntRef count = new IntRef();
  count.element = 0;
  button.setOnClickListener((OnClickListener)(new OnClickListener() {
     public final void click() {
        StringBuilder var10000 = (new StringBuilder()).append("click ");
        IntRef var10001 = count;
        ++count.element;
        String var1 = var10000.append(var10001.element).toString();
        System.out.println(var1);
     }
  }));
  button.setOnClickListener((OnClickListener)(new OnClickListener() {
     public final void click() {
        StringBuilder var10000 = (new StringBuilder()).append("click ");
        IntRef var10001 = count;
        ++count.element;
        String var1 = var10000.append(var10001.element).toString();
        System.out.println(var1);
     }
  }));
}

由此,我们做一个小结:

1、一个 lambda 对应一个内部类

2、如果 lambda 体里没有使用外部变量,则调用方法时只会有一个内部类对象

3、如果 lambda 体里使用了外部变量,则每调用一次该方法都会新建一个内部类对象

三、lambda 表达式赋值给变量

 lambda 除了可以当作参数进行传递,还可以把 lambda 赋值给一个变量:

//定义一个 lambda,赋值给一个变量
val sum = { x: Int, y: Int, z: Int ->
    x + y + z
}

fun main(args: Array<String>) {
    //像调用方法一样调用lambda
    println(sum(12, 10, 15))
}

//控制台输出:37

接下来分析来其实现原理,反编译查看其对应的 Java 代码:

public final class LambdaToVariableTestKt {
   @NotNull
   private static final Function3 sum;

   @NotNull
   public static final Function3 getSum() {
      return sum;
   }

   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      int var1 = ((Number)sum.invoke(12, 10, 15)).intValue();
      System.out.println(var1);
   }

   static {
      sum = (Function3)null.INSTANCE;
   }
}

其对应的 Java 代码是看不到具体的细节的,而且还是会有 null.INSTANCE 的情况,但是我们还是可以看到主体逻辑。

但由 于class 字节篇幅很大,就不贴出来了,通过我们上面的分析,INSTANCE 是一个常量,在这里也是这样的:

首先会新建一个内部类,该内部类实现了接口 kotlin/jvm/functions/Function3,为什么是 Function3 因为我们定义的 lambda 只有 3 个参数。

所以 lambda 有几个参数对应的就是 Function 几,最多支持 22 个参数,也就是Function22。我们把这类接口称之为 FunctionN。

然后内部类实现了接口的 invoke 方法,invoke 方法体里的代码就是 lambda 体的代码逻辑。

这个内部类会暴露一个实例常量 INSTANCE,供外界使用。

如果把上面 Kotlin 的代码放到一个类里,然后在 lambda 体里使用外部的变量,那么每调用一次 sum 也会创建一个新的内部类对象,上面我们对 lambda 的小结在这里依然是有效的。

上面 setOnClickListener 的例子,我们传了两个 lambda 参数,生成了两个内部类,我们也可以把监听事件的 lambda 赋值给一个变量:
 

val button = Button()
val listener = Button.OnClickListener {
    println("click event")
}
button.setOnClickListener(listener)
button.setOnClickListener(listener)

这样对于 OnClickListener 接口,只会有一个内部类。

从这个例子中我们发现,className{} 这样的格式也能创建一个对象,这是因为接口 OnClickListener 是 SAM interface,只有一个抽象函数的接口。

编译器会生成一个 SAM constructor,这样便于把一个 lambda 表达式转化成一个 functional interface 实例对象。

至此,我们又学到了另一种创建对象的方法。

做一个小结,在 Kotlin 中常规的创建对象的方式(除了反射、序列化等):

1、类名后面接括号,格式:className()
2、创建内部类对象,格式:object : className
3、SAM constructor 方式,格式:className{}

四、高阶函数

由于高阶函数和 lambda 表达式联系比较紧密,在不介绍高阶函数的情况下,lambda 有些内容无法讲,所以在高阶函数这部分,再继续分析lambda表达式。

高阶函数的定义

如果某个函数是以另一个函数作为参数或者返回值是一个函数,我们把这样的函数称之为高阶函数

比如 Kotlin 库里的 filter 函数就是一个高阶函数:

//Kotlin library filter function
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> 

//调用高阶函数 filter,直接传递 lambda 表达式
list.filter { person ->
    person.age > 18
}

ilter 函数定义部分 predicate: (T) -> Boolean 格式有点像 lambda,但是又不是,传参的时候又可以传递 lambda 表达式

弄清这个问题之前,我们先来介绍下 function type,它格式如下:

名称 : (参数) -> 返回值类型

冒号左边是 function type 的名字
冒号右边是参数
尖括号右边是返回值
比如:predicate: (T) -> Boolean predicate 就是名字,T 泛型就是参数,Boolean 就是返回值类型

高阶函数是以另一个函数作为参数或者其返回值是一个函数,也可以说高阶函数参数是 function type 或者返回值是 function type

在调用高阶函数的时候,我们可以传递 lambda,这是因为编译器会把 lambda 推导成 function type

高阶函数原理分析

我们定义一个高阶函数到底定义了什么?我们先来定义一个简单的高阶函数:

fun process(x: Int, y: Int, operate: (Int, Int) -> Int) {
    println(operate(x, y))
}

编译后代码如下:

public static final void process(int x, int y, @NotNull Function2 operate) {
   Intrinsics.checkParameterIsNotNull(operate, "operate");
   int var3 = ((Number)operate.invoke(x, y)).intValue();
   System.out.println(var3);
}

我们又看到了 FunctionN 接口了,上面介绍把 lambda 赋值给一个变量的时候讲到了 FunctionN 接口

发现高阶函数的 function type 编译后也会变成 FunctionN,所以能把 lambda 作为参数传递给高阶函数也是情理之中了

这是一个高阶函数编译后的情况,我们再来看下调用高阶函数的情况:
 

//调用高阶函数,传递一个 lambda 作为参数
process(a, b) { x, y ->
    x * y
}

//编译后的字节码:
GETSTATIC higher_order_function/HigherOrderFuncKt$main$1.INSTANCE : Lhigher_order_function/HigherOrderFuncKt$main$1;
CHECKCAST kotlin/jvm/functions/Function2
INVOKESTATIC higher_order_function/HigherOrderFuncKt.process (IILkotlin/jvm/functions/Function2;)V

发现会生成一个内部类,然后获取该内部类实例,这个内部类实现了 FunctionN。介绍 lambda 的时候,我们说过了 lambda会编译成 FunctionN

如果 lambda 体里使用了外部变量,那每次调用都会创建一个内部类实例,而不是 INSTANCE 常量实例,这个也在介绍lambda 的时候说过了。

五、lambda 表达式参数和 function type 参数

除了 filter,还有常用的 forEach 也是高阶函数:

//list 里是 Person 集合
//遍历list集合
list.forEach {person -> 
    println(person.name)
}

我们调用 forEach 函数的传递 lambda 表达式,lambda 表达式的参数是 person,那为什么参数类型是集合里的元素 Person,而不是其他类型呢?比是集合类型?

到底是什么决定了我们调用高阶函数时传递的 lambda 表达式的参数是什么类型呢?

我们来看下 forEach 源码:
 

public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

发现里面对集合进行 for 循环,然后把集合元素作为参数传递给 action (function type)

所以,调用高阶函数时,lambda 参数是由 function type 的参数决定的

六、lambda与this

我们再看下 Kotlin 高阶函数 apply,它也是一个高阶函数,调用该函数时 lambda 参数是调用者本身 this

list.apply {//lambda 参数是 this,也就是 List
    println(this)
}

我们看下 apply 函数的定义:

public inline fun <T> T.apply(block: T.() -> Unit): T 

发现 apply 函数的的 function type 有点不一样,block: T.() -> Unit 在括号前面有个 T.

调用这样的高阶函数时,lambda 参数是 this,我们把这个 this 称之为 lambda receiver

把这类 lambda 称之为带有接受者的 lambda 表达式 (lambda with receiver)

这样的 lambda 在编写代码的时候提供了很多便利,调用所有关于 this 对象的方法 ,都不需要 this.,直接写方法即可,如下面的属于 StringBuilder 的 append 方法:

 

除了 apply,函数 with、run 的 lambda 参数都是 this

public inline fun <T> T.apply(block: T.() -> Unit): T
public inline fun <T, R> T.run(block: T.() -> R): R
public inline fun <T, R> with(receiver: T, block: T.() -> R): R

它们三者都能完成彼此的功能:

//apply
fun alphabet2() = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}
//with
fun alphabet() = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know alphabet!").toString()
}
//run
fun alphabet3() = StringBuilder().run {
    for (c in 'A'..'Z') {
        append(c)
    }
    append("\nNow I know the alphabet!")
}

高阶函数 let、with、apply、run 总结

1) let 函数一般用于判断是否为空

//let 函数的定义
public inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}

//let 的使用
message?.let { //lambda参数it是message
    val result = it.substring(1)
    println(result)
}

2) with 是全局函数,apply 是扩展函数,其他的都一样

3) run 函数的 lambda 是一个带有接受者的 lambda,而 let 不是,除此之外功能差不多

public inline fun <T, R> T.run(block: T.() -> R): R
public inline fun <T, R> T.let(block: (T) -> R): R

所以 let 能用于空判断,run 也可以:

七、高阶函数的优化

通过上面我们对高阶函数原理的分析:在调用高阶函数的时候 ,会生成一个内部类。

如果这个高阶函数被程序中很多地方调用了,那么就会有很多的内部类,那么程序员的体积就会变得不可控了。

而且如果调用高阶函数的时候,lambda 体里使用了外部变量,则会每次创建新的对象。

所以需要对高阶函数进行优化下。

上面我们在介绍 kotlin 内置的一些的高阶函数如 let、run、with、apply,它们都是内联函数,使用 inline 关键字修饰

内联 inline 是什么意思呢?就是在调用 inline 函数的地方,编译器在编译的时候会把内联函数的逻辑拷贝到调用的地方。

依然以在介绍高阶函数原理那节介绍的 process 函数为例:
 

//使用 inline 修饰高阶函数
inline fun process(x: Int, y: Int, operate: (Int, Int) -> Int) {
    println(operate(x, y))
}


fun main(args: Array<String>) {
    val a = 11
    val b = 2
    //调用 inline 的高阶函数
    process(a, b) { x, y ->
        x * y
    }
}

//编译后对应的 Java 代码:
public static final void main(@NotNull String[] args) {
    int a = 11;
    int b = 2;
    int var4 = a * b;
    System.out.println(var4);
}

结束

©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值