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

Этой серией статей я хочу простым человеческим языком показать, зачем нужны Contracts, как их использовать на практике и как они работают внутри.
Какую проблему решают 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 — там мы постим новости, опросы, видео с митапов, краткие выжимки из статей, иногда шутим.
Может быть интересно: