細說 Lambda 表達式

原文鏈接:https://github.com/enbandari/Kotlin-Tutorials

1. 什麼是 Lambda 表達式

Lambda 表達式,其實就是匿名函數。而函數其實就是功能(function),匿名函數,就是匿名的功能代碼了。在 Kotlin 當中,函數也是作爲類型的一種出現的,儘管在當前的版本中,函數類型的靈活性還不如 Python 這樣的語言,不過它也是可以被賦值和傳遞的,這主要就體現在 Lambda 表達式上。

我們先來看一個 Lambda 表達式的例子:

fun main(args: Array<String>) { 
    val lambda = { 
        left: Int, right: Int 
        -> 
        left + right 
    } 
    println(lambda(2, 3)) 
} 

大家可以看到我們定義了一個變量 lambda,賦值爲一個 Lambda 表達式。Lambda 表達式用一對大括號括起來,後面先依次寫下參數及其類型,如果沒有就不寫,接着寫下 -> ,這表明後面的是函數體了,函數體的最後一句的表達式結果就是 Lambda 表達式的返回值,比如這裏的返回值就是參數求和的結果。

後面我們用 () 的形式調用這個 Lambda 表達式,其實這個 () 對應的是 invoke 方法,換句話說,我們在這裏也可以這麼寫:

println(lambda.invoke(2,3)) 

這兩種調用的寫法是完全等價的。

毫無疑問,這段代碼的輸出應該是 5。

2. 簡化 Lambda 表達式

我們再來看個例子:

fun main(args: Array<String>) { 
    args.forEach { 
        if(it == "q") return 
        println(it) 
    } 
    println("The End") 
} 

args 是一個數組,我們已經見過 for 循環迭代數組的例子,不過我們其實有更現代化的手段來迭代一個數組,比如上面這個例子。這沒什麼可怕的,一旦撕下它的面具,你就會發現你早就認識它了:

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

這是一個擴展方法,擴展方法很容易理解,原有類沒有這個方法,我們在外部給它擴展一個新的方法,這個新的方法就是擴展方法。大家都把它當做 Array 自己定義的方法就好,我們看到裏面其實就是一個 for 循環對吧,for 循環幹了什麼呢?調用了我們傳入的Lambda表達式,並傳入了每個元素作爲參數。所以我們調用 forEach 方法時應該怎麼寫呢?

args.forEach({ 
    element -> println(element) 
}) 

這相當於什麼呢?

for(element in args){ 
    println(element) 
} 

很容易理解吧?

接着,Kotlin 允許我們把函數的最後一個Lambda表達式參數移除括號外,也就是說,我們可以改下上面的 forEach 的寫法:

args.forEach(){ 
    element -> println(element) 
} 

看上去有點兒像函數定義了,不過區別還是很明顯的。這時候千萬不能暈了,暈了的話我這兒有暈車藥喫點兒吧。

事兒還沒完呢,如果函數只有這麼一個 Lambda 表達式參數,前面那個不就是麼,剩下一個小括號也沒什麼用,乾脆也丟掉吧:

args.forEach{ 
    element -> println(element) 
} 

大家還好吧?你以爲這就結束了?nonono,如果傳入的這個Lambda表達式只有一個參數,還是比如上面這位 forEach,參數只有一個 element ,於是我們也可以在調用的時候省略他,並且默認它叫 it,說得好有道理,它不就是 it 麼,雖然人家其實是 iterator 的意思:

args.forEach{ 
     println(it) 
} 

嗯,差不多了。完了沒,沒有。還有完沒啊?就剩這一個了。如果這個 Lambda 表達式裏面只有一個函數調用,並且這個函數的參數也是這個Lambda表達式的參數,那麼你還可以用函數引用的方式簡化上面的代碼:

args.forEach(::println) 

這有沒有點兒像 C 裏面的函數指針?函數也是對象嘛,沒什麼大驚小怪的,只要實參比如 println 的入參和返回值與形參要求一致,那麼就可以這麼簡化。

總結一下:

  1. 最後一個Lambda可以移出去
  2. 只有一個Lambda,小括號可省略
  3. Lambda 只有一個參數可默認爲 it
  4. 入參、返回值與形參一致的函數可以用函數引用的方式作爲實參傳入

這樣我們之前給的那個例子就大致能夠看懂了吧:

fun main(args: Array<String>) { 
    args.forEach { 
        if(it == "q") return 
        println(it) 
    } 
    println("The End") 
} 

3. 從 Lambda 中返回

真看懂了嗎?假設我輸入的參數是

o p q r s t 

你知道輸出什麼嗎?

o 
p 
The End 

對嗎?

不對,return 會直接結束 main 函數。爲啥?Lambda 表達式,是個表達式啊,雖然看上去像函數,功能上也像函數,可它看起來也不過是個代碼塊罷了。這就像琅琊榜前期,靖王雖然獲得了自由進宮拜見母妃的特權,但他當時並不是親王,而只是一個郡王一樣。

那,就沒辦法 return 了嗎?當然不是,兵來將擋水來土掩:

fun main(args: Array<String>) { 
    args.forEach forEachBlock@{ 
        if(it == "q") return@forEachBlock 
        println(it) 
    } 
    println("The End") 
} 

定義一個標籤就可以了。你還可以在 return@forEachBlock 後面加上你的返回值,如果需要的話。

4. Lambda 表達式的類型

好,前面說到 Lambda 表達式其實是函數類型,我們在前面的 forEach 方法中傳入的 Lambda 表達式其實就是 forEach 方法的一個參數,我們再來看下 forEach 的定義:

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

