25
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

inline classについてまとめる

Posted at

この記事はKotlin 1.3.10でのinline classについて解説しています。
現状ではExperimentalなAPIなので、破壊的な変更がある場合があります。

inline classes

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

これらの問題を解決するために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
}
25
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?