プログラマ英語学習日記

プログラミングと英語学習のまとめなど

Kotlin inline class 入門

Kotlin1.3で追加された inline class、あまり存在自体知られてないのではないかと思います。

ところがこれが使いこなすと結構便利。そこで今回は inline class自体と、簡単な活用方法を紹介します。

inline class

inline関数が実際には関数が生成されないように、inline-class も実態クラスは生成されずコンパイラが頑張って展開してくれます。このためパフォーマンス向上が期待できます。

ただし制限があり、 inline classはvalのプロパティ一個のみ です。0個もだめ。1つ必要でかつvarは不可能。val2つ以上も不可能です。

inline class Second(
  val value: Int
)

dataclassの変形版ですね。大げさなイメージですが、 typealias Second = Int とするのとよく似ています。classとなっていますが、一つの型に別名をつけると思ったほうが理解しやすいでしょう。


typealiasとの違い

typealiasと異なり、inline-classは きっちりコンパイラが型チェックします。実例をみてみましょう。まずはinlineから

inline class Second(
  val value: Int
)

fun sleep(time: Second) {
  /* A */
}
fun sleep(time: Int) {
  /* B */
}

fun main() {
  val s = Second(100)
  sleep(s)  //Aにいく
  sleep(100)  //Bにいく
  
  //これはできない
  //val s = 100 as Second
}

普通のクラスの挙動ですね。違和感ないと思います。一方でtypealiasだと...?

typealias Second = Int

fun sleep(time: Second) {
  /* A */
}
//この時点で二重定義ビルドエラー
//fun sleep(time: Int) { 
//  /* B */
//}

fun main() {
  //sleep(Second(100))  //生成できない
  sleep(100)  //Aにいく

  val s: Second = 100 //これは可能
  sleep(s)
}

本当に別名にすぎません。このため Int/Second でのメソッドオーバーロードが不可能。ビルドエラーです。
コードが読みやすくなるとは思いますが、型としての強制力をもちません。


仕組み自体は難しくないと思います。大げさにいうとプロパティが一個に限定されたdata-classです。まとめると

  • data-class
    • 複数個プロパティが持てる
    • var/valとも可能
  • typealias
    • 型としての強制力がない
    • 単純な名前の置き換えにすぎないので高速
  • inline-class
    • val一個のみ
    • クラスとして生成されない(=高速)
    • (inlineしきれずクラスになってしまう場合もあるらしい...未調査)

inline-class は data-class と typealias の中間的存在といえるかもしれませんね。特にクラス生成はヒープにメモリ空間を確保してしまいます。inlineはそこを回避できるのが大きい。

実用例

では実用的な例をみてみましょう。inline-classの仕様上、簡単な値に明示的に意味をもたせたいときに非常に向いています。

たとえば sleep(100)。この 100 の単位は何でしょう?秒?ミリ秒?マイクロ秒? こういうときこそ inline-class の出番です

inline class Second(val value: Int)
inline class MilliSecond(val value: Int)
inline class MicroSecond(val value: Int)

fun sleep(s: Second) {}
fun sleep(s: MilliSecond) {}
fun sleep(s: MicroSicond) {}

fun main() {
  sleep(MilliSecond(100))
}

単位がはっきりしましたね、非常にわかりやすいです。何秒まつのか明確です。

さらにこの上、 inlineとはいえクラスなのでインターフェース実装ができます。上の3つの単位クラスをinterfaceで共通化させましょう。

//interface定義。ナノ秒取得可能
interface TimeValue {
    val nanoSec: Long
}
//各単位を宣言
inline class Second(val value: Int): TimeValue {
    override val nanoSec: Long
        get() = value.toLong() * 1_000_000_000L
}
inline class MilliSecond(val value: Int): TimeValue {
    override val nanoSec: Long
        get() = value.toLong() * 1_000_000L
}
inline class MicroSecond(val value: Int): TimeValue {
    override val nanoSec: Long
        get() = value.toLong() * 1_000L
}

//それを使った関数
fun debugPrint(time: TimeValue) {
      val nano = time.nanoSec 
    println(nano)
}
fun main() {
    debugPrint(Second(1))   // 1000000000
    debugPrint(MilliSecond(2)) // 2000000
    debugPrint(MicroSecond(3)) // 3000
}

このようにしてクラスと同様にInterfaceで処理をまとめることができます。これなら単位追加も楽。わざわざ単位追加のたびに関数定義する必要がありません。

もう少し楽したい場合、拡張関数を使うと便利です。 100.msec のように。コードは大したこと無いので省略。


少し頑張る必要がありますが、次のようなコードも面白いです

inline class UserID(id: Long)
inline class TweetID(id: Long)

data class Tweet(
  val id: TweetID,
  val userId: UserId,
  //他
)

fun delete(id: TweetID) { }
fun delete(id: UserID) { }

//fun deleteTweet(id: Long) { }

まず delete関数ですが、引数を見るだけでツイートを削除するのかuserを削除するのかがひと目で分かります。その上、全く関係ないIDを渡してしまうというミスを防げます。

deleteTweet関数の引数がLongだと、deleteTweetにUserIDや関係ないIDを渡してしまえます。inlineなら不可能(意図的に生成する必要がある)


ただしこの方法で面倒くさいのが、そのままだとORマッパーが壊滅なこと。データベースから読み込む場合は一般にInt/Longにマップされてしまうので、このinline-classがあると正常に動作しません。一度読み込んだ値を明示的にコードで変換する必要があります。コードとしては安全性があがる反面、手間に見合うかというと微妙なところです。

まとめ

あまり使われていない気がするinline-classについてまとめてみました。使い所は多くないとは思いますが、うまく使うと書くときの安全性/読みやすさとも向上します。

特に単純な数値/文字列に明示的な意味を与えたい場合に強力!ぜひ活用してみてください。