mtkw.dev

Kotlinのreifiedとinlineについて勉強する

これは何?

ライブラリ内の実装を見るとKoltinのreifiedとinlineの組み合わせによる実装がよく見られます。この言語機能についてお勉強していきます。

JVMのType Erasure(型消去)

reified inlineを学ぶ前に、Type Erasureという機能について整理します。

これはJVM特有の話ですが、ジェネリクスにおいて型パラメータの情報はコンパイル時にのみ利用され、実行時には利用されません。これをType Erasureと言います。

fun getName(list: List<String>): String {
    return list[0]
}

このようなコードを書いたとします。このコードをデコンパイルしJavaに変換すると以下になります。

@NotNull
public static final String getName(@NotNull List list) {
    Intrinsics.checkNotNullParameter(list, "list");
    return (String)list.get(0);
}

Listの型パラメータであるStringが削除されています。要素はObjectとして扱われ、取り出す際にStringにキャストされています。

このようにKotlin/JVMではジェネリクスにおいて、型パラメータはあくまでコンパイル時点に削除される、Type Erasure(型消去)という特徴があります。

Type Erasureによるコンパイルエラー

fun <T> check(value: Any): Boolean {
    return value is T // Cannot check for instance of erased type 'T (of fun <T> check
}

このコードはコンパイルエラーになります。なぜなら、実行時に型パラメータTを利用していますが、型パラメータTの情報は実行時には存在せず、値はObjectとして扱われるためチェックできないためです。

// コンパイル前
check<String>("hello")
 
// コンパイル後
check(Object value)

概念的にはこのようなイメージで、実行時には型パラメータは存在しないためチェックが出来ないわけです。

reified + inline

実行時に型パラメータTを利用するコードを書きたい場合は、reifiedとinlineを組み合わせることで実現できます。

inline fun <reified T> check(value: Any): Boolean {
    return value is T
}
 
check<String>("Hello")

inline関数にすることで呼び出し元に展開します。重要な点は呼び出し元ではTの具体的な型がわかっていることです。

reifiedは具体化するという意味です。inlineで呼び出し元に展開し、型パラメータTを具体的な型に置き換える(reifiedする)ことでコンパイルが通ります。

fun main() {
    val value: Any = getValue()
 
    println(check<String>(value))
}
 
fun getValue(): Any = "Hello"
 
inline fun <reified T> check(value: Any): Boolean {
    return value is T
}

このようなコードをデコンパイルしJavaに変換してみます。コンパイル時の最適化されすぎないように冗長なコードを用います。

public static final void main() {
    Object value = getValue();
    int $i$f$check = 0;
    boolean value$iv = value instanceof String;
    System.out.println(value$iv);
}
 
@NotNull
public static final Object getValue() {
    return "Hello";
}
 
// $FF: synthetic method
public static final boolean check(Object value) {
    Intrinsics.checkNotNullParameter(value, "value");
    int $i$f$check = 0;
    Intrinsics.reifiedOperationMarker(3, "T");
    return value instanceof Object;
}

check関数はmainから呼ばれておらず、main関数で展開されています。main関数ではTがStringだと分かるため具体化されています。

まとめ

Type Erasureとreified inlineを勉強しました。

ライブラリ内のコードを読む際にreified inlineは目にしていましたが、いまいち理解できていなかったので理解できてよかったです。