var и val в Kotlin: в чем разница? Полный разбор
Если вы только начинаете свой путь в Kotlin или пришли из других языков (Java, Python, JavaScript), то один из первых вопросов, который вы зададите — зачем нужны два ключевых слова для объявления переменных? Действительно, зачем усложнять, если можно обойтись одним? Ответ кроется в философии языка: Kotlin создавался, чтобы сделать код безопаснее, понятнее и предсказуемее. И разница между var и val — это не прихоть, а мощный инструмент управления состоянием.
Давайте разберемся на примерах, когда использовать var, а когда val, и почему это важно для вашего кода.
Что такое var и val? Базовые определения
Начнем с самого простого.
var(от англ. variable) — изменяемая переменная. Вы можете переприсвоить ей новое значение сколько угодно раз.val(от англ. value) — неизменяемая ссылка (read-only). После инициализации вы не можете изменить её значение. Это аналогfinalв Java илиconstв JavaScript (с оговорками).
Вот самый простой пример:
fun main() {
var x = 10
x = 20 // OK: var можно менять
val y = 30
// y = 40 // Ошибка компиляции: val нельзя переприсвоить
println("x = $x, y = $y")
}
Всё кажется очевидным, но дьявол, как всегда, кроется в деталях.
Главное отличие: изменяемость ссылки vs изменяемость объекта
Здесь часто возникает путаница. val гарантирует, что ссылка на объект не изменится, но не гарантирует неизменность самого объекта.
Разберем на примере списка:
fun main() {
val numbers = mutableListOf(1, 2, 3)
// numbers = mutableListOf(4, 5, 6) // Ошибка: val нельзя переприсвоить
numbers.add(4) // А вот так — можно! Мы меняем содержимое объекта
numbers[0] = 100 // И так тоже можно
println(numbers) // [100, 2, 3, 4]
}
Обратите внимание: мы объявили numbers как val, но спокойно добавляем и изменяем элементы. Почему? Потому что val защищает только саму ссылку (переменную numbers), а не объект, на который она указывает.
Если вы хотите полную неизменность, нужно использовать неизменяемые коллекции (listOf, mapOf, setOf) и val одновременно.
fun main() {
val immutableNumbers = listOf(1, 2, 3)
// immutableNumbers.add(4) // Ошибка: listOf возвращает неизменяемый список
// immutableNumbers[0] = 100 // Ошибка
}
Это важный момент, который часто спрашивают на собеседованиях. Запомните: val ≠ immutable object.
Когда использовать var, а когда val?
Золотое правило Kotlin: используйте val везде, где это возможно, и var только тогда, когда это действительно необходимо. Почему?
- Безопасность. Чем меньше изменяемых переменных, тем меньше шансов случайно изменить значение в неожиданном месте. Особенно это важно в многопоточном программировании.
- Читаемость. Когда вы видите
val, вы сразу знаете, что значение не поменяется. Это упрощает понимание кода. - Функциональный стиль. Kotlin поощряет функциональное программирование, где неизменяемость — один из ключевых принципов.
Вот пример, где var оправдан:
fun main() {
var counter = 0
while (counter < 10) {
println("Counter: $counter")
counter++ // Счётчик должен меняться
}
}
А вот пример, где var — это плохая практика, и его можно заменить на val:
// Плохо: var не нужен
fun processOrder(order: Order) {
var result = ""
if (order.isValid) {
result = "Valid"
} else {
result = "Invalid"
}
println(result)
}
// Хорошо: используем val с when
fun processOrderBetter(order: Order) {
val result = if (order.isValid) "Valid" else "Invalid"
println(result)
}
Видите разницу? Во втором случае код стал короче, понятнее и безопаснее.
Ленивая инициализация с val
Еще одна интересная возможность — val можно использовать с ленивой инициализацией через делегат lazy. Это позволяет вычислить значение только при первом обращении к нему.
fun main() {
val heavyValue: String by lazy {
println("Вычисляем тяжелое значение...")
"Результат сложных вычислений"
}
println("До обращения к heavyValue")
println(heavyValue) // Здесь значение будет вычислено
println(heavyValue) // А здесь — просто взято из кэша
}
Вывод:
До обращения к heavyValue
Вычисляем тяжелое значение...
Результат сложных вычислений
Результат сложных вычислений
С var такой фокус не пройдет. Это еще один аргумент в пользу val.
var и val в классах: свойства и геттеры/сеттеры
Внутри классов var и val ведут себя так же, но с одним нюансом: для val генерируется только getter, для var — и getter, и setter.
class Person {
var name: String = "Unknown" // Есть getter и setter
val birthYear: Int = 1990 // Только getter
}
fun main() {
val person = Person()
person.name = "Alice" // OK
// person.birthYear = 2000 // Ошибка: setter недоступен
}
Вы можете переопределить getter и setter для var, а для val — только getter.
class Rectangle(val width: Int, val height: Int) {
val area: Int
get() = width * height // Кастомный getter для val
}
fun main() {
val rect = Rectangle(5, 10)
println(rect.area) // 50
}
Обратите внимание: area — это val, но его значение вычисляется каждый раз при обращении. Это нормально, ведь мы не меняем ссылку, а только вычисляем новое значение на основе других val.