1. 程式人生 > >Kotlin系列之Lambda表示式(2)

Kotlin系列之Lambda表示式(2)

上一篇文章講到了最基本的Lambda表示式,今天這篇文章繼續講Lambda表示式中的在作用域中訪問變數。

Java中的內部類訪問變數

當我們在函式內部使用匿名內部類時,我們可以在匿名內部類內使用函式的引數和函式內的區域性變數。當我們在使用Lambda表示式時,我們也可以訪問這個函式的引數和使用那些在Lambda表示式之前定義的變數。

下面先看一個在Java中匿名內部類中訪問函式引數和區域性變數的例子。

public void search() {
    final String str = "xxxx";
    new Thread(new Runnable() {
        @Override
public void run() { for (int i = 0; i < 10; i++) { System.out.println(str); } } }).start(); }

上面的這個例子中區域性變數str在匿名內部類中被使用,所以必須要加final修飾符。

如果你使用的JDK是Java8版本,那你會發現你不寫final修飾符也是不會報錯的。這因為Java8中final修飾符不是必需的,但是如果你嘗試在內部類中更改str的值,IDE就會告訴你,在匿名內部類中使用的區域性變數是final的,不可以被修改。所以在Java8中如果你沒有嘗試去修改那個值,final修飾符是可以省略的。

那為什麼在Java中匿名內部類中使用的變數必須是final型別呢?這裡涉及到一個作用域的問題。str多的作用域是search()這個函式,函式執行結束,那這個區域性變數就消失了,但是我們的匿名內部類的執行時機並不是在search()函式執行完成前,可能search()函式已經結束了,匿名內部類執行緒才會執行,如果這時候str不是final型別,它就已經被回收了,當執行緒類執行的時候就會找不到這個變數而報錯。如果使用final修飾符,Java就會複製一份這個變數作為內部類的成員變數,由於是final修飾的,還保證了這份複製過來的變數不會被篡改,使內部類和外部類的變數保持一致性。

Kotlin在作用域中訪問變數

上面解釋了Java中匿名內部類中變數訪問的情況及原理,其實把上面的匿名內部類換成Lambda表示式也可以做同樣的事,達到同樣的效果。
那下面看看在Kotlin中使用Lambda表示式訪問作用域中的變數的規則。

在Kotlin中在Lambda表示式內部可以訪問外部的變數,而無需宣告為final,而且在Lambda內部可以修改這些變數。下面看一個示例

fun countThings(datas : List<Int>){
    var count = 0
    datas.forEach {
        if (it == 0){
            count++
        }
    }
    print("count = $count")
}

上面程式碼forEach裡面傳遞的就是一個Lambda表示式,對於區域性變數count並沒有被宣告為final,同時這個變數還可以在Lambda表示式內部被修改。

與Java比起來這是不是有點不可思議,那Kotlin是怎麼做到的。

在分析之前我們先來看一下,加入我們在Java中是如何實現匿名內部類中修改外部變數的值的。

public void search() {
    final String[] str = {"xxxx"};
    new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            str[0] = str[0] + "|" + i;
            System.out.println(str[0]);
        }
    }).start();
}

上面的程式碼中Java為了讓我們讓匿名內部類可以修改外部的變數,我們建立了一個單元素的陣列,並宣告為final型別。這樣雖然str陣列是final的,但它其中的元素卻是可以修改的,這樣就既保證了匿名內部類從函式中複製到匿名內部類內部的變數是不可變的,又保證了我們可以在匿名內部類中修改這個變數值。

Java除了使用上面的方法解決這個問題外,還有一種方法如下

public void search() {
    final Ref<String> ref = new Ref<>("xxxx");
    new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            ref.value= ref.value + "|" + i;
            System.out.println(ref.value);
        }
    }).start();
}


static class Ref<T>{
    T value;
    public  Ref(T value){
        this.value = value;
    }
}

上面的程式碼中我們使用了一個靜態內部類,雖然靜態內部類的引用是final型別的,但我們卻可以修改它內部的value屬性,也就達到了在Lambda表示式中修改外部變數的目的。

其實上面的這種方法,正是Kotlin使用的方法。Kotlin內部就是通過這樣的方法使我們可以在Lambda內部修改外部的變數。只是不需要我們顯式去建立這樣的包裝器類。

所以用專業一點的術語解釋就是,預設情況下,區域性變數的宣告週期是被限制在了宣告這個變數的函式中,但是如果它在Lambda內部被使用了,我們就稱它被Lambda捕獲了,這時候,使用這個變數的程式碼就會被儲存並稍後執行。當捕獲了一個final變數時,它的值就會和使用這個值的Lambda表示式一起被儲存,如果對非final變數,它的值就像上面演示的一樣被封裝在一個包裝器內部,這樣這個值就可以被改變,同時會被這個包裝器類的引用和Lambda程式碼一起儲存。

說白了,就是如果你只是在Lambda內部使用這個值而不修改它,那就複製一份它的值到Lambda表示式內部,如果你在Lambda內部使用它,那Kotlin就會建立一個包裝器類,同時把這個包裝器類的引用複製到Lambda表示式內部方便你修改它。

寫在最後

本節涉及到的這個只是點還是蠻重要的,但是Kotlin已經幫我們隱藏了背後的實現細節,但我覺得背後的原因還是有必要搞清楚的。