Привет! Меня зовут Виталий. Я — Android‑разработчик в Альфа‑Банке. За время собеседований я заметил одну любопытную вещь: даже опытные котлиноводы частенько не в курсе такой мощной фичи, как Kotlin Contracts.

Этой серией статей я хочу простым человеческим языком показать, зачем нужны контракты в Kotlin, как их использовать на практике и как они работают внутри.
Апдейт. А вот и вторая часть из серии. В ней я изучил то, о чём в документации не пишут: как парсится список эффектов, как работает новый Contracts API изнутри, и почему, чёрт возьми, на уровне компилятора можно использовать контракты не только на уровне функций.
Какую проблему решают Kotlin Contracts?
Все мы любим Kotlin за умные проверки типов. Например, напишешь так:
fun foo(x: Any) { if (x is List<*>) { x.size // Всё ок, компилятор молодец! } }
И всё работает!
Но стоит вынести ту же проверку в отдельную функцию:
fun isList(x: Any): Boolean = x is List<*> fun foo(x: Any) { if (isList(x)) { x.size // Ошибка: "Unresolved reference: size" } }
В чём подвох?
Компилятор больше не верит, что после isList(x) переменная x — это List, ведь он не знает, что делает ваша функция.
Вот тут и начинается магия Kotlin Contracts!
С помощью специальной подсказки (контракта) можно объяснить компилятору, как именно устроена ваша логика проверки, и получить такой же «умный» smart‑cast, как и с оператором is, только уже с вашей собственной функцией.
Посмотрим на примере работы функции isList.
@OptIn(ExperimentalContracts::class) fun isList(x: Any): Boolean { contract { returns(true) implies (x is List<*>) } return x is List<*> } fun foo(x: Any) { if (isList(x)) { x.size // Всё ок 👍🏻 } }
С помощью такого незамысловатого синтаксиса мы говорим Kotlin компилятору: «Если функция вернула true, то x гарантированно типа List<*>».

Давайте вместе залезем внутрь Kotlin Contracts DSL и разберём, какие ещё возможности открывает нам эта удивительная фича.
Contracts DSL
Все функции, связанные с Contracts DSL, располагаются всего в одном небольшом файле ContractBuilder.
Посмотрим, из чего состоит функция contract, с которой начинается взаимодействие c контрактами
public inline fun contract(builder: ContractBuilder.() -> Unit) { }
Здесь всё просто: inline — функция, которая в качестве параметра принимает лямбду с receiver'ом типа ContractBuilder. Данный трюк часто используется для описания своего DSL.
Примечание. Подробнее с возможностями Kotlin DSL вы можете ознакомиться в официальной Kotlin документации Type‑safe builders.
Теперь посмотрим, что из себя представляет ContractBuilder.
public interface ContractBuilder { public fun returns(): Returns public fun returns(value: Any?): Returns public fun returnsNotNull(): ReturnsNotNull public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace }
Как видим, в контракте нам доступны 4 функции. Каждая из них возвращает объект‑эффект (Effect), о котором мы чуть позже поговорим отдельно.
А пока давайте посмотрим, как каждая из этих функций из ContractBuilder работает в реальном коде и что они позволяют делать.
Если вы уже знаете как читать Kotlin контракты — смело переходите к разделу Contracts API!
Функция returns()
returns() — описывает ситуацию, когда функция завершает свою работу без каких‑либо исключений.
Функция возвращает эффект Returns, который реализует интерфейс SimpleEffect. Внутри этого эффекта прописана функция implies, позволяющая прописывать условия, н�� которые Kotlin компилятор может гарантированно полагаться, если выполнится эффект SimpleEffect (в частности, если выполнится эффект Returns).
public infix fun implies(booleanExpression: Boolean): ConditionalEffect
returns() на практике:
public inline fun check(value: Boolean): Unit { contract { returns() implies value } if (!value) { throw IllegalStateException("Check failed.") } } fun foo(str: String?) { check(str != null) str.length // Тут компилятор уже знает, что "str != null" }
Компилятор читает этот контракт следующим образом: «Если функция завершилась без исключений, то
valueистинно (value == true)».
После того как функция check(Boolean) успешно отработает (выполнится эффект Returns), Kotlin компилятор подкинет во Flow анализа данных информацию из параметра функции implies(str != null).
Функция returns(value: Any?)
returns(value: Any?) описывает ситуацию, когда функция завершает свою работу без каких‑либо исключений, и при этом функция возвращает значение указанное в параметре value.
В качестве аргумента в параметр value можно передать одно из трёх значений: true, false и null.
Посмотрим пример:
public inline fun CharSequence?.isNullOrEmpty(): Boolean { contract { returns(false) implies (this@isNullOrEmpty != null) } return this == null || this.length == 0 } fun foo(str: String?) { if (!str.isNullOrEmpty()) { str.length // Тут компилятор уже знает, что "str != null" } }
Компилятор читает этот контракт следующим образом: «Если функция завершилась без исключений и вернула
„false“, то истинно выражениеthis@isNullOrEmpty!= null (str!= null)».
Функция returnsNotNull()
returnsNotNull() описывает ситуацию, когда функция завершает свою работу без каких‑либо исключений, и при этом функция возвращает из функции не null.
К сожалению, я не нашёл пример использования подобной функции в стандартной библиотеке Kotlin, но от этого returnsNotNull() не менее полезен. Посмотрим на такой пример:
@OptIn(ExperimentalContracts::class) fun <T : Any> T?.forceCast(): T { contract { returnsNotNull() implies (this@forceCast != null) } if (this == null) { throw IllegalStateException("Object is null") } else { return this } } fun foo(str: String?) { val nonNullableStr = str.forceCast() nonNullableStr.length // Тут компилятор уже знает, что "nonNullableStr != null" }
Здесь компилятор читает этот контракт следующим образом: «Если функция завершилась без исключений и вернула не
null, то истинно выражениеthis@forceCast!= null (str!= null)».
Функция callsInPlace(Function, InvocationKind)
callsInPlace(Function, InvocationKind) передаёт компилятору информацию о том, что лямбда, переданная в функцию, не будет вызвана после завершения функции, а также опционально передает информацию о том, сколько раз может вызваться лямдба внутри функции. Это позволяет Kotlin компилятору снять механизм защиты на инициализацию val свойства внутри лямбды.
Посмотрим, как callsInPlace(Function, InvocationKind) работает на таком примере:
public inline fun <R> run(block: () -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block() } fun foo() { val x: Int run { x = 1 // Тут компилятор уже знает, лямбда вызовется только 1 раз, а значит не перезатрём значение `val` } println(x) }
Компилятор читает этот контракт следующим образом: «Лямбда
blockбудет вызвана только внутри текущей функции и будет вызвана гарантированно один раз».
InvocationKind определяет как много раз лябда переданная в функцию может быть вызвана внутри функции:
public enum class InvocationKind { AT_MOST_ONCE, // Будет вызвана 0 или 1 раз AT_LEAST_ONCE, // Будет вызвана [1..] раз EXACTLY_ONCE, // Будет вызвана ровно 1 раз UNKNOWN // Может быть вызвана любое кол-во раз }
Ну что ж, с «детскими» вопросами разобрались: зачем нужны контракты, где они уже применяются и как выглядят в стандартных функциях.
Но на этом магия не заканчивается — давайте нырнём чуть глубже и посмотрим, как этот API устроен изнутри: какие там есть эффекты, как они сочетаются, и как на самом деле выглядит «контрактная кухня» Kotlin.
Contracts API
Общий вид контракта такой:
[ effect_1 effect_2 ... effect_n ] fun function() { ... }
Можно читать это так: «вызов этой функции приводит к списку эффектов effect_1, effect_2,..., effect_n».
В частности, функция с контрактами может выглядеть следующим образом:
@OptIn(ExperimentalContracts::class) fun <T : Any> requireNotNull(value: T?): T { contract { returnsNotNull() implies (value != null) // effect_1 } ... }
Наверняка, даже если вы уже встречали Kotlin‑контракты в стандартной библиотеке, то про такую штуку, как Effect (эффект), слышали редко. А между тем — именно эффекты лежат в самой основе всей магии Kotlin Contracts.
Давайте разберёмся, что это за зверь, и почему без него контракт не имеет смысла.

Effect'ы
Поведение функции в контракте описывается с помощью эффектов (Effect).
Эффект — понятие довольно широкое: по сути, это любое знание о состоянии программы, которое появляется после вызова функции. Например, если у функции
fесть эффектe, это значит, что при вызовеfвозникает эффектe.
Компилятор отслеживает все возникшие эффекты и использует их, чтобы строить более умный анализ.
Все эффекты расположены в файле Effect.kt. Мы можем представить их в виде дерева.

Давайте рассмотрим каждый эффект по‑отдельности.
Effect
Effect — это обычный интерфейс‑маркер, от которого наследуются все эффекты.
public interface Effect
SimpleEffect
SimpleEffect — эффект, который вступает в силу после того как функция завершит выполнение. Особенностью данного эффекта является то, что он позволяет указывать импликацию.
Импликация — это бинарная логическая связка, по своему применению приближенная к союзам «если…, то…». Посмотрим на общий пример импликации:
Effect -> Condition
Читать это выражение можно следующим образом: «Если функция излучила эффект
Effect, то условиеConditionгарантировано истинно».
Кстати, под капотом, в коде Kotlin компилятора, этот термин используется достаточно часто.
В исходниках стандартной библиотеки Kotlin эффект SimpleEffect выглядит следующим образом:
public interface SimpleEffect : Effect { /** Specifies that this effect, when observed, guarantees [booleanExpression] to be true. Note: [booleanExpression] can accept only a subset of boolean expressions, where a function parameter or receiver (this) undergoes - true of false checks, in case if the parameter or receiver is Boolean; - null-checks (== null, != null); - instance-checks (is, !is); - a combination of the above with the help of logic operators (&&, ||, !). */ public infix fun implies(booleanExpression: Boolean): ConditionalEffect }
Обратите внимание на комментарий! Тут подсветили 2 важных момента:
№1. В аргументы функции implies мы можем передавать только параметры функции, к которой относится контракт, и receiver (this).
№2. booleanExpression может принимать только подмножество таких операций, как:
проверки на
trueилиfalse(например:... implies (value == true)или... implies value);проверки на
null(например:... implies (value != null)или... implies (value != null));проверки на соответствие типу данных (например:
... implies (value is String)или... implies (value !is String));и комбинации выражений выше, используя логические операторы
&&,||,!
Кстати, обратите внимание, что при использовании this в контракте Extension функции необходимо использовать label, иначе Kotlin компилятор обратиться к ContractBuilder, который предоставляется функцией contract. Например, написать вот так не получится:
fun CharSequence?.isNullOrEmpty(): Boolean { contract { returns(false) implies (this != null) // Ошибка: Error in contract description: 'this' can only be a qualified reference to the extension receiver of contract owner.. } return this == null || this.length == 0 }
А так получится:
fun CharSequence?.isNullOrEmpty(): Boolean { contract { returns(false) implies (this@isNullOrEmpty != null) // Всё ок 👍🏻 } return this == null || this.length == 0 }
Теперь давайте поговорим о наследниках SimpleEffect: Returns и ReturnsNotNull.
Returns
Returns — возникает при обычном вызове и выполнении функции. Является наследником SimpleEffect.
public interface Returns : SimpleEffect
ReturnsNotNull
ReturnsNotNull — данный эффект произойдет только если функция возвращает не нулевое значение. Является наследником SimpleEffect.
public interface ReturnsNotNull : SimpleEffect
ConditionalEffect
ConditionalEffect — эффект, который производится функцией, если соответствующий ему SimpleEffect валиден. Этот эффект указывается путем присоединения логического выражения к другому эффекту SimpleEffect с помощью функции SimpleEffect.implies.
Пример использования ConditionalEffect:
fun CharSequence?.isNull(): Boolean { contract { returns(false) implies (this@isNull != null) } return this == null }
Если переписать наш контракт на абстрактный синтаксис, то контракт будет выглядеть следующим образом:
[Returns(false) -> this@isNull != null] // effect_1 fun CharSequence?.isNull(): Boolean { return this == null }
Читать этот контракт можно следующим образом: «Если функция вернёт
false, то выражение'this@isNullOrEmpty != null'гарантированно истинно».
Может показаться, что если поставить отрицание над обеими частями импликации, то выражение тоже будет валидно:
[Returns(true) -> this@isNull == null] // effect_1 fun CharSequence?.isNull(): Boolean { return this == null }
Но такой эффект не будет иметь никакой практической пользы. Давайте посмотрим на практике как будет работать функция с эффектом Returns(true).
[Returns(true) -> this@isNull == null] // effect_1 fun CharSequence?.isNull(): Boolean { return this == null } fun foo() { val myString: String? = "" if (!myString.isNull()) { // .isNull() вернет false myString.length // Ошибка: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'String?'. } }
Почему произошла ошибка компиляции? Дело в том, что список эффектов функции выглядит так: [Returs(true) -> ...]. Функция isNull() в данном случае вернёт false. Компилятор попробует найти в списке эффектов функции эффект Returns(false), но такого эффекта там не окажется, значит компилятор не получится никакой дополнительной информации о функции, и в блоке if компилятор будет считать, что myString может быть null.
Лайфхак: когда пишете контракт для функции — обращайте внимание на контекст её применения. Эффекты, которые вы опишите в контрактах должны быть актуальны для контекста использования функции
CallsInPlace
CallsInPlace — данный эффект помогает подсказать компилятору, что lambda, которая будет передаваться в функцию f будет вызвана в рамках функции f и не будет вызываться после завершения работы функции f. Дополнительно мы можем передать информацию компилятору о том, сколько раз будет вызвана lambda в рамках функции f.
В исходниках Kotlin библиотеки CallsInPlace эффект выглядит так:
public interface CallsInPlace : Effect
А когда эффект нам может пригодиться? Смотрим на пример ниже.
fun methodForInitialisation(call: (Int) -> Unit) { call(15) } fun main() { val number: Int methodForInitialisation { value -> number = value // Ошибка: Captured values cannot be initialized because of possible reassignments. } println(number) }
В данном примере мы хотим при вызове лямбды call инициализировать val number свойство. Однако компилятор не даст нам это сделать, потому что не знает, а точно ли лямбда вызывается 1 раз? Если лямбда будет вызвана несколько раз, то значение в val number перезапишется, однако val переменная не должна перезаписываться.
С помощью контракта с эффектом CallsInPlace мы можем решить эту проблему. В рамках данного эффекта мы говорим компилятору, что лямбда call будет вызвана в рамках methodForInitialisation ровно 1 раз.
@OptIn(ExperimentalContracts::class) fun methodForInitialisation(call: (Int) -> Unit) { contract { callsInPlace(call, InvocationKind.EXACTLY_ONCE) } call(15) } fun main() { val number: Int methodForInitialisation { value -> number = value // Всё ок 👍🏻 } println(number) // Здесь будет 15 }
Вуаля, и ошибки больше нет! Теперь компилятор знает, что val будет инициализировано ровной 1 раз.
Доверие компилятора
А что будет. если мы скажем компилятору, что лямбда вызовем 1 раз, но по факту будем вызывать дважды? Другими словами, что будет, если "соврём" компилятору? 😈
@OptIn(ExperimentalContracts::class) fun methodForInitialisation(call: (Int) -> Unit) { contract { callsInPlace(call, InvocationKind.EXACTLY_ONCE) // Компилятор ругается, но даст выполнить } call(30) call(15) } fun main() { val number: Int methodForInitialisation { value -> number = value } println(number) // Здесь будет 15 }
Как видите, компилятор не будет делать double-check контракта, и просто поверит вам наслово. Мы наврали компилятору при описании контракта, и поэтому наша программа повела себя неожиданным образом, перезаписав неизменяемое свойство.

Помните: контракты — это договорённость с компилятором. Нарушил договор — баги сам расхлёбывай!
Итоги
Contracts в Kotlin — недооценённая, но мощная фича 💪. Она позволяет договориться с компилятором о поведении функций и тем самым избавиться от лишних !!, кастов и паранойи по поводу null. Особенно полезна с валидаторами и DSL, где важно точно знать, что происходит. Главное — помнить: контракт нужно выполнять. Это не подсказка, а обещание. Нарушишь — словишь багов. Используешь с умом — получаешь читаемый, надёжный и уверенный в себе код.
Это была первая часть. Во второй части мы разберём новые фичи контрактов, которые вот-вот появятся на свет. Мы заглянем в их начинку, покопавшись в исходниках компилятора Kotlin — и даже опробуем одну из них в деле!
Дополнительные материалы
Подписывайтесь на Телеграм-канал Alfa Digital — там мы постим новости, опросы, видео с митапов, краткие выжимки из статей, иногда шутим.
Может быть интересно:
