クロージャ

関数の中に関数を書くことができます。

fun outer() {
   fun inner() {
   }
}

通常の関数

fun inner() {
}

匿名関数

val inner = fun() {
}

ラムダ式

val inner = {
}

記法はいくつかあれど、Kotlinの場合関数の中に書く関数はすべてクロージャとなります。
つまり内側の関数は外側の関数にあるローカル変数へのアクセスが可能です。

fun enclosure() {
   val localVariable = 42

   fun closure1(): Int {
      return localVariable + 1
   }

   val closure2 = fun(): Int {
      return localVariable + 1
   }

   val closure3: () -> Int = {
      localVariable + 1
   }
}

このキャプチャを実現している手法についてです

用語


どの記法を使ったとしてもクロージャJVMclass へとコンパイルされます (Kotlin 1.4.10)

final class enclosure$1 implements kotlin.jvm.functions.Function0<Integer> {
   final int $a;

   enclosure$1(final int a) {
      this.$a = a;
   }

   @Override
   public final Integer invoke() {
      return Integer.valueOf(invoke());
   }

   public final int invoke() {
      return this.$a + 1;
   }
}

invoke() をふたつ定義しているので実際にはこのJavaコードはコンパイルできませんが、 クラスファイルとしてはこれは有効なためこの形式へと変換されます。

より厳密に言えば型引数である <Integer> はイレイジャによって消されて Object になります。

public final Object invoke() {
   //        ^^^^^^
   return Integer.valueOf(invoke());
}

簡単ですね。

少しややこしくなってくるのが、 エンクロージャの変数が再代入可能な場合です。

fun enclosure() {
   var localVariable = 42

   fun closure1(): Int {
      localVariable++
      return localVariable
   }
}

Javaだとコンパイルできないやつです。
Javaクロージャクロージャじゃないとか言われる所以ですね。今回はそんな話はしたくないので割愛します

クロージャが再代入した値はエンクロージャから見ても反映されているし逆もまた然りです。

この場合にKotlinのコンパイラが生成するclassは下記のようになります。

final class enclosure$1 implements kotlin.jvm.functions.Function0<Integer> {
   final kotlin.jvm.internal.Ref.IntRef $a;

   enclosure$1(final kotlin.jvm.internal.Ref.IntRef a) {
      this.$a = a;
   }

   @Override
   public final Integer invoke() {
      return Integer.valueOf(invoke());
   }

   public final int invoke() {
      this.$a.element++;
      return this.$a.element;
   }
}

Ref.IntRef でラップ(wrap)されます。
だいたい想像できるかと思いますがこうなっています。

public static final class IntRef implements Serializable {
   public int element;
}

この場合エンクロージャ側のキャプチャされる変数は暗黙的に Ref.IntRef 型に変換されます。

public final void enclosure() {
   final Ref.IntRef localVariable = new Ref.IntRef();
   localVariable.element = 42;

   enclosure$1 closure1 = new enclosure$1(localVariable);
}

そりゃそうですね。

ただし inline funクロージャがインライン展開される場合にはこのwrapは発生せず 通常通りローカル変数のままコンパイルされます。

inline fun についてはまたいつか書くかもしれません