Что такое contracts в Kotlin и когда их используют?
Что проверяют
Знаете ли вы, откуда берутся умные приведения типов, и понимаете ли границу между тем, что компилятор доказал, и тем, что вы ему пообещали. На android-собеседовании вопрос не базовый, его задают, чтобы отличить тех, кто заглядывал в стандартную библиотеку, от тех, кто ей просто пользуется.
Как отвечать
Contract это способ сообщить компилятору о поведении функции, которое он сам вывести не может. Объявляется первым выражением в теле функции, доступен для функций верхнего уровня и требует опт-ина экспериментального API.
Практических видов два. Первый связывает результат с состоянием аргумента и включает smart cast на стороне вызова:
@OptIn(ExperimentalContracts::class)
fun String?.isPresent(): Boolean {
contract { returns(true) implies (this@isPresent != null) }
return !isNullOrBlank()
}Второй вид обещает, что лямбда будет вызвана на месте (callsInPlace), и тогда компилятор разрешает присвоить val внутри неё и считать переменную инициализированной. Именно так устроены run, let, apply и require из стандартной библиотеки: без контракта после require(x != null) пришлось бы разворачивать x вручную.
Итог: контракты нужны, когда вы пишете свою функцию-проверку или свою обёртку вроде let и хотите, чтобы вызывающий код читался так же удобно, как с библиотечными.
Где спотыкаются
Считают, что компилятор контракт проверит. Он не проверяет его вовсе, это обещание на слово, и неверный контракт оборачивается тихим падением с приведением типа у вызывающего.
Приписывают контрактам ускорение и защиту типов в духе аннотаций. На производительность они не влияют, байт-код функции не меняется, всё влияние сводится к анализу потока данных в месте вызова.
Пробуют навесить контракт на метод класса или ставят его не первой строкой и упираются в ошибку компиляции, не понимая причины. Ограничения жёсткие, и обойти их, спрятав вызов внутрь условия, нельзя.