こんにちはsekitakaです。
AWSのバージニアリージョンがトラブってて開発しにくいのでQiita記事書いています。
Anko-Layoutsを使ってみようと思ってコードを見てみたところ、inline
という見慣れない修飾子を見つけたので調べてみました。
inline修飾子とは
inline修飾子は主に関数を引数に持つ関数に使われる修飾子です。
この修飾子が付与された関数は、呼び出し元から関数が呼ばれるのでなく、関数の中身がコードとして展開されます。
C言語を知っている方はマクロをイメージすると分かりやすいと思います。
Javaにデコンパイルされたコードを見ると分かりやすいので確認してみましょう。
inline関数
引数で渡されたラムダ式の関数を2回実行するinline修飾子が付与された関数repeat
を,main
から呼び出します。
inline fun repeat(fn: () -> Unit) {
fn()
fn()
}
fun main(args: Array<String>) {
repeat {
print("foo")
}
}
これをAndroid StudioのKotlin Bytecodeの機能でデコンパイルすると次のようになります。
@Metadata(
mv = {1, 1, 6},
bv = {1, 0, 1},
k = 2,
d1 = {"\u0000\u001a\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u0011\n\u0002\u0010\u000e\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0000\u001a\u0019\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00040\u0003¢\u0006\u0002\u0010\u0005\u001a\u0017\u0010\u0006\u001a\u00020\u00012\f\u0010\u0007\u001a\b\u0012\u0004\u0012\u00020\u00010\bH\u0086\b¨\u0006\t"},
d2 = {"main", "", "args", "", "", "([Ljava/lang/String;)V", "repeat", "fn", "Lkotlin/Function0;", "production sources for module app"}
)
public final class FooKt {
public static final void repeat(@NotNull Function0 fn) {
Intrinsics.checkParameterIsNotNull(fn, "fn");
fn.invoke();
fn.invoke();
}
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
String var1 = "foo"; // このあたり
System.out.print(var1);
var1 = "foo";
System.out.print(var1);
}
}
main
からrepeat
が呼ばれるのでなく、"foo"
を表示するコードが2回呼ばれている事がわかると思います。
このように引数のラムダ式を呼び出し元の関数内に展開して使用するのがinline
修飾子の役割です。
非inline関数
inlineでないrepeat関数がどのようにデコンパイルされるのかも見てみましょう。
inline
修飾子を付けない以下のコードをデコンパイルします。
fun repeat(fn: () -> Unit) {
fn()
fn()
}
fun main(args: Array<String>) {
repeat {
print("foo")
}
}
結果は次の通りです。
ごらんのようにmain
からはrepeat
関数を呼んでいます。repeat
に渡すラムダ式はクラス内に見当たりませんが@Metadata
にそれらしき記述があるので、ごにょごにょうまいことやっているのでしょう。
@Metadata(
mv = {1, 1, 6},
bv = {1, 0, 1},
k = 2,
d1 = {"\u0000\u001a\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u0011\n\u0002\u0010\u000e\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0000\u001a\u0019\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00040\u0003¢\u0006\u0002\u0010\u0005\u001a\u0014\u0010\u0006\u001a\u00020\u00012\f\u0010\u0007\u001a\b\u0012\u0004\u0012\u00020\u00010\b¨\u0006\t"},
d2 = {"main", "", "args", "", "", "([Ljava/lang/String;)V", "repeat", "fn", "Lkotlin/Function0;", "production sources for module app"}
)
public final class FooKt {
public static final void repeat(@NotNull Function0 fn) {
Intrinsics.checkParameterIsNotNull(fn, "fn");
fn.invoke();
fn.invoke();
}
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
repeat((Function0)null.INSTANCE); // ここ
}
}
どうしてinline修飾子が必要なの?
関数を引数にとったり戻り値として返すことは様々なオーバーヘッドがあるようです。詳しくは公式ドキュメントの初めの方に書いてあります。
inline
修飾子を使うことでプログラムのサイズは大きくなるが、パフォーマンスの向上になるんだそうです。
特に大きなループの中で毎回ラムダ式を実行するような場合に効果を発揮しそうですね。
注意点
呼び出し元の関数内に展開されるのはreturn
なども同じなので、inline関数に渡すラムダ式やinline関数内で書いたreturn
は呼び出し元関数のreturn
になってしまいます。
先程のrepeat
関数へ渡す引数のラムダ式内でreturn
するとfooは一度しか表示されずmain関数が終了してします。
inline fun repeat(fn: () -> Unit) {
fn()
fn()
}
fun main(args: Array<String>) {
repeat {
print("foo")
return // mainのreturn になる
}
}
List
のforEach
なんかも実はinline関数なので、次のようなコードを書くとループの1つめの要素でmain
が終了してしまいます。
fun main(args: Array<String>) {
val foo = listOf<String>("foo", "bar")
foo.forEach { item ->
print(item)
return
}
}
JavaScriptの匿名関数とかになれてるとうっかりハマることもあるかもしれません。
まとめ
いかがでしたでしょうか。inline
関数の特性を使いこなしてハイパフォーマンスなアプリ開発をしたいものですね。