この記事はKotlin 1.3.10でのinline class
について解説しています。
現状ではExperimentalなAPIなので、破壊的な変更がある場合があります。
inline classes
ビジネスロジックのためにラッパークラスが必要となることはありますが、ランタイムのオーバーヘッドやヒープを無駄遣いするといったことが起こります。特にラッピングしている型がプリミティブな場合、パフォーマンスはとても悪くなります。
これらの問題を解決するためにinline class
が導入されました。
inline class
はinline
修飾子をクラスの前に置くことで宣言できます。
inline class Password(val value: String)
公式ドキュメントにある仕様のまとめ
-
単一のプロパティを持ち、プライマリコンストラクタで初期化しなくてはならない。2つのプロパティを持つことはできない。
-
Read-Onlyプロパティと関数を持つことができる。
- 実際には
static
関数が生成され、呼び出される際にはその関数が呼ばれる。
- 実際には
-
init
ブロックは持てない。 -
inner
クラスを定義できない。 -
バッキングフィールドを持つことができない。
- つまり
lateinit
やデリゲートプロパティを持つことができない。
- つまり
-
インターフェースを実装することができる。
- 前述の通り実際には
static
関数が生成される。
- 前述の通り実際には
-
プリミティブ型同様にBoxingが可能。
inline class UserName(val value: String) fun asInline(userName: UserName) {} fun <T> asGeneric(t: T) {} fun main() { val userName = UserName("John") asInline(userName) // inlined asGeneric(userName) // boxed }
- ジェネリクスパラメーターの関数へ渡すときなどにAuto-Boxing/Unboxingされる。
- そのため、実際に生成される
inline class
にはプライベートコンストラクタが存在し、static box-impl
関数を通してBoxingされる。
-
inline class
をパラメーター/戻り値に取る関数は名前の衝突回避のためにfuncName-<hashcode>
などに変換される。- このため、Javaからは
inline class
はほぼ利用できなくなっている。
- このため、Javaからは
Type aliasとの比較
inline class
とtypealias
は似ているが相違点がある。
-
typealias
は、実際には指定されたエイリアス名のクラスは生成されず、エイリアスされた型と相互利用できる。 -
inline class
は、実際にクラスが生成され、ラップしている型とは相互利用できない。
拡張などもtypealias
には癖があるが、inline class
は通常のクラス同様に扱える。
typealias StringTypeAlias = String
inline class StringInlineClass(val value: String)
fun StringTypeAlias.printlnTypeAlias() = println(this)
fun StringInlineClass.printlnInlineClass() = println(value)
fun main() {
"foo".printlnTypeAlias() // typealiasはエイリアスの方を拡張してしまう。
"foo".printlnInlineClass() // Compile error
StringInlineClass("foo").printlnInlineClass() // OK
}
その他確認できたこと
トップレベルにしか定義できない。
// ネストして定義はできない
data class User(val id: Id) {
inline class Id(val value: Long)
}
コンパイル後にラップした型に置換される。
inline class UserId(val value: Int)
inline class UserName(val value: String)
class User(val id: UserId, val name: UserName)
上記コードはbytecode上では以下のように解釈される。
public final class User {
private final int id;
@NotNull
private final String name;
public final int getId() {
return this.id;
}
@NotNull
public final String getName() {
return this.name;
}
private User(int id, String name) {
this.id = id;
this.name = name;
}
}
またクラスのメタデータなどもラップした型に置換される。
inline class Foo(val value: String)
println(Foo::class.java.simpleName) // output: String
Gsonとは問題なく使える
リフレクションを使う場合、ラップしている型としてシリアライズ/デシリアライズされるため、特にAdapterなどを作らなくても利用できる。
inline class UserId(val value: Int)
inline class UserName(val value: String)
data class User(val id: UserId, val name: UserName)
val user = Gson().fromJson<User>("""{"id":123,"name":"John"}""", User::class.java)
println(user) // User(id=UserId(value=123), name=UserName(value=John))
Annotation processorとは相性が悪そう
Moshiのコード生成とRoomを試したが、実際に生成されるクラスの型情報とコンパイラによりアンラップされた型との不一致により、エラーになるようだった。
inline class UserId(val value: Int)
inline class UserName(val value: String)
// コンストラクタの型とプロパティの型が不一致というエラーになってしまう
@Entity
data class User(
@PrimaryKey val id: UserId,
val name: UserName
)
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE id = :id")
fun findById(id: UserId): User
}