ПрограммированиеMiddle Kotlin Developer

Что такое scope-функции в Kotlin (let, also, run, apply, with)? В чем различие этих функций, как их выбирать для разных задач, какие тонкости могут возникнуть при их использовании? Приведите примеры.

Проходите собеседования с ИИ помощником Hintsage

Ответ.

Scope-функции ("функции-области") в Kotlin — это стандартные функции (let, also, run, apply, with), которые позволяют управлять контекстом исполнения блока кода для объекта. Они отличаются:

  • типом возвращаемого значения,
  • тем, как объект доступен внутри блока: через it или через this.

Краткое сравнение:

Функцияthis/itВозвращаетДля чего?
letitрезультатцепочка операций, работа с nullable, map
alsoitобъектсайд-эффекты, логгирование, дебаг
runthisрезультатвычисления, инициализация с возвратом
applythisобъектконфигурация объекта, билдеры
withthisрезультатработа с внешними API, объект "снаружи"

Примеры:

  • let: удобно, если объект nullable:
val str: String? = "Text" str?.let { println(it.length) }
  • apply: настройка объекта:
val paint = Paint().apply { color = Color.RED strokeWidth = 2f }
  • run: выполнение на объекте, возвращаем результат:
val length = "abcde".run { length }
  • with: для работы с внешним объектом:
val sb = StringBuilder() with(sb) { append("Hello, ") append("world!") toString() }
  • also: для сайд-эффектов (например, логов):
val list = mutableListOf(1, 2, 3) list.also { println("Before: $it") }.add(4)

На что обратить внимание:

  • let создаёт копию объекта в it, менять свойства объекта не очень удобно.
  • apply и also всегда возвращают сам объект (this / it), полезно для билдеров.
  • run/with часто путают: with — это обычная функция, не extension.

Вопрос с подвохом.

В чём отличие между let и also?

Ответ:

  • Оба используют it внутри блока,
  • let возвращает результат лямбды, часто используется для цепочек трансформации,
  • also возвращает исходный объект, используется для побочных эффектов (l og, дебаг), чтобы не вмешиваться в цепочку преобразования.

Пример:

val result = listOf(1).also { println(it) }.map { it * 2 } // результат — List<Int>

Примеры реальных ошибок из-за незнания тонкостей темы:


История

Новичок использовал let для настройки объекта, считая, что так можно изменить его состояние "в цепочке". В результате по завершении блока конфигурации получал не объект, а результат лямбды (например, ничего), из-за чего нарушался цепочка построения DSL.


История

При написании кода по работе с nullable-объектами, использовали run вместо let, не заметив разницы в возвращаемом значении. В итоге результат выражения отличался от ожидаемого, появлялись null там, где не должно было быть — логика приложения ломалась.


История

В большом билдере случайно использовали with для внутренних объектов, рассчитывая на паттерн extension. Поскольку with — это не extension-функция, цепочка из нескольких with-блоков работала некорректно, внутренние вызовы путались и выходили за границы актуального объекта. Пришлось полностью переписывать иерархию создания объектов.