LoginSignup
8
9

kotlin.contractsの使い方

Posted at

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宣言されているため、初期値を一回しか代入できません。しかし、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を使うことで、少し便利に書けるようになるかもしれません。

以上です。

8
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9