Kotlin
KotlinDay 3

inline classについてまとめる

この記事はKotlin 1.3.10でのinline classについて解説しています。

現状ではExperimentalなAPIなので、破壊的な変更がある場合があります。


inline classes

https://kotlinlang.org/docs/reference/inline-classes.html

ビジネスロジックのためにラッパークラスが必要となることはありますが、ランタイムのオーバーヘッドやヒープを無駄遣いするといったことが起こります。特にラッピングしている型がプリミティブな場合、パフォーマンスはとても悪くなります。

これらの問題を解決するためにinline classが導入されました。

inline classinline修飾子をクラスの前に置くことで宣言できます。

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はほぼ利用できなくなっている。




Type aliasとの比較

inline classtypealiasは似ているが相違点がある。



  • 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
}