Kotlin-Verträge

Kotlin-Verträge

1. Überblick

In diesem Tutorial werden wir überKotlin Contracts sprechen. Ihre Syntax ist noch nicht stabil, aber die binäre Implementierung ist es, und Kotlinstdlib itself setzt sie bereits ein.

Grundsätzlich sind Kotlin-Verträge eine Möglichkeit, den Compiler über das Verhalten einer Funktion zu informieren.

2. Maven Setup

Diese Funktion wird in Kotlin 1.3 eingeführt, daher müssen wir diese oder eine neuere Version verwenden. Für dieses Tutorialwe’ll use the latest version available - 1.3.10.

Weitere Informationen zum Einrichten finden Sie in unserenintroduction to Kotlin.

3. Motivation für Verträge

So intelligent der Compiler auch ist, er kommt nicht immer zum besten Ergebnis.

Betrachten Sie das folgende Beispiel:

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")
    }
}

Jeder Programmierer kann diesen Code lesen und wissen, dassrequest nichtnull ist, wenn ein Aufruf vonvalidate keine Ausnahme auslöst. In other words, it’s impossible for our println instruction to throw a NullPointerException.

Leider ist sich der Compiler dessen nicht bewusst und erlaubt uns nicht, aufrequest.arg zu verweisen.

Wir können jedochvalidate durch einen Vertrag verbessern, der definiert, dass das angegebene Argument nichtnull ist, wenn die Funktion erfolgreich zurückgibt - das heißt, sie löst keine Ausnahme aus:

@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")
    }
}

Schauen wir uns diese Funktion als Nächstes genauer an.

4. Die Vertrags-API

Das allgemeine Vertragsformular lautet:

function {
    contract {
        Effect
    }
}

Wir können dies als "Aufrufen der Funktion erzeugt den Effekt" lesen.

In den folgenden Abschnitten werfen wir einen Blick auf die Arten von Effekten, die die Sprache jetzt unterstützt.

4.1. Garantien basierend auf dem Rückgabewert

Here we specify that if the target function returns, the target condition is satisfied. Wir haben dies im AbschnittMotivation verwendet.

Wir können auch einen Wert inreturns angeben, der den Kotlin-Compiler anweist, dass die Bedingung nur erfüllt ist, wenn der Zielwert zurückgegeben wird:

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
}

Dies hilft dem Compiler, eine intelligente Umwandlung in der FunktionprocessEventvorzunehmen.

Beachten Sie, dassreturns Verträge derzeit nurtrue,false undnull auf der rechten Seite von implies. zulassen

Und obwohlimplies einBoolean-Argument akzeptiert, wird nur eine Teilmenge gültiger Kotlin-Ausdrücke akzeptiert:null-Checks (== null,! = null ), Instanzprüfungen (is,!is), Logikoperatoren (&&,||,!).

Es gibt auch eine Variation, die auf einen Rückgabewert von nichtnullabzielt:

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

4.2. Garantien für die Verwendung einer Funktion geben

DercallsInPlace-Vertrag enthält die folgenden Garantien:

  • Diecallablewerden nach Abschluss der Eigentümerfunktion nicht mehr aufgerufen

  • Es wird auch nicht ohne Vertrag an eine andere Funktion weitergegeben

Dies hilft uns in folgenden Situationen:

inline fun  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
}

Wir können die Fehler beheben, indem wir dem Compiler helfen, sicherzustellen, dass der angegebene Block garantiert und nur einmal aufgerufen wird:

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

Standard-Kotlin-Dienstprogrammfunktionenrun,with,apply usw. definieren solche Verträge bereits.

Hier haben wirInvocationKind.EXACTLY_ONCE verwendet. Other options are AT_LEAST_ONCEAT_MOST_ONCE, and UNKNOWN.

5. Vertragsbeschränkungen

Während Kotlin-Verträge vielversprechend aussehen, sindthe current syntax is unstable at the moment, and it’s possible that it will be completely changed in the future.

Sie haben auch ein paar Einschränkungen:

  • Wir können Verträge nur für Funktionen auf oberster Ebene mit einem Körper abschließen, d. H. Wir können sie nicht für Felder und Klassenfunktionen verwenden.

  • Der Vertragsaufruf muss die erste Anweisung im Funktionskörper sein.

  • Der Compiler vertraut den Verträgen vorbehaltlos. Dies bedeutet, dass der Programmierer für das Schreiben korrekter und solider Verträge verantwortlich ist. A future version may implement verification.

Und schließlich erlauben Vertragsbeschreibungen nur Verweise auf Parameter. Der folgende Code wird beispielsweise nicht kompiliert:

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. Fazit

Das Feature sieht ziemlich interessant aus und obwohl sich seine Syntax im Prototypenstadium befindet, ist die binäre Darstellung stabil genug und bereits Teil vonstdlib. Es wird sich ohne einen ordnungsgemäßen Migrationszyklus nicht ändern, und das bedeutet, dass wir uns auf binäre Artefakte mit Verträgen verlassen können (z. stdlib), um alle üblichen Kompatibilitätsgarantien zu haben.

That’s why our recommendation is that it’s worth using contracts even now - Es wäre nicht allzu schwierig, Vertragserklärungen zu ändern, wenn sich ihr DSL ändert.

Wie üblich ist der in diesem Artikel verwendete Quellcodeover on GitHub verfügbar.