Kotlinのcontractについて勉強する
これは何?
Kotlinのライブラリを見るとよくcontractというものが出てきます。普段のモバイルアプリ開発では使ったことがない言語仕様です。
最近、ライブラリの中を覗くのにハマっており、よくcontractが出てくるのでこの言語使用について勉強していきます。
TL;DR
- この関数を通れば必ずnullじゃないのに、何でsmart castしてくれないんだ?
- ラムダ内でローカル変数を初期化しているのに、何で初期化済みだと処理してくれないんだ?
このような場合において、contractによってKotlinコンパイラに情報を伝えて賢く解析してもらうときに利用できる。
contractは実行時の処理ではなく、コンパイル時にこの実装はこのような性質があると性的解析を助けるためのもの。
contactのcallsInPlace
val name: String
initialize {
name = "Hello"
}
println(name) // Variable 'name' must be initialized.
fun initialize(block: () -> Unit) {
block()
}上記のコードはコンパイルエラーになります。
人間の目からはnameを初期化していることがわかりますが、コンパイラは分かりません。そのためcontractで情報を伝えます。
なぜ初期化されていないとなるのかというと、initialize関数の中で引数のラムダが実行される保証がありません。実行されないとnameは初期化されません。
このような場面でcallsInPlaceが利用できます。ラムダの実行回数を保証します。
val name: String
initialize {
name = "Hello"
}
println(name)
@OptIn(ExperimentalContracts::class)
fun initialize(block: () -> Unit) {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
}initialize関数でblockが1度実行されることを保証しており、初期化されることが保証でき、コンパイルが通ります。
contractのreturns
val user: User? = null
requireUser(user)
user.id // Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'User?'.
data class User(val id: Long)
fun requireUser(user: User?) {
if (user == null) {
error("User is required")
}
}人間の目から見るとrequireUserを通る時点でuserが非nullであることがわかりますが、コンパイラはわからないためコンパイルエラーになってしまいます。
このような場合にreturnsが利用できます。
val user: User? = null
requireUser(user)
user.id
data class User(val id: Long)
@OptIn(ExperimentalContracts::class)
fun requireUser(user: User?) {
contract {
returns() implies (user != null)
}
if (user == null) {
error("User is required")
}
}requireUserが正常終了する時点でuserが非nullであることをコンパイルに伝えることができ、requireUser以降、userは非nullとして処理されます。
まとめ
contractにより、Kotlinコンパイラに情報を渡し、解析をより賢くする仕組みであることがわかりました。