この記事を書くきっかけは、長澤さんがKotlin1.3の新機能「Contracts」について発表していた資料が素晴らしかったので、復習も兼ねてアウトプットしようと思ったためです。
長澤さんの資料
Kotlin Contracts #m3kt - Speaker Deck
https://speakerdeck.com/ntaro/kotlin-contracts-number-m3kt
Contractsについて
1.3前はnullチェックしてもスマートキャストできない
val name: String? = ""
if (!name.isNullOrBlank()) { //stdlibの関数
println("Hello, ${name.capitalize()}") // <= エラー
}
エラーメッセージ
Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
「nullableなので ?.
または !!.
じゃないとダメだぞ」
これは自分のプロジェクトでも書いてて遭遇した。
null&空白チェックしてるのになぁ
1.3より前の場合、こうしなきゃいけなかった
val name: String? = ""
if (name != null && name.isNotBlank()) { // <= 1.3前
println("Hello, ${name.capitalize()}")
}
1.3からはスマートキャストが効く
val name: String? = ""
if (!name.isNullOrBlank()) {
println("Hello, ${name.capitalize()}") // <= エラーにならない!
}
Contracts(契約)とは
事後条件
- 関数を呼び出したあとの状況を関数が保証する
ソースで見てみる
isNullOrBrank()
の中を覗いてみる
public inline fun CharSequence?.isNullOrBlank(): Boolean {
contract { // <= 注目ここから
returns(false) implies (this@isNullOrBlank != null)
} // <= ここまで
//falseが返されるとき、この文字列はNotNullであることを意味する、という契約
return this == null || this.isBlank()
}
自作関数に契約
自作してみる
@ExperimentalContracts
fun Any?.isNotNull(): Boolean {
contract { // <= 契約
returns(true) implies (this@isNotNull != null)
}
// trueが返されるとき、thisはNotNullである契約
return this != null
}
作ると分かるが、 @ExperimentalContracts
が必要とIDEに怒られる
契約対応している標準関数
kotlin.testも含む
- assertTrue
- check
- require
- assetFalse
- assertNOtNull
- checkNotNull
- requireNotNull
val変数が絶対初期化されるのにコンパイルエラーになる(Kotlin 1.3前)
val x: Int
run {
x = 12345 // <= ラムダのなかで初期化してるのにコンパイルエラー
}
println(x)
Kotlin 1.3ではContractsによりこれが解決された
runの中を覗いてみる
public inline fun <T, R> T.run(block: T.() -> R): R {
contract { // <= 契約を発見
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
// blockが「ちょうど一度だけ」呼び出される
}
return block()
}
InvocationKindの種類
- AT_MOST_ONCE //関数のパラメータは一度呼び出されるか、まったく呼び出されません。
- AT_LEAST_ONCE //関数のパラメータが1回以上呼び出されます。
- EXACTLY_ONCE //関数パラメータは、1回だけ呼び出されます。
- UNKNOWN //関数のパラメータが呼び出されますが、呼び出すことができる回数は不明です。
契約対応している標準関数
- EXACTLY_ONCE
- run
- with
- apply
- also
- let
- takeIf
- takeUnless
- UNKNOWN
契約DSLまとめ
- contract: 関数の先頭におく必要がある
- returns(): 関数の実行が成功したら
- returns(Any?): 引数の値 true|false|null を返したら
- returnNotNull(): NotNullを返したら
- callsInPlace: 引数の関数が呼ばれる回数を保証
まとめ
- スマートキャストや変数初期化において、面倒なことがKotlin 1.3で解消された
- 契約DSLで契約を表明することでコンパイラに意味が伝わる