注意到,action 這個形參的類型是 (T) -> Unit,這個是 Lambda 表達式的類型,或者說函數的類型,它表示這個函數接受一個 T 類型的參數,返回一個 Unit 類型的結果。我們再來看幾個例子:

() -> Int //無參,返回 Int  
(Int, Int) -> String //兩個整型參數,返回字符串類型 
(()->Unit, Int) -> Unit //傳入了一個 Lambda 表達式和一個整型,返回 Unit 

我們平時就用這樣的形式來表示 Lambda 表達式的類型的。有人可能會說,既然人家都是類型了,怎麼就沒有個名字呢?或者說,它對應的是哪個類呢?

public interface Function<out R> 

其實所有的 Lambda 表達式都是 Function 的實現,這時候如果你問我,那 invoke 方法呢?在哪兒定義的?說出來你還真別覺得搞笑,Kotlin 的開發人員給我們定義了 23 個 Function 的子接口,其中 FunctionN 表示 invoke 方法有 n 個參數。。

public interface Function0<out R> : Function<R> { 
    public operator fun invoke(): R 
} 
public interface Function1<in P1, out R> : Function<R> { 
    public operator fun invoke(p1: P1): R 
} 
... 
public interface Function22<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, in P11, in P12, in P13, in P14, in P15, in P16, in P17, in P18, in P19, in P20, in P21, in P22, out R> : Function<R> { 
    public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10, p11: P11, p12: P12, p13: P13, p14: P14, p15: P15, p16: P16, p17: P17, p18: P18, p19: P19, p20: P20, p21: P21, p22: P22): R 
} 

說實在的,第一看到這個的時候,我直接笑噴了,Kotlin 的開發人員還真是黑色幽默啊。

這事兒不能這麼完了,萬一我真有一個函數,參數超過了 22 個,難道 Kotlin 就不支持了嗎?

fun hello2(action: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) -> Unit) { 
    action(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22) 
} 

於是我們定義一個參數有 23 個的 Lambda 表達式,調用方法也比較粗暴:

hello2 { i0, i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16, i17, i18, i19, i20, i21, i22 -> 
    println("$i0, $i1, $i2, $i3, $i4, $i5, $i6, $i7, $i8, $i9, $i10, $i11, $i12, $i13, $i14, $i15, $i16, $i17, $i18, $i19, $i20, $i21, $i22,") 
} 

編譯運行結果:

Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/Function23 
    at java.lang.Class.getDeclaredMethods0(Native Method) 
    at java.lang.Class.privateGetDeclaredMethods(Class.java:2701) 
    at java.lang.Class.privateGetMethodRecursive(Class.java:3048) 
    at java.lang.Class.getMethod0(Class.java:3018) 

果然,雖然這個參數有 23 個的 Lambda 表達式被映射成 kotlin/Function23 這個類,不過,這個類卻不存在,也就是說,對於超過 22 個參數的 Lambda 表達式,Kotlin 代碼可以編譯通過,但會拋運行時異常。這當然也不是個什麼事兒了,畢竟有誰腦殘到參數需要 22 個以上呢?

5. SAM 轉換

看名字挺高大上,用起來炒雞簡單的東西你估計見了不少,這樣的東西你可千萬不要回避,多學會一個就能多一樣拿出去唬人。

val worker = Executors.newCachedThreadPool() 
  
worker.execute { 
    println("Hello") 
} 

本來我們應該傳入一個 Runnable 的實例的,結果用一個 Lambda 表達式糊弄過去,Java 怎麼看?

GETSTATIC net/println/MainKt$main$1.INSTANCE : Lnet/println/MainKt$main$1; 
CHECKCAST java/lang/Runnable 
INVOKEINTERFACE java/util/concurrent/ExecutorService.execute (Ljava/lang/Runnable;)V 

Java 說介叫嘛事兒,介不就一 Lambda 麼,轉成 Runnable 在拿過來!

你看上面的這三句字節碼,第一句拿到了一個類的實例,這個類一看就是一個匿名內部類:

final class net/println/MainKt$main$1 implements java/lang/Runnable  { 
    ... 
} 

這是這個類定義的字節碼部分,實現了 Runnable 接口的一個類!

第二句,拿到這個類的實例以後做強轉——還轉啥,直接拿來用唄,肯定沒問題呀。

那你說 SAM 轉換有什麼條件呢?

  • 首先,調用者在 Kotlin 當中,被調用者是 Java 代碼。如果前面的例子當中 worker.execute(…) 是定義在 Kotlin 中方法,那麼我們是不能用 SAM 轉換的。
  • 其次,參數必須是 Java 接口,也就是說,Kotlin 接口和抽象類、Java 抽象類都不可以。
  • 再次,參數的 Java 接口必須只有一個方法。

我們再來舉個 Android 中常見的例子:

view.setOnClickListener{ 
    view -> 
    ... 
} 

view.setOnClickListener(…) 是 Java 方法,參數 OnClickListener 是 Java 接口,並且只有一個方法:

public interface OnClickListener { 
    void onClick(View v); 
} 

6. 小結

Lambda 表達式就是這麼簡單,簡單的讓人有點兒害怕。不知道大家有沒有過這樣的感覺,越是簡單的東西用起來越複雜,不相信你回去翻一翻高中物理課本的牛頓第二定律。Lambda 表達式就是這樣的東西,它能夠極大的簡化代碼的書寫,儘管一旦有了 Lambda 表達式的摻和,代碼本身理解起來可就要稍微困難一些了,不過,因噎廢食的事情想必大家也是不喜歡做的,對吧?

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