Контракты contracts функции

Контракты в Kotlin — это мощный инструмент, позволяющий разработчику предоставлять компилятору дополнительную информацию о поведении функций. Эта информация помогает компилятору лучше понимать логику программы и, как следствие, производить более точную проверку типов, а также улучшать оптимизацию кода.

Основные понятия

Контракт — это декларация, описывающая гарантии, которые предоставляет функция. Например, контракт может сказать компилятору: "Если функция возвращает false, то переданный аргумент точно не равен null".

Контракты создаются с использованием специального DSL (domain-specific language) из пакета kotlin.contracts. Этот DSL предоставляет несколько ключевых выражений, таких как returns, implies, и callsInPlace.

Ключевые выражения контрактов

  1. returns:

    • returns описывает условие возвращаемого значения функции.
    • Например, returns(true) означает, что функция возвращает true в определенных условиях.
  2. implies:

    • implies используется для связывания результата выполнения функции с состоянием какого-либо выражения.
    • Например, returns(false) implies (x != null) означает, что если функция возвращает false, то x не null.
  3. callsInPlace:

    • callsInPlace указывает, как функция передает выполнение лямбды (или другой функции) внутри себя.
    • Например, callsInPlace(block, InvocationKind.EXACTLY_ONCE) означает, что переданная лямбда-функция block будет вызвана ровно один раз.

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

  • Сложность: Контракты могут быть сложны для понимания, особенно для новичков в Kotlin.
  • Ограниченные сценарии: Контракты не поддерживают все возможные условия и виды анализа. Например, они не могут описать поведение, зависящее от внешнего состояния.
  • Inline-функции: Контракты могут быть использованы только в inline функциях, так как это требование для их работы в Kotlin.

Примеры использования контрактов

Проверка на null и оптимизация типа

Рассмотрим функцию, которая проверяет, является ли строка null или пустой. Компилятор должен после вызова этой функции знать, что строка не null, если функция вернула false.

import kotlin.contracts.contract

fun String?.isNullOrEmpty(): Boolean {
    contract {
        returns(false) implies (this@isNullOrEmpty != null)
    }
    return this == null || this.isEmpty()
}

fun example() {
    val str: String? = "Hello"
    if (str.isNullOrEmpty()) {
        println("String is null or empty")
        return
    }

    // Здесь компилятор знает, что str не null, так как str.isNullOrEmpty() вернул false
    println(str.length) // Безопасно использовать str, т.к. она не null
}

В этом примере функция isNullOrEmpty возвращает true, если строка null или пустая. Контракт функции указывает, что если она вернула false, то строка не может быть null. Благодаря этому компилятор знает, что после проверки if (str.isNullOrEmpty()), если условие не сработало, переменная str не будет null, и с ней можно работать как с обычной String.

Контракт с callsInPlace

Рассмотрим случай, когда вы хотите гарантировать, что переданная лямбда-функция будет вызвана определенное количество раз.

import kotlin.contracts.contract
import kotlin.contracts.InvocationKind

inline fun executeExactlyOnce(block: () -> Unit) {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
}

fun example() {
    val x: Int
    executeExactlyOnce {
        x = 42
    }
    // Компилятор знает, что x инициализирован, так как блок был вызван ровно один раз
    println(x) // Выводит: 42
}

В этом примере функция executeExactlyOnce имеет контракт, который говорит компилятору, что переданная лямбда будет вызвана ровно один раз (InvocationKind.EXACTLY_ONCE). Это позволяет компилятору понять, что переменная x, инициализированная внутри лямбды, будет точно инициализирована после вызова executeExactlyOnce.

Пример с Int? - проверка на 0 и null

Представим ситуацию, что нам надо выполнять проверку над Int, и если она не null избегать лишних проверок. В этом нам поможет следующая конструкция

@OptIn(ExperimentalContracts::class)  
fun Int?.izZeroOrNull(): Boolean {  
    contract {  
        returns(false) implies (this@izZeroOrNull != null)  
    }  
    return this == null || this == 0  
}

Используя ее, компилятор точно будет знать что число не null и позволит избегать добавления лишних проверок if