1. 程式人生 > >kotlin 關於lambda,你想知道的都在這裡

kotlin 關於lambda,你想知道的都在這裡

Java語言轉到Kotlin,最讓人頭疼的問題恐怕就是lambda表示式了。

lambda,準確的中文翻譯是:匿名函式
不過,在Kotlin語言中本身就有匿名函式的概念,為了區分,我們姑且把它叫做Lambda表示式。

對於Java程式設計師來說,這是一個比較新的概念。而在計算機領域,這其實是一個非常普遍的概念。在C++11,OC,Java8,Python等語言中均有相應實現。

一起來簡單看一下其它語言關於lambda表示式的實現!

C++

Java語言的老祖宗C++11標準已經開始支援使用lambda表示式了!

語法:[ capture ] ( params) mutable exception attribute -> ret { body }

這是一個完整的lambda表示式語法,capture表示捕獲的外部變數列表,mutable修飾說明lambda表示式內部程式碼是否可以修改捕獲的外部變數的值。exception表示lambda表示式丟擲的異常,和函式宣告類似。

#include <functional>

int main() {
  auto sum = [] (int x, int y) { return x + y; };
  std::cout << "sum(3, 4) = " << sum(3, 4) << std::endl;

  // lambda表示式本身就是一個函式。因此,宣告可以用函式接收,like this:
  std::function<int (int, int)> sum1 = [] (int x, int y) { return x + y; };
  std::cout << "sum1(3, 4) = " << sum1(3, 4) << endl;
}

可以看到,C++在實現lambda表示式上面顯得有點中規中矩,基本上就是將函式名去掉,再完整Copy。估計它老人家年紀大了,也懶得動了。不過,它的這種實現恰好可以作為lambda表示式實現的範本。可以說,這是lambda表示式實現最完整、靈活性也最高的版本。

C++ 靈活性... LOL

OC

OC語言中,是我最早看到lambda表示式實現的地方。OC其實也是一門古老的語言,像C++一樣提供了lambda表示式實現,並且實現的更早。在OC語言中,它叫做block,中文翻譯為程式碼塊。一起來看一下它的實現:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int(^sum) (int, int) = ^(int x, int y) {
            return x + y;
        };
        NSLog(@"sum(3, 4) = %i", sum(3, 4));
    }
    return 0;
}

Java8

在Java8語言標準中,Oracle官方終於提供了lambda表示式的實現。

語法: ( params) -> { expressions; }

package com.company;

import java.util.function.BiFunction;

public class Main {

    public static void main(String[] args) {
        BiFunction<Integer, Integer, Integer> sum = (Integer x, Integer y) -> {
            return x + y;
        };

        System.out.println("sum(3, 4) = " + sum.apply(3, 4));
    }
}

PS:這裡需要說明一點的是,雖然我沒有宣告函式式介面,就完成了lambda表示式的宣告。這並不代表大家在使用的過程中不需要使用函式式介面,在Java8語言中,只提供了少量的函式式介面可以使用,這裡恰好可以實現我的簡單需求,這其實是一個小小的tricks。在大多數使用場景中,你依然需要先宣告一個函式式介面。

什麼?你還不知道什麼叫做函式式介面,趕緊去看官方文件吧!

有人說,這樣的設計不是掩耳盜鈴嗎?
-_- || ,是的,不得不說,這是Java8 lambda表示式設計的一大遺憾!

好啦,接下來,我們開始本文的重點,Kotlin語言lambda表示式的相關知識!

基礎知識

廢話不多說,我們直接開始。Kotlin語言中,lambda表示式的完整語法如下:
{ params -> expressions }
params表示引數列表,expressions表示具體實現,可以是單行語句,也可以是多行語句。

作為Java語言的近親,lambda表示式的語法和Java8非常接近。不過,由於Kotlin語言天然支援函數語言程式設計的特性。宣告lambda表示式不需要顯式宣告函式式介面,顯得優雅了許多。

來看一下實現上述同樣功能,Kotlin語言的實現:

val sum = { x: Int, y: Int -> x + y; };
print("sum(3, 4) = ${sum(3, 4)}")

型別推導

lambda表示式常常和型別推導一起使用,剛開始使用lambda表示式的時候,總是會遇到到底該不該使用具體型別宣告的疑惑。其實,這並不是一個難題,在思考型別推導的時候,注意以下兩點即可:

1)宣告lambda表示式:如果在左邊定義中,已經寫了具體的型別宣告,後面的實現就可以不用。反之,實現中則需要具體的宣告。這裡,可能有人會問,如果有多個引數,我一部分在定義中宣告,一部分在實現中宣告,是否可以?LOL,親愛的,你覺得呢?

2)使用lambda表示式:基本不用,某些特殊情況可能需要。

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

這裡要注意一個非常的特殊的lambda表示式的寫法:如果一個lambda表示式只有一個引數,這個引數可以使用it指代,注意只能使用it指代,不能使用其它的單詞代替,這點要謹記!看下面的例子:

val condition: (x: Int) -> Boolean = { x -> x > 0 }
// 等價於(注意:這裡只能用it,不能使用其它單詞)
val condition: (x: Int) -> Boolean = { it > 0 }

這在控制流中使用比較廣泛,關於控制流的使用,大家如果覺得有必要講解一下,在文章最後評論告訴我。

Closure(閉包)

敲黑板!!!
這是大家非常容易混淆的概念,我們常常把lambda表示式也稱為閉包,這其實無可厚非?但這真的是一個概念嗎?

