概要
Kotlin Contractsとは簡単に言うと、コンパイラに対象のメソッドを呼んだあとは、「想定のエラーは起きませんよ」と伝えることができる機能です。Kotlin1.3から導入されました。まだ、experimentalな機能です。
簡単な例
data class DataDto(val str: String)
private fun process1(dto: DataDto?) {
validate(dto)
println(dto.str) // コンパイルエラー
}
private fun validate(dto : DataDto?) {
if (dto == null) {
throw IllegalArgumentException()
}
}
この例では、dtoはnullableですが、validate()
メソッドの後では、nullはありえません。
しかし、コンパイラはそれがわからないので dto.str
をエラーにします。
これがエラーの内容です。
Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type DataDto?
これをContractsによって、コンパイラに伝えると
+ @ExperimentalContracts
private fun validate(dto : DataDto?) {
+ contract {
+ returns() implies (dto != null)
+ }
if (dto == null) {
throw IllegalArgumentException()
}
これだけで、validate()
メソッドを呼んだ後のdtoはnullはあり得ないことが、コンパイラに伝わります。
そのため dto.str
はコンパイルが通ります。
制限事項
引数だけ対象になっています。
たとえば、以下のようにDataDto#str
がnullでない場合を保証しようとしてもできません。
data class DataDto(val str: String?)
@ExperimentalContracts
private fun process1(dto: DataDto?) {
validate(dto)
println(dto.str) // コンパイルエラー
}
@ExperimentalContracts
private fun validate(dto : DataDto?) {
contract {
returns() implies (dto?.str != null) // コンパイルエラー
}
if (dto?.str == null) {
throw IllegalArgumentException()
}
条件指定
returns
returns
で返した値の場合だけコンパイルエラーを回避できるようできます。
以下の例は、returns(true)
でtrueの場合だけ、コンパイルが通るようになります。
@ExperimentalContracts
private fun process2(dto: Any) {
if (isDataDto(dto)){
println(dto.str) // コンパイルエラーなし
}
}
@ExperimentalContracts
fun isDataDto(dto: Any): Boolean {
contract {
returns(true) implies (dto is DataDto)
}
return dto is DataDto
}
isDataDto()
がtrueかどうかわからない場合は、当然コンパイルエラーになります。
@ExperimentalContracts
private fun process2(dto: Any) {
if (isDataDto(dto)){
println(dto.str) // コンパイルエラーなし
}
+ println(dto.str) // コンパイルエラー
}
returnsNotNull
正直、よくわかってないです。わかる方教えて欲しいです。
メソッドの戻り値で、nullがこないってこと?
だとしたら、メソッド定義でnot nullにすればいいだけな気がしています。
callsInPlace
関数が呼ばれる回数を保証して、コンパイルエラーを回避させます。
AT_MOST_ONCE=0 or 1 、AT_LEAST_ONCE=1以上、EXACTLY_ONCE=1、UNKNOWN=何回でも、を指定できます。
@ExperimentalContracts
inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
@ExperimentalContracts
fun process3() {
val i: Int
run {
i = 1 // callsInPlaceがなければ、valに二度設定できるかもしれないのでエラーになる。
}
println(i) // callsInPlaceがなければ、設定してるかわからないのでエラーになる。
}