Kotlin の無名関数 (ラムダ) は、外側のスコープにある変数を参照することができる。
うっかりして、ループ内で変更している変数を参照してしまったので、忘れないようにメモ。
例
今日から明後日までの日付を出力しようとして、次のように書いたとする。
val l = mutableListOf<() -> Unit>() var date = LocalDate.now() for (i in 0..2) { l.add { println("$i $date") } date = date.plusDays(1) } for (f in l) f()
実行してみると、
0 2018-10-19 1 2018-10-19 2 2018-10-19
変数 date
は var
で宣言されていて、ループを抜けた時点では 3 日後の日付に変わってしまっているため、その後で関数を実行すると、必ず 3 日後の日付が出力されるようになってしまった。
これを修正するには、より狭いスコープで別の変数を用意する。
val l = mutableListOf<() -> Unit>() var date = LocalDate.now() for (i in 0..2) { val d = date l.add { println("$i $d") } date = date.plusDays(1) } for (f in l) f()
実行結果は、期待どおり。
0 2018-10-16 1 2018-10-17 2 2018-10-18
この例では val
を使ったけれど、重要なのは可変・不変 (再代入の可否) ではなくスコープなので、var
を使ったとしても同じ出力が得られる。
ループで使っている変数 i
の方は修正前でも意図どおりの値が表示されていたけれど、これは for
の繰り返しに使う変数のスコープが「ループの内側」固定になるため。
JavaScript の場合
そういえば、JavaScript でも同じだった。
var i, j; var a = new Array(); var date = new Date(Date.now()); for (i = 0; i < 3; i++) { a.push(function() { console.log(i + ' ' + date.toLocaleDateString()); }); date = new Date(date.getTime() + 24 * 60 * 60 * 1000); } for (j = 0; j < a.length; j++) { var f = a[j]; f(); }
これの出力結果は、
3 2018/10/19 3 2018/10/19 3 2018/10/19
JavaScript の場合、元々はブロックスコープがなく、関数スコープだけだったので、ループ内に狭いスコープを作るためだけに無名関数を作って呼び出していたりした。
var i, j; var a = new Array(); var date = new Date(Date.now()); for (i = 0; i < 3; i++) { (function() { var n = i; var d = date; a.push(function() { console.log(n + ' ' + d.toLocaleDateString()); }); })(); date = new Date(date.getTime() + 24 * 60 * 60 * 1000); } for (j = 0; j < a.length; j++) { var f = a[j]; f(); }
結果はこう。
0 2018/10/16 1 2018/10/17 2 2018/10/18
ES2015 では、ブロックスコープで変数を宣言できるようになった。var
の代わりに、不変 (再代入不可) なら const
、可変 (再代入可) なら let
を使う。for
ループの変数に let
を付けると、スコープはループの内側になる。
ついでに、同じく ES2015 で導入されたアロー関数も使うと、さらに短くなる。
const a = new Array(); let date = new Date(Date.now()); for (let i = 0; i < 3; i++) { let d = date; a.push(() => { console.log(i + ' ' + d.toLocaleDateString()); }); date = new Date(date.getTime() + 24 * 60 * 60 * 1000); } for (let i = 0; i < a.length; i++) { const f = a[i]; f(); }
Java の場合
Java にはラムダがあるけれど、仕組みの都合上、クロージャは作れない。
昔、「Java にクロージャが入るらしいぞ」と聞いたけれど、数年するうちに「クロージャは議論がまとまらなくて大変らしい」という噂が聞こえてきて、最終的に Java 7 に入ったのは、メソッドひとつだけの (インタフェースや抽象クラスに対する実装の) クラスを簡単にかけるものとしてのラムダ式だった。
final
な変数ならキャプチャできるけれど、これはつまり、Java のラムダでは仕組み上、変数自体を記憶できないため、代わりに値を保存しているからだ。
var l = new ArrayList<Runnable>(); var date = LocalDate.now(); for (var i = 0; i < 3; i++) { final var n = i; final var d = date; l.add(() -> { System.out.println(n + " " + d); }); date = date.plusDays(1); } for (var r : l) { r.run(); }
これで結果は正しく出る。
0 2018-10-16 1 2018-10-17 2 2018-10-18
ラムダ導入以前の Java 6 時代だともっと長くなる。JSR-310 もなかったから、こんな感じかな。
List<Runnable> l = new ArrayList<Runnable>(); Date date = new Date(); for (int i = 0; i < 3; i++) { final int n = i; final Date d = date; l.add(new Runnable() { @Override public void run() { System.out.printf("%d %tF%n", n, d); } }); date = new Date(date.getTime() + TimeUnit.DAYS.toMillis(1)); } for (Runnable r : l) { r.run(); }
結果はこう。
0 2018-10-16 1 2018-10-17 2 2018-10-18