kotlin lambda由浅入深

Lambda

本来是将lambda这块内容一起放在kotlin高级语法里面的,但是由于内容实在太多了,特意提出来单独写。

lambda表达式,简称为lambda,本质上就是可以传递给其他函数的一小段代码。原生Java语言在Java8的时候引入了lambda的概念,kotlin中进一步加深了对lambda的支持。

基础

意义

  • 在代码中存储和传递一小段行为是常有的任务,但是以往的Java中并不支持直接传递代码,因此我们通常会使用实现抽象类和接口的方式创建一个匿名内部类来完成代码的传递,例如:
button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            	log("onClick")
            }
        });

这时候我们会想,要是能够直接传递方法或者代码就好了,因此便有了lambda:

button.setOnClickListener {
	log("onClick")
}

看到这里可能很多人就懵逼了,就算是lambda为什么可以写成这样,其实是因为函数的参数中有且只有一个lambda表达式时,是可以省略普通函数中填写参数的()的。并且当lambda表达式本身没有形参或者没有被使用时也可以省略。所以完整写法其实应该是这样的:

fun Button.setOnClickListener((this) -> Unit)
button.setOnClickListener(){
  	view->log("onClick")
}

这样看着是不是就觉得正常多了。

  • 通过lambda我们将一段代码(行为)通过参数的形势注入给了方法,省去了以往定义和实现抽象类/接口的过程,而是用最直接有效的方式完成了操作。

语法

  • lambda本质上是一段行为,因此也有它固定的语法规则,在我们以往的习惯中任何行为都是由两部分组成:参数,结果。也就是形参返回类型,这这里我们用()来表示形参,通过->连接行为最后的返回值。

    (Int,Int) -> Int
    参数1	参数2	   返回/行为
    
  • 如前所述,一个lambda把一小段行为进行编码,你能把他当做值进行传递,它可以被独立地声明并存储到一个变量中。如下:

    val sum = { x:Int,y:Int -> x+y }
    or
    val sum:(Int,Int)->Int{x,y->x+y}//将声明行为独立出来,后面智能识别
    log("2+3=${sum(2,3)}")
    

    logcat输出:2+3=5

  • 上面演示了最简单的带参数和返回类型的行为,那么就需要传递一串代码块呢,和上述基本语法相同,其实传递代码块无非是一种特殊的行为,它特殊的地方在于不需要形参,也没有返回值。熟悉Kotlin的话你马上就能反应过来,没有返回值也就是Unit。因此如下:

        /**
         * 统一管理请求判断
         */
        private fun connect(operation: () -> Unit) =
                with(connection) {
                    try {
                        kotlin.run(operation)
                    } catch (e: Exception) {
                        when (e) {
                            is InterruptedIOException -> log("停止线程")
                            else ->  e.printStackTrace()
                        }
                    } finally {
                        disconnect()
                    }
                }
    

    如上通过operation: () -> Unit传入了一段代码块用operation表示,在方法体中通过kotlin.run(operation)方法来执行该代码块。这样就完成了一个简单的异常拉取以及判断,在很多需要判断异常的地方直接调用该方法并传入代码块就能实现预期的效果,极大的提高的效率和代码可读性,也抽离了异常判断进行统一管理。

提升

  • 上面讲的都是教你怎么用,而实际上你可能并没有理解他真正的表现形式,就像面向对象一样,是一个很抽象的概念,很多人用了多很年也没有彻底理解面向对象到底是个什么东西。拿我自己来说,刚开始的时候看着也是一脸懵逼,是的,确实能用了而且也没有什么大问题,但是让我说出为什么这样写的时候还是无法表达出来,这就是仅仅停留在会用层面上,接下来我会结合最近一段时间的使用来说明具体的理解。

  • 不同场景结构解析

    • 首先lambda是一种表达式,他像方法一样具有参数和返回(方法体)。这里为什么分别说具有参数、返回和方法体呢。因为lambda的表达式是(xxx)->xxxx这样的,它由两个部分组成,当它作为参数被定义的时候,它具有的是参数和返回,当它作为值被传入的时候,它具有的是参数和方法体。例如:

      val sum: (Int,Int) -> Int = { x, y -> x + y}
      

      这行代码严格说是包含了两段lambda表达式,(Int,Int) -> Int是作为参数的定义被使用的,所以包含的是参数和返回类型,x, y -> x + y是作为值来使用的,所以包含的是参数和方法体。

  • 用法进阶

    • lambda表达式主要有两种用法,一种用于方法传参,另外一种用于变量定义。前者很容易理解,但是后者很多人包括很多相关书籍也没有写出这样设计的目的是什么,只是单纯告诉了有这种用法。

      //方法传参
      button.setOnClickListener { view -> log("onClick") }
      //变量定义
      val sum: (Int,Int) -> Int = { x, y -> x + y}
      
    • 拿最简单的例子按钮点击事件监听来说明,在Java的时候当我们需要传递一串代码或者行为我们通过编写相应的接口并且实现来完成的。在有了lambda之后,我们可以更简单的直接编写一种lambda表达式来表示和传递行为,也就是可以看成匿名接口的一种简单实现。

      button.setOnClickListener(new View.OnClickListener() {
                  @Override
                  public void onClick(View v) {
                  	log("onClick")
                  }
              });
      button.setOnClickListener ({ view ->
      			log("onClick") 
      })
      
    • 那么,既然是接口,我们通常的直接将代码块传入lambda表达式就可以看成是匿名内部类的写法,相当于创建了一个实现了lambda表达式的匿名内部类。既然有匿名内部类,那么也可以让普通类去实现该lambda表达式。

    • 当将一个lambda表达式赋值给一个变量时,这时该变量充当的就相当于是lambda表达式的实现类对象。在需要重复使用该表达式时我们就可以将其提出单独声明,然后重复传入对应的方法。

      val clickEvent:(View)-> Unit = {view -> log("onClick")}
      button.setOnClickListener (clickEvent)
      button2.setOnClickListener(clickEvent)
      
    • 同时这样也让我们对变量式的lambda表达式有了更深了理解,例如之前很多人不能理解为什么变量里面的表达式参数不用赋值就能直接使用。现在再来理解就容易的多,因为变量式的表达式只是一种表达式的“实现类”,本身没有具体的值,只是定义了当拿到了具体的值后应该怎么做的行为规则

