1概要

このチュートリアルでは、https://kotlinlang.org/docs/reference/whatsnew13.html#contracts[Kotlin Contracts]について説明します。それらの構文はまだ安定していません、しかしバイナリ実装はそうです、そして、Kotlin

__stdlib

__それ自身はすでにそれらを使用するようにしています。

  • 基本的に、Kotlin規約は、関数の動作についてコンパイラに通知する方法です。


2 Mavenのセットアップ

この機能はKotlin 1.3で導入されたので、このバージョンかそれより新しいものを使う必要があります。このチュートリアルでは、https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.jetbrains.kotlin%22%20AND%20a%3A%22kotlin-stdlib%22[we’ll入手可能な最新版を使用する] – 1.3.10。

設定の詳細については、https://www.baeldung.com/kotlin[Kotlinの紹介]を参照してください。


3契約の動機

  • コンパイラのように賢いとはいえ、必ずしも最良の結論に達するとは限りません。

以下の例を見てください。

data class Request(val arg: String)

class Service {

    fun process(request: Request?) {
        validate(request)
        println(request.arg)//Doesn't compile because request might be null
    }
}

private fun validate(request: Request?) {
    if (request == null) {
        throw IllegalArgumentException("Undefined request")
    }
    if (request.arg.isBlank()) {
        throw IllegalArgumentException("No argument is provided")
    }
}


validate

を呼び出しても例外がスローされない場合、すべてのプログラマがこのコードを読んで

request



null

ではないことを知ることができます。言い換えれば、

__ println


命令が

NullPointerExceptionをスローすることは不可能です。

残念ながら、コンパイラはそれを認識しておらず、

request.arg

を参照することを許可していません。

ただし、関数が正常に戻った場合(つまり、例外がスローされない場合)、指定された引数が

null

ではないことを定義する規約によって

validate

を拡張できます。

@ExperimentalContracts
class Service {

    fun process(request: Request?) {
        validate(request)
        println(request.arg)//Compiles fine now
    }
}

@ExperimentalContracts
private fun validate(request: Request?) {
    contract {
        returns() implies (request != null)
    }
    if (request == null) {
        throw IllegalArgumentException("Undefined request")
    }
    if (request.arg.isBlank()) {
        throw IllegalArgumentException("No argument is provided")
    }
}

次に、この機能をさらに詳しく見てみましょう。


4契約API

一般契約書は次のとおりです。

function {
    contract {
        Effect
    }
}

これを「関数を呼び出すと効果が出る」と読むことができます。

次のセクションでは、言語が現在サポートしている効果の種類を見てみましょう。

** 4.1. 戻り値に基づいて保証する

  • ここでは、target関数が戻ったときにtarget条件が満たされるように指定しています。


returns

に値を指定することもできます。これは、ターゲット値が返された場合にのみ条件が満たされることをKotlinコンパイラに指示します。

data class MyEvent(val message: String)

@ExperimentalContracts
fun processEvent(event: Any?) {
    if (isInterested(event)) {
        println(event.message)
    }
}

@ExperimentalContracts
fun isInterested(event: Any?): Boolean {
    contract {
        returns(true) implies (event is MyEvent)
    }
    return event is MyEvent
}

これにより、コンパイラは

processEvent

関数でスマートキャストを行うことができます。

  • 現時点では、

    return

    規約では、

    の右側にある

    true



    false

    、および

    null__のみが許可されています。


implies



Boolean

引数を取りますが、有効なKotlin式のサブセットのみが受け入れられます。つまり、

null

-checks(

== null



__!


= null)、instance-checks(

is



!is

)、論理演算子(


null

以外の戻り値をターゲットにしたバリエーションもあります。

contract {
    returnsNotNull() implies (event is MyEvent)
}

** 4.2. 機能の使用について保証する


callsInPlace

契約は、以下の保証を表します。


  • callable

    はowner-functionが終了した後には呼び出されません

  • 契約なしに他の機能に渡すこともできません。

これは以下のような状況で私たちを助けます:

inline fun <R> myRun(block: () -> R): R {
    return block()
}

fun callsInPlace() {
    val i: Int
    myRun {
        i = 1//Is forbidden due to possible re-assignment
    }
    println(i)//Is forbidden because the variable might be uninitialized
}

与えられたブロックが確実に呼び出され、一度だけ呼び出されることを保証するようにコンパイラに手助けすることでエラーを修正できます。

@ExperimentalContracts
inline fun <R> myRun(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY__ONCE)
    }
    return block()
}

標準のKotlinユーティリティ関数

run



with



apply

などはすでにそのような規約を定義しています。

ここでは

InvocationKind.EXACTLY

ONCE

を使用しました。 ** 他のオプションは

AT

LEAST

ONCE



AT

MOST

ONCE

、および

UNKNOWN __です。


5契約の制限

Kotlinの契約は有望に見えますが、現在の構文は現時点では不安定であり、将来完全に変更される可能性があります。

また、いくつかの制限があります。

  • 私たちは、ボディを持つトップレベルの関数にのみ契約を適用できます

フィールドやクラス関数には使用できません。

  • 契約呼び出しは、関数本体の最初の文でなければなりません。

コンパイラは無条件に契約を信頼します。これは

プログラマーは、正しい、そして正しい契約を書く責任があります。

将来のバージョンは検証を実装するかもしれません

そして最後に、契約の説明ではパラメータへの参照のみが許可されています。

たとえば、以下のコードはコンパイルされません。

data class Request(val arg: String?)

@ExperimentalContracts
private fun validate(request: Request?) {
    contract {
       //We can't reference request.arg here
        returns() implies (request != null && request.arg != null)
    }
    if (request == null) {
        throw IllegalArgumentException("Undefined request")
    }
    if (request.arg.isBlank()) {
        throw IllegalArgumentException("No argument is provided")
    }
}


6. 結論

この機能はかなり面白く見え、その構文はプロトタイプ段階にありますが、バイナリ表現は十分に安定しており、すでに

stdlib

の一部です。グレースフルな移行サイクルがなくても変更はありません。つまり、通常の互換性を保証するために、契約を持つバイナリアーティファクト(

stdlib

など)に頼ることができます。

  • だからこそ、今でも契約を使用する価値があると私たちが推奨しているのです。** DSLが変更されても、契約宣言を変更するのはそれほど困難ではないでしょう。

いつものように、この記事で使われているソースコードはhttps://github.com/eugenp/tutorials/tree/master/core-kotlin[GitHubで利用可能]です。