其實並不是,這裡要先提一個新的概念capture,這個單詞大家在看C++例子的時候已經見到過了。中文意思是捕獲,是指閉包使用非自己作用域的外部變數。閉包這個概念與capture息息相關,簡單來說,可以用一句話概括:

如果lambda表示式訪問了它的作用域外部的變數,這個lambda表示式加上它訪問的外部變數一起就構成了閉包

var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}
print(sum)

從某種層面來說,這個概念並不影響使用,但瞭解它的意思有利於更深層次理解lambda表示式。

lambda表示式的問題

同函式不一樣,Kotlin版本lambda表示式中不允許使用return關鍵字。return關鍵字在解決多分支語句程式碼優化方面有很好的作用。有人說,如果我一定要使用return關鍵字怎麼辦,目前比較好的替代方案是:匿名函式

大部分情況下,匿名函式和lambda表示式幾乎可以通用,推薦使用lambda表示式。兩者唯一的區別就是上面提到的匿名函式可以使用return關鍵字,而lambda表示式不行。一起來看一下匿名函式的簡單用法:

// 這是一個完整函式
fun sum(x: Int, y: Int): Int {
  return x + y
}

// 轉化為匿名函式
fun(x: Int, y: Int): Int {
  return x + y
}

匿名函式因為沒有函式名稱,不能直接使用,需要使用一個變數接收。然後,呼叫使用這個變數間接呼叫這個匿名函式。

看到這裡,細心的同學一定會有所疑問,看這裡:

// 這裡用一段虛擬碼模擬http請求
fun httpRequest(url: String, onSuccess: (code: Int)->Unit) {
    ...
}
// 在呼叫的時候,我們需要在回撥中對Code進行處理
httpRequest(url = "http://www.youngfeng.com?id=xxx",
     onSuccess = { code ->
          if(code == 1) {
              ....
          [email protected]
       }
})

在這段程式碼中,明顯在閉包中使用了return關鍵字,你為什麼說不可以使用呢?

囧... 是的,這裡的確使用了return關鍵字,可是這裡的return並不表示閉包邏輯退出,而是退出整個httpRequest函式。換而言之,如果閉包是服務於某個函式的,在其中的確可以使用return關鍵字,但這表示退出函式邏輯,而閉包本身是不能使用return關鍵字進行邏輯分支的。

提問:這裡Kotlin語言設計lambda表示式不允許使用return關鍵字你認為是否是一個缺陷呢?

這個問題文章最後我們再做討論

尾隨閉包

在上文的講解中,我們忽略了一個非常重要的概念:尾隨閉包。這個概念在實際開發中經常使用,初次接觸這個概念的Java程式設計師會感覺到非常的不適應,需要一段時間的磨合後才能慢慢習慣這種寫法。別急,且聽我慢慢分解。

lambda表示式可以作為函式引數使用,如果一個函式的最後一個引數恰好是一個lambda表示式,lambda表示式可以寫到括號的外面。

使用尾隨閉包後,上面的表示式可以這樣表示:

httpRequest(url = "http://www.youngfeng.com?id=xxx")  { code ->
          if(code == 1) {
              ....
          [email protected]
       }
}

對於這樣一種表達方式,Java陣營的同學常常會有這樣一種疑惑:通常來說,括號外面應該是函式的定義,這樣的表達不會和定義混淆嗎?

其實是不會的,注意看上面,這裡是函式的呼叫,而非定義。如果你看到一個函式在呼叫的時候,表示式寫到了括號的外面,就表示它一定是一個尾隨閉包實現。而如果是函式定義,就沒什麼可說的了!

儘管如此,對於這樣的一種表達方式,依然要在平時編碼過程中不斷使用,不要去排斥它。否則,很難駕輕就熟。

總結

這篇文章從以下幾個方面介紹了關於lambda表示式的相關知識:

  • 基礎知識(語法:{ params -> expressions }
  • 靈活運用型別推導
  • 閉包與lambda表示式的區別
  • 閉包的“缺陷”(不能使用return關鍵字)
  • 尾隨閉包

答疑解惑

a)使用lambda表示式是否容易造成程式碼可閱讀性變差?

使用lambda表示式帶來的一個最直觀的問題就是:變數的型別有點難以預測,如果命名不規範將需要檢視原始碼才知道具體變數的意思。從這個層面來說,的確帶來一定的閱讀問題,但只要養成良好的命名習慣,這個問題是可以避免的。這個問題對於使用弱型別語言(如JS)編碼的同學來說,根本不是一個問題!

b)lambda表示式和函式是什麼關係?針對這兩者應該如何取捨?

注意:lambda表示式和函式其實完全是一回事,lambda表示式可以理解為一個沒有函式宣告的匿名函式。因此,在使用的過程中,如果你需要一個不需要宣告就使用的函式,使用lambda表示式將是一個不二的選擇。在Android應用開發中,lambda表示式通常用在回撥場景中。

c)在Java8語言中,lambda表示式是可以明確使用return關鍵字的,而Kotlin語言中,lambda表示式卻不能使用return關鍵字。這是一個設計缺陷嗎?

我認為不是!注意:Kotlin語言是一門支援型別推導的語言,通過對閉包上下文的分析,我們可以推斷出哪一部分是作為返回值return的,只是在主觀閱讀上缺少了一個明顯的return關鍵字而已,但這並不影響使用!