进阶

内联

  • 正如上面我们的将lambda的实现比喻成接口的实现,而实际上,在Java中本质上也就是将每个lambda表达式编译成了一个个的匿名内部类或其他类的对象来使用的。因此如果频繁的使用lambda则会创建出无数个对象,这会带来运行时额外的开销。
  • 针对这种情况,kotlin定义了一种新的关键字inline(内联)。简单来说就是被该关键字所修饰的函数所使用的lambda表达式不会被编译成新的对象,而是将表达式包含的真是代码替换到对应的函数中去,实现真正的代码块传递。
inline fun Boolean.isTrue(operation: () -> Unit) {
    if (this) {
        kotlin.run(operation)
    }
}

运行时泛型(reified)

  • 泛型是一种增加代码拓展性常见的使用方式。但是我们知道因为系统不知道泛型具体代表的是哪个类型,所以我们是不能直接在代码中获得该类型的属性的。这么说可能很抽象,下面举个例子:

    fun <T> getClassName(t: T) = t::class.java.name
    

    我们通过该方法直接获得某个类的名称,从逻辑上是没毛病的,但是因为系统不能确定这个类,所以不允许这种写法。

  • 在内联函数中,我们的函数会直接被编译成代码块放到对应的流程中去使用,因此其实这时候代码是知道传入的类型具体是什么东西的。所以以上的方法在内联函数中是可行的。这时只需要添加一个关键字reified

    inline fun <reified T> getClassName() = T::class.java.name
    
  • 有了运行时泛型后给了我们代码更多的可能性,以下是简单的应用之一:

    /**
     * Activity 跳转
     */
    inline fun <reified T> Activity.startActivity() {
        startActivity(Intent(this, T::class.java))
    }
    //跳转到NextActivity
    startActivity<NextActivity>()
    

注意事项

  • public inline 函数不能访问私有属性

  • 注意程序控制流
    当使用 inline 时,如果传递给 inline 函数的 lambda 有 return 语句,那么会导致闭包的调用者也返回。

    inline fun sum(a: Int, b: Int, lambda: (result: Int) -> Unit): Int {    
      val r = a + b    
      lambda.invoke(r)    
      return r
    }
    
    fun main(args: Array<String>) {
        println("Start")
        sum(1, 2) {
            println("Result is: $it")
            return // 这个会导致 main 函数 return
        }
        println("Done")
    }
    

    转换成JVM字节码,return之后的代码将不会执行。

    public static final void main(@NotNull String[] args) {   
      String var1 = "Start";   
      System.out.println(var1);   
      byte a$iv = 1;   
      int b$iv = 2;   
      int r$iv = a$iv + b$iv;   
      String var7 = "Result is: " + r$iv;   System.out.println(var7);
    }
    

    如何避免?

    使用 return@label 语法,返回到 lambda 被调用的地方。

    fun main(args: Array<String>) {
        println("Start")
        sum(1, 2) {
            println("Result is: $it")
            return@sum
        }
        println("Done")
    }
    
  • noinline

    当一个 inline 函数中,有多个 lambda 作为参数时,可以在不想内联的 lambda 前使用 noinline 声明。

  • crossinline

    声明一个 lambda 不能有 return 语句(可以有 return@label 语句)。这样可以避免使用 inline 时,lambda 中的 return 影响程序流程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章