var и val в Kotlin: в чем разница? Полный разбор

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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.

Топ-3 ошибки новичков с var и val

Блоги

Рекомендаций по книгам