Котлин Контракты

Котлин Контракты

1. обзор

В этом уроке мы поговорим оKotlin Contracts. Их синтаксис еще нестабилен, но бинарная реализация стабильна, и Kotlinstdlib уже использует их.

По сути, контракты Kotlin - это способ сообщить компилятору о поведении функции.

2. Maven Setup

Эта функция представлена ​​в Kotlin 1.3, поэтому нам нужно использовать эту версию или более новую. В этом руководствеwe’ll use the latest version available - 1.3.10.

Пожалуйста, обратитесь к нашемуintroduction to 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")
    }
}

Любой программист может прочитать этот код и знать, чтоrequest неnull, если вызовvalidate не вызывает исключения. In other words, it’s impossible for our println instruction to throw a NullPointerException.

К сожалению, компилятор не знает об этом и не позволяет нам ссылаться наrequest.arg.

Тем не менее, мы можем улучшитьvalidate с помощью контракта, который определяет, что если функция успешно возвращается, то есть не генерирует исключение, то данный аргумент неnull:

@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. Предоставление гарантий на основе возвращаемой стоимости

Here we specify that if the target function returns, the target condition is satisfied. Мы использовали это в разделеMotivation.

Мы также можем указать значение в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.

Обратите внимание, что на данный момент контрактыreturns допускают толькоtrue,false иnull в правой части implies..

И хотяimplies принимает аргументBoolean, принимается только подмножество допустимых выражений Kotlin: а именно,null-check (== null,! = null ), проверки экземпляра (is,!is), логические операторы (&&,||,!).

Также существует вариант, нацеленный на любое возвращаемое значение, отличное отnull:

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

4.2. Предоставление гарантий относительно использования функции

КонтрактcallsInPlace выражает следующие гарантии:

  • callable не будет вызываться после завершения функции-владельца

  • он также не будет передан другой функции без контракта

Это помогает нам в следующих ситуациях:

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
}

Мы можем исправить ошибки, помогая компилятору гарантировать, что данный блок будет гарантированно вызываться и вызываться только один раз:

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

Стандартные служебные функции Kotlinrun,with,apply и т. Д. Уже определяют такие контракты.

Здесь мы использовалиInvocationKind.EXACTLY_ONCE. Other options are AT_LEAST_ONCEAT_MOST_ONCE, and UNKNOWN.

5. Ограничения контрактов

Хотя контракты на Kotlin выглядят многообещающими,the current syntax is unstable at the moment, and it’s possible that it will be completely changed in the future.

Также у них есть несколько ограничений:

  • Мы можем применять контракты только к функциям верхнего уровня с телом, т.е. мы не можем использовать их в полях и функциях классов.

  • Контрактный вызов должен быть первым оператором в теле функции.

  • Компилятор доверяет контрактам безоговорочно; это означает, что программист несет ответственность за написание правильных и надежных контрактов. A future version may implement verification.с

И наконец, описания контрактов допускают только ссылки на параметры. Например, приведенный ниже код не компилируется:

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), чтобы иметь все обычные гарантии совместимости.

That’s why our recommendation is that it’s worth using contracts even now - не составит большого труда изменить декларации контрактов, если и когда их DSL изменится.

Как обычно, исходный код, используемый в этой статье, доступенover on GitHub.