Kotlinではcontractという仕組みを使って、コンパイラーとの約束ができます。
この仕組みが使われている、最も身近な例の一つがisNullOrEmpty()
でしょう。
以下のようにisNullOrEmpty()
がfalseを返した場合、そのレシーバーはNonNullとして判断され、スマートキャストされます。
if (!a.isNullOrEmpty()) {
a.get(0)
}
例えば、isNullOrEmpty()
を独自実装すると以下のようになると思います。
inline fun <T> Collection<T>?.isNullOrEmpty(): Boolean =
this == null || this.isEmpty()
しかし、このメソッドを使った場合は、スマートキャストしてくれません。
コンパイラーがisNullOrEmpty()
の判定と、レシーバーがnullか否かの関係を知らないからです。
isNullOrEmpty()
の実装は以下のようになっています。
@SinceKotlin("1.3")
@kotlin.internal.InlineOnly
public inline fun <T> Collection<T>?.isNullOrEmpty(): Boolean {
contract {
returns(false) implies (this@isNullOrEmpty != null)
}
return this == null || this.isEmpty()
}
ここで、contractの構文が使われています。
DSLが使われており、contractの仕組みを知らなくてもなんとなく意味が分かると思います。
このメソッドがfalseを返した場合は、レシーバがnullでないことを意味する。みたいなことが書かれているみたいですね。
kotlin.contractsの使い方
kotlin.contractsはSDKの中でしか使えない機能というわけでは無く、ユーザープログラム上で使うこともできます。
ただし、
@OptIn(ExperimentalContracts::class)
のアノテーションが必要です。まだ実験的な機能ですので、将来的に使えなくなったり仕様が変化する可能性があるのでご注意ください。
戻り値とレシーバ・引数の型情報を伝える
kotlin.contractsでは、拡張関数のレシーバ、もしくは引数の型情報を伝えることができます。
レシーバを使う場合、以下のように定義し
@OptIn(ExperimentalContracts::class)
private fun Any?.isNotNull(): Boolean {
contract {
returns(true) implies (this@isNotNull != null)
}
return this != null
}
以下のように使うことで、戻り値がtrueの時、レシーバがNonNullとして扱われます
if (a.isNotNull()) {
a.length
}
引数の情報を返すなら以下のように定義します。
@OptIn(ExperimentalContracts::class)
private fun isNotNull(value: Any?): Boolean {
contract {
returns(true) implies (value != null)
}
return value != null
}
returns()
では戻り値が、true/false/nullの場合の条件を、returnsNotNull()
であれば戻り値がNonNullの場合の条件を指定することができます。
例えば、以下のように使い、戻り値がNonNullの場合、レシーバーもNonNullであると伝えることができます。
@OptIn(ExperimentalContracts::class)
private fun Any?.toInt(): Int? {
contract {
returnsNotNull() implies (this@toInt != null)
}
return this as? Int
}
戻り値の条件は、true/false/null/NonNullだけで、それ以外の条件、例えば、戻り値が0なら〜のような条件は指定できません。
また、注意点として、以下のように、レシーバーがnullであると伝えても
@OptIn(ExperimentalContracts::class)
private fun Any?.isNull(): Boolean {
contract {
returns(true) implies (this@isNull == null)
}
return this == null
}
以下のように、否定がNonNullであるとは判定してくれません。
if (!a.isNull()) {
a.length
}
NonNollの条件を伝える必要があります。
@OptIn(ExperimentalContracts::class)
private fun Any?.isNull(): Boolean {
contract {
returns(false) implies (this@isNull != null)
}
return this == null
}
Nullable以外に、型情報を伝えることもできます。
例えば、以下のように定義することで、
sealed interface Foo {
data class Valid(val value: Int) : Foo
object Invalid: Foo
}
@OptIn(ExperimentalContracts::class)
fun Foo.isValid(): Boolean {
contract {
returns(true) implies (this@isValid is Foo.Valid)
}
return this is Foo.Valid
}
以下のように、型判定したのと同様に、スマートキャストが使えるようになります。
if (foo.isValid()) {
foo.value // Validにスマートキャストされている
ラムダの実行回数を伝える
callsInPlace
でラムダの実行回数を伝えることができます。
例えば、以下のようにラムダを実行するメソッドがあったとして、
private inline fun run(block: () -> Unit) {
block()
}
以下の記述は、通常コンパイルエラーとなります。
val a: String?
run {
a = "Hello"
}
a
はval
宣言されているため、初期値の指定として1回しか代入が許されません。しかし、block
が何回実行されるのか、実行されない可能性がないのかが分からないため、この記述を許可して良いのかをコンパイラーが判断できないためです。
contract
を使って以下のように記述すると、block
が1回だけ必ず実行されることを約束できます。
@OptIn(ExperimentalContracts::class)
private inline fun run(block: () -> Unit) {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
}
これにより、以下の記述はコンパイルできるようになります。
val a: String?
run {
a = "Hello"
}
実行回数については、以下のような条件が指定可能です。
指定 | 意味 |
---|---|
AT_MOST_ONCE | 最大でも1回実行される |
AT_LEAST_ONCE | 最低でも1回実行される |
EXACTLY_ONCE | 必ず1回実行される |
UNKNOWN | 実行回数は不明 |
「このメソッドの中でNullチェックしているから、ここではNonNullであることは確実なのにスマートキャストしてくれない。not-null assertion operator (!!) は使いたくないし、無駄に再チェックも書きたくない」
というのはKotlin使っていてよくあるシーンですが、kotlin.contractsを使うことで、少し便利に書けるようになるかもしれません。
以上です。