覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Kotlin - ラムダにおけるスコープ内の変数アクセス・キャプチャ

今日も相変わらず 「Kotlinイン・アクション」 を読みながらの写経です。

Kotlinイン・アクション

Kotlinイン・アクション

  • 作者: Dmitry Jemerov,Svetlana Isakova,長澤太郎,藤原聖,山本純平,yy_yank
  • 出版社/メーカー: マイナビ出版
  • 発売日: 2017/10/31
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログ (2件) を見る


関数で匿名の内部クラスを宣言した場合、その関数の引数とローカル変数をそのクラスの中から参照できますが、
ラムダでも同様のことが可能です。関数でラムダを使用すると、関数の引数とラムダの前に宣言されているローカル変数を参照できます。

以下のコードでは、関数の引数の prefix, suffix と 関数内でラムダの前に宣言したローカル変数 title にアクセスしています。

fun main() {
    val messages = listOf("0 success", "1 Not super-user", "2 No such file or directory",
        "3 No such process", "4 Interrupted system call", "5 I/O error")

    printMessages(messages, "'", "'")
}

fun printMessages(messages: Collection<String>, prefix: String, suffix: String) {
    var title = "Message:"
    messages.forEach {
        println("$title ${prefix}${it}${suffix}")
    }
}


実行結果は以下の通りで、正常に各変数にアクセスできていることがわかります。

Message: '0 success'
Message: '1 Not super-user'
Message: '2 No such file or directory'
Message: '3 No such process'
Message: '4 Interrupted system call'
Message: '5 I/O error'


変数のキャプチャ

先ほどのコードを Java で記述すると大凡以下のようになります。

import java.util.Collection;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> messages = List.of("0 success", "1 Not super-user", "2 No such file or directory",
                "3 No such process", "4 Interrupted system call", "5 I/O error");

        printMessages(messages, "'", "'");
    }

    private static void printMessages(Collection<String> messages, String prefix, String suffix) {
        String title = "Message:";
        messages.forEach(
                msg -> {
                    System.out.println(title + " " + prefix + msg + suffix);
                });
    }
}

ここで重要なことはJavaのコードでのラムダ内で操作している変数は 実質的final であるため、変更ができません。
それに対して、Kotlinではfinal変数への参照だけに制限されていません。そのため、ラムダ内から変数を変更することもできます。


例えばJavaで以下のようなコードを書いてもコンパイルエラーとなってしまいます。(successCount, errorCountのインクリメント部分がエラーになります)

public class Main {
    public static void main(String[] args) {
        List<String> messages = List.of("0 success", "1 Not super-user", "2 No such file or directory",
                "3 No such process", "4 Interrupted system call", "5 I/O error");

        printMessages(messages, "'", "'");
    }

    private static void printMessages(Collection<String> messages, String prefix, String suffix) {
        String title = "Message:";
        int successCount = 0;
        int errorCount = 0;
        messages.forEach(
                msg -> {
                    if (msg.startsWith("0")) {
                        successCount++;
                    } else {
                        errorCount++
                    }
                    
                    System.out.println(title + " " + prefix + msg + suffix);
                });

        System.out.println("success count = " + successCount + ", error count = " + errorCount);
    }
}


これに対してKotlinでは以下のようなコードをコンパイルすることが可能です。

fun main() {
    val messages = listOf("0 success", "1 Not super-user", "2 No such file or directory",
        "3 No such process", "4 Interrupted system call", "5 I/O error")

    printMessages(messages, "'", "'")
}

fun printMessages(messages: Collection<String>, prefix: String, suffix: String) {
    var title = "Message:"
    var successCount = 0
    var errorCount = 0
    messages.forEach {

        if (it.startsWith("0")) {
            successCount++
        } else {
            errorCount++;
        }

        println("$title ${prefix}${it}${suffix}")
    }

    println("success count = $successCount, error count = $errorCount");
}

実行結果は以下の通りです

Message: '0 success'
Message: '1 Not super-user'
Message: '2 No such file or directory'
Message: '3 No such process'
Message: '4 Interrupted system call'
Message: '5 I/O error'
success count = 1, error count = 5


Javaとは違い、Kotlinはラムダ内からfinalではない変数を参照して、それを変更することも可能です。
上記コード例の prefix, suffix, successCount, errorCount などのラムダから参照される外部変数は、ラムダによって キャプチャ(caputure)されている と表現されます。

デフォルトでは、ローカル変数のライフタイムは変数が宣言されている関数によって制約されます。
ただし、ラムダによってキャプチャされた場合は、この変数を使用するコードは保持されて、あとで実行されます。
final変数をキャプチャすると、その値はそれを使用するラムダのコードとともに保持され、final変数でない場合はその値を変更できるようにとくべるなラッパーに囲まれ、ラッパーへの参照がラムダとともに保持されます。


注意点

ラムダがイベンハンドラのように用いられて、非同期に実行される場合、ラムダが実行された時に初めてローカル変数への変更発生します。
例えば以下のようなコードを書くと、この関数は常に 0 を返します。

fun countButtonClicks(button: Button): Int {
    var clickCount = 0
    button.onClick { clickCount++}
    return clickCount
}

onClickハンドラはclickCount(クリック数)の値をインクリメントして変更しますが、関数が返された後に onClickハンドラが呼び出されるため、変更されたクリック数を観測することはできません。


まとめ

Kotlinにおけるラムダのスコープ内の変数アクセスは、変数に対するキャプチャによってJavaと異なる操作が可能であることがわかりました。