Функции в Kotlin умеют больше, чем методы Java: аргументы по умолчанию, именованные аргументы, расширения, инфиксная запись. А пять scope-функций (let, run, with, apply, also) спрашивают почти на каждом собесе.
Параметры по умолчанию и именованные аргументы
fun connect(host: String, port: Int = 443, secure: Boolean = true) { }
connect("api.example.com")
connect("localhost", secure = false) // пропустили port
Перегрузки почти не нужны. Для вызова из Java с короткими списками аргументов добавляют @JvmOverloads, иначе Java видит один метод со всеми параметрами.
Функция из одного выражения пишется через =: fun square(x: Int) = x * x. vararg принимает переменное число аргументов, массив передают через spread: sum(*numbers).
Функции расширения
fun String.isEmail(): Boolean = contains('@') && contains('.')
"anna@mail.ru".isEmail()
Расширение выглядит как метод, но компилируется в статическую функцию, которой объект передаётся первым параметром. Отсюда главные свойства:
- Разрешаются статически, по объявленному типу переменной, а не по реальному объекту. Полиморфизма нет.
- Член класса всегда побеждает расширение с той же сигнатурой.
- Нет доступа к
privateчленам класса. - Можно объявить расширение для nullable-типа:
fun String?.orDash() = this ?: "-".
open class Shape
class Circle : Shape()
fun Shape.name() = "shape"
fun Circle.name() = "circle"
val s: Shape = Circle()
println(s.name()) // shape: выбрано по объявленному типу
Бывают и свойства-расширения: val String.lastChar get() = this[length - 1]. Поля у них нет, только геттер.
infix и operator
infix fun Int.pow(p: Int): Int = Math.pow(toDouble(), p.toDouble()).toInt()
2 pow 10
data class Money(val rub: Long) {
operator fun plus(o: Money) = Money(rub + o.rub)
}
Money(5) + Money(7)
Стандартные инфиксные функции: to ("a" to 1 создаёт Pair), until, step, and, shl.
Scope-функции
Все пять выполняют блок кода над объектом. Отличаются двумя вещами: как объект доступен внутри (it или this) и что возвращается (результат лямбды или сам объект).
| Функция | Объект внутри | Возвращает | Типичный случай |
|---|---|---|---|
let | it | результат лямбды | x?.let { } для не-null, преобразование |
run | this | результат лямбды | вычислить значение из полей объекта |
with(x) | this | результат лямбды | несколько вызовов над объектом |
apply | this | сам объект | настройка объекта (builder) |
also | it | сам объект | побочное действие: лог, проверка |
val intent = Intent(context, MainActivity::class.java).apply {
putExtra("id", 42)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
val user = repository.load(id).also { log("загружен ${it.name}") }
val length = name?.let { it.trim().length } ?: 0
Как запомнить: apply и also возвращают объект (цепочки и настройка), let и run возвращают результат. this у run, with, apply, it у let и also.
Есть ещё takeIf { условие }: вернёт объект, если условие истинно, иначе null. Удобно с Elvis: input.takeIf { it.isNotBlank() } ?: "по умолчанию".
Локальные функции
Функцию можно объявить внутри функции. Она видит переменные внешней и не засоряет API класса. Хвостовую рекурсию помечают tailrec, и компилятор превращает её в цикл.
Частые ошибки
- Ждать полиморфизма от расширений.
- Вкладывать
letвletвapply: код с тремя уровнямиitиthisнечитаем. Лучше именованный параметрlet { user -> }или обычныеif. - Использовать
apply, когда нужен результат вычисления, и получать объект вместо значения. x?.let { } ?: run { }как замена if-else: если лямбдаletвернётnull, выполнится и веткаrun.
Что спросят на собесе
- Как работают функции расширения под капотом и почему они не полиморфны?
- Чем отличаются
let,run,with,apply,also? - Что победит: член класса или расширение с той же сигнатурой?
- Зачем
@JvmOverloads? - Как сделать свой оператор
+для класса?