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についてまとめてみました。使い所は多くないとは思いますが、うまく使うと書くときの安全性/読みやすさとも向上します。
特に単純な数値/文字列に明示的な意味を与えたい場合に強力!ぜひ活用してみてください。