inline fun

その名の通りインライン展開される関数です。

インライン展開とは?

fun caller() {
   a()
   callee()
   d()
}

fun callee() {
   b()
   c()
}

通常、関数呼び出しは関数呼び出しにコンパイルされます (???)

JVMの場合はINVOKEVIRTUALというオペコードに変換されます。

ところがインライン展開されると、 関数呼び出しを行わずに関数の中身をコピーするようなバイナリが生成されるんですね。

fun caller() {
   a()
   b()
   c()
   d()
}

このインライン展開、他言語ではプログラマがわざわざ明示しなくてもよしなにやってくれるのですが Kotlinのコンパイラはあまり積極的に行ってくれません。 あまり最適化を行わずProGuardなどの最適化にまかせる方針っぽいです。

JVMのクラスファイルを他言語のLLVMと同じようなものとして利用しているんですね。 実際にProGuardはこういった関数のインライン展開を行うことができます。

さて、もうお分かりかと思いますが、 Kotlinのinline funという言語仕様は最適化が目的ではありません。

Kotlinのinline funが活きてくるのは"関数を受け取る関数"のときです。高階関数の一種ですね。

fun caller() {
   a()
   callee {
      c()
   }
   e()
}

fun callee(block: () -> Unit) {
   b()
   block()
   d()
}

高階関数を呼び出す際に、ラムダ式を渡すことができます。 当たり前です。そもそも高階関数を使う根本的なモチベーションがそれですので

inline funを使わない場合に生成されるクラスファイルを一応書いておきます。 読み飛ばしてもらってもOKです

public static void caller() {
   a();
   callee(new caller$1());
   e();
}

public static void callee(final Function0<Unit> block) {
   b();
   block.invoke();
   d();
}

public static final class caller$1 implements Function0<Unit> {
   @Override
   public final Unit invoke() {
      invoke();
      return Unit.INSTANCE;
   }

   public final void invoke() {
      c();
   }
}

ラムダ式がどのようにコンパイルされるかは 前回のクロージャのエントリにも書きましたので興味がある方はそちらもどうぞ wcaokaze.hatenablog.com

ここで高階関数の方をinline funとすることで、高階関数そのもののインライン展開と同時に 高階関数に引数として渡しているラムダ式もインライン展開されます。

fun caller() {
   a()
   callee {
      c()
   }
   e()
}

inline fun callee(block: () -> Unit) {
^^^^^^
   b()
   block()
   d()
}

こうなります。

fun caller() {
   a()
   b()
   c()
   d()
   e()
}

ラムダ式がその場にインライン展開されることで大きなメリットが生まれます。

ラムダ式内で return を記述できるようになるということです。

fun caller() {
   callee {
      return
   }
}

Kotlinの returnラムダ式ではなく最も近い fun 宣言からのreturnになります。

Kotlinのラムダ式は意図的にforやifなどのブロックの記法にかなり似せてあるので 直感的に書けるようにしてあるんでしょうね。

  • ラムダ式からのreturnではない
    callee { return }
  • 無名関数からのreturn
    callee(fun () { return })

ちなみにラムダ式からのreturnを明示することもできる。

callee { return@callee }

これはラベルという機能。return以外にも使えるけど今回の主題ではないので割愛

さて話を戻します。

ラムダ式がその場にインライン展開されることで大きなメリットが生まれます。

ラムダ式内で return を記述できるようになるということです。

fun caller() {
   callee {
      return
   }
}

インライン展開が行われない場合のコンパイル結果はこのようになります。冗長なところはごっそり省略しますが

public static void caller() {
   callee(new Function0() {
      public final void invoke() {
         // return;
      }
   });
}

ラムダ式内でcallerからのreturnを記述することは不可能ですね。

インライン展開が行われる場合

public static void caller() {
   return;
}

当然callerからのreturnを記述することが可能になるというわけです

技術的には continuebreak も可能になるはずですがそちらはなぜかコンパイルエラーとなります。

noinline

ここから先はそんなに重要じゃないというか… 知らなくてもほとんどの場合困りません

inline fun を宣言しているにも関わらずインライン展開が不可能になっている場合コンパイルエラーとなります。

val f: (() -> Unit)? = null

inline fun noInlineFun(block: () -> Unit) {
   f = block
}

たとえば

noInlineFun { println() }

と書いたとして、

f = new Function0() {
   public final void invoke() {
      println();
   }
};

という具合にインライン展開できるんじゃないの? と思われるかもしれませんが、
これは noInlineFun はインライン展開できているけど noInlineFun に渡しているラムダ式 はインライン展開できていないんですね。
あくまでinline funはラムダ式を展開するためのものです。ラムダ式やそれに当たるインスタンスが生成されてしまうのはinline funではないのです。

というわけでこのようなケースで使うのが noinline 、 呼んで字のごとくインライン展開しないことの明示です。

inline fun noInlineFun(noinline block: () -> Unit) {
   f = block()
}

2つ以上の関数を受け取るときに、いずれかがインライン展開不可能な場合なんかに使います。

crossinline

インライン展開自体は可能だけど、インライン展開されたラムダ式が "その場で" 実行されない場合に使います。

こういうのです

inline fun crossInlineFun(block: () -> Unit) {
   view.setOnClickListener {
      block()
   }
}

インライン展開自体は実現できますがラムダ式内で return が記述できないですからね。

crossInlineFun { return }

view.setOnClickListener(new View.OnClickListener() {
   public final void onClick(final View v) {
      // return;
   }
});

典型的には、受け取った関数を他の inline ではない高階関数で使う場合に起こります。

これは本来 noinline で事足りるのですが、そうすると生成されるクラスファイルが

view.setOnClickListener(new View.OnClickListener() {
   public final void onClick(final View v) {
      new Function0() {
         public final void invoke() {
         }
      }.invoke();
   }
});

ちょっとあまりにもアレなので crossinline を使ってインライン展開することもできるって感じです。
無論、代わりに noinline を使ってProGuardなどの最適化を効かせても結果は同じです。

Contracts

inline funとは直接的な関係はないのですが、性質上inline funのときに使われることが多い Contracts という言語仕様があります。

ここにContractsの話まで書くと長くなってしまうのでまた別の機会に書くかもしれません。

書かないかもしれません。