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 影響程序流程。

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