val, var и настоящая неизменяемость. Extension-функции
Вопрос выглядит разминочным, но на нём отсеивают тех, кто выучил синтаксис, а не семантику.
val означает «ссылку нельзя переприсвоить», а не «объект нельзя изменить». Это разные вещи, и путаница между ними — источник настоящих багов, а не только неверных ответов.
Вопрос 1: «Чем val отличается от var? А от immutable?»
Что проверяет интервьюер
Первую половину вопроса знают все. Интересна вторая: понимает ли кандидат, что val ничего не говорит о содержимом объекта.
Развёрнутый ответ
var — переменная, которой можно присвоить новое значение. val — read-only ссылка: присвоить второй раз нельзя. Но объект по этой ссылке вполне может меняться:
val items = mutableListOf("a", "b")
items.add("c") // разрешено: меняем объект, а не ссылку
// items = mutableListOf() // ошибка компиляции: val нельзя переприсвоить
class Counter {
val history = mutableListOf<Int>() // val, но содержимое изменяемо
}
Второй нюанс: val — это не константа. Свойство с кастомным геттером объявляется через val и возвращает разные значения при каждом обращении:
class Cart(private val items: List<Item>) {
val total: Int
get() = items.sumOf { it.price } // val, но значение меняется вместе с items
}
Настоящая константа — это const val: значение подставляется компилятором в место использования. Ограничения: только на верхнем уровне, в object или в companion object, и только для примитивов и String.
Третий уровень того же вопроса — коллекции. В Kotlin List — это read-only интерфейс, а не immutable-коллекция. Он лишь не объявляет методов изменения. Под ним может лежать тот же ArrayList, который кто-то другой продолжает менять:
val source = mutableListOf(1, 2, 3)
val readOnly: List<Int> = source // тот же самый объект
source.add(4)
println(readOnly) // [1, 2, 3, 4] — «неизменяемый» список изменился
Отсюда практический вывод, который стоит озвучить: если наружу отдаётся список, который вы держите внутри, отдавайте копию (toList()) — иначе вызывающий код увидит ваши будущие изменения. Настоящие персистентные коллекции есть в kotlinx.collections.immutable (PersistentList), и в Compose они полезны ещё и как стабильные типы.
Типичные ошибки кандидатов
- «
val— это immutable». Нет:valпро ссылку, immutability — про объект. - «
Listнельзя изменить». Нельзя через сам интерфейс, но объект под ним может быть изменяемым. - Путают
const valиvalвcompanion object: второе — обычное свойство с геттером, к которому из Java обращаются черезCompanion.
Вопрос 2: «Как работают extension-функции? Можно ли ими переопределить метод класса?»
Что проверяет интервьюер
Знаете ли вы, что extension — это синтаксический сахар над статическим методом, а значит, никакого полиморфизма там нет.
Развёрнутый ответ
Extension-функция не добавляется в класс. Компилятор превращает её в обычную статическую функцию, первым параметром которой становится получатель. Из этого следуют три правила.
1. Extension разрешается статически — по объявленному типу, а не по фактическому:
open class Animal
class Dog : Animal()
fun Animal.name() = "Animal"
fun Dog.name() = "Dog"
fun main() {
val pet: Animal = Dog()
println(pet.name()) // Animal — выбор по типу переменной, а не по объекту
}
2. Член класса всегда побеждает extension. Если у класса уже есть метод toString(), ваш fun MyClass.toString() просто никогда не вызовется — компилятор даже предупредит. Переопределить поведение класса extension-функцией невозможно.
3. Extension не имеет доступа к private и protected членам (кроме случая, когда объявлен внутри того же класса или файла для приватных top-level элементов). Это не часть класса, а функция рядом с ним.
Зачем тогда extension нужны? Чтобы добавлять удобные операции к чужим типам, которые вы не можете править — String, Context, классам из библиотек. В Android это стандартный приём:
fun View.showIf(condition: Boolean) {
visibility = if (condition) View.VISIBLE else View.GONE
}
fun Context.dp(value: Int): Int =
(value * resources.displayMetrics.density).toInt()
// использование читается как метод самого класса
binding.error.showIf(state.hasError)
Ещё одна деталь на балл сверху: extension можно объявить и для nullable-типа. Тогда внутри this может быть null, и вызывать функцию можно без ?. — именно так устроен fun Any?.toString() в стандартной библиотеке.
Типичные ошибки кандидатов
- Утверждают, что extension «добавляет метод в класс» — из этого потом следует неверный ответ про полиморфизм.
- Не знают, что при конфликте выигрывает член класса.
- Пишут extension на свои же классы там, где хватило бы обычного метода: extension оправдан, когда класс чужой или когда операция не относится к его ответственности.
Как ответить кратко (20–30 секунд)
«
valзапрещает переприсваивание ссылки, но не запрещает менять объект:val list = mutableListOf()прекрасно принимаетadd. Настоящая константа —const val, она подставляется на этапе компиляции.Listв Kotlin — read-only интерфейс, а не immutable-коллекция: под ним может лежать изменяемый список, поэтому наружу отдаю копию. Extension-функции — это статические функции с получателем первым параметром: они разрешаются по объявленному типу, а не по фактическому, и никогда не перекрывают члены класса. Использую их, чтобы расширять чужие типы вродеViewиContext.»