Урок по треку Kotlin 7 мин

Функции, расширения и scope-функции за 7 минут

Параметры по умолчанию, extension-функции, infix, let, run, with, apply, also.

Функции в 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) и что возвращается (результат лямбды или сам объект).

ФункцияОбъект внутриВозвращаетТипичный случай
letitрезультат лямбдыx?.let { } для не-null, преобразование
runthisрезультат лямбдывычислить значение из полей объекта
with(x)thisрезультат лямбдынесколько вызовов над объектом
applythisсам объектнастройка объекта (builder)
alsoitсам объектпобочное действие: лог, проверка
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?
  • Как сделать свой оператор + для класса?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.