inline classについてまとめる

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

# inline classes

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

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

これらの問題を解決するために`inline class`が導入されました。
`inline class`は`inline`修飾子をクラスの前に置くことで宣言できます。

```kotlin
inline class Password(val value: String)
```


# 公式ドキュメントにある仕様のまとめ

- 単一のプロパティを持ち、プライマリコンストラクタで初期化しなくてはならない。2つのプロパティを持つことはできない。
- Read-Onlyプロパティと関数を持つことができる。
  - 実際には`static`関数が生成され、呼び出される際にはその関数が呼ばれる。
- `init`ブロックは持てない。
- `inner`クラスを定義できない。
- バッキングフィールドを持つことができない。
  - つまり`lateinit`やデリゲートプロパティを持つことができない。
- インターフェースを実装することができる。
  - 前述の通り実際には`static`関数が生成される。
- プリミティブ型同様にBoxingが可能。

    ```kotlin
    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 class`と`typealias`は似ているが相違点がある。

- `typealias`は、実際には指定されたエイリアス名のクラスは生成されず、エイリアスされた型と相互利用できる。
- `inline class`は、実際にクラスが生成され、ラップしている型とは相互利用できない。

拡張なども`typealias`には癖があるが、`inline class`は通常のクラス同様に扱える。

```kotlin
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
}
```

# その他確認できたこと

#### トップレベルにしか定義できない。

```kotlin
// ネストして定義はできない
data class User(val id: Id) {
    inline class Id(val value: Long)
}
```

#### コンパイル後にラップした型に置換される。

```kotlin
inline class UserId(val value: Int)
inline class UserName(val value: String)
class User(val id: UserId, val name: UserName)
```

上記コードはbytecode上では以下のように解釈される。

```java
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;
   }
}
```

またクラスのメタデータなどもラップした型に置換される。

```kotlin
inline class Foo(val value: String)
println(Foo::class.java.simpleName) // output: String
```

#### Gsonとは問題なく使える

リフレクションを使う場合、ラップしている型としてシリアライズ/デシリアライズされるため、特にAdapterなどを作らなくても利用できる。

```kotlin
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を試したが、実際に生成されるクラスの型情報とコンパイラによりアンラップされた型との不一致により、エラーになるようだった。

```kotlin
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
}
```