Переменные, типы и null-безопасность

В Kotlin переменные объявляются через val (неизменяемые) и var (изменяемые), типы чаще выводятся автоматически, а возможность хранить null встроена прямо в систему типов.
Суть: правильный выбор между val и var и аккуратная работа с nullable-типами — это фундамент надёжного Kotlin-кода, на котором держится всё остальное.

Любая программа крутится вокруг данных, а данные живут в переменных. От того, как вы их объявляете, зависит читаемость и безопасность кода. В Kotlin есть два ключевых слова. val создаёт неизменяемую ссылку: присвоить значение можно один раз. var создаёт изменяемую переменную, которой можно присваивать новые значения. Правило хорошего тона простое: всегда начинайте с val и переходите на var, только когда значение действительно должно меняться.

val pi = 3.14159        // тип Double выведен автоматически
var counter = 0         // тип Int, значение будет меняться
counter = counter + 1   // ок, var

val city: String = "Казань"  // тип указан явно
// city = "Москва"      // ОШИБКА: val переприсвоить нельзя

Базовые типы

Kotlin предлагает привычный набор: целые числа Int и Long, дробные Double и Float, логический Boolean, символ Char и строку String. Важная деталь: в Kotlin нет примитивов в синтаксисе — всё это объекты, у которых есть методы. При этом компилятор сам решает, где использовать эффективное примитивное представление под капотом, так что за производительность можно не волноваться.

Строки поддерживают удобную интерполяцию через $: вместо склейки плюсами вы вставляете значения прямо в текст.

val name = "Лена"
val age = 19
println("$name, $age лет")              // Лена, 19 лет
println("Через год будет ${age + 1}")   // выражение в фигурных скобках

Как работает под капотом

Когда вы пишете val pi = 3.14159, компилятор анализирует литерал справа и выводит тип Double. Этот механизм называется выводом типов (type inference). Он не делает язык динамическим: тип фиксируется на этапе компиляции и дальше не меняется. Просто вам не нужно писать его руками там, где он очевиден.

Nullable-типы реализованы тоже статически. Тип String и тип String? — это, с точки зрения компилятора, разные типы. Для String? компилятор отслеживает, проверяли ли вы значение на null, и не разрешает «небезопасный» доступ. Это называется smart cast: после проверки if (s != null) внутри блока переменная автоматически считается не-null.

  String   --> гарантированно не null
  String?  --> может быть null

  s?.length   безопасный вызов: null, если s == null
  s ?: "-"     элвис: подстановка, если s == null
  s!!.length  опасно: бросит исключение, если s == null

Операторы для null

Безопасный вызов ?. возвращает null вместо падения, если объект слева null. Оператор Элвиса ?: подставляет значение по умолчанию. Оператор !! утверждает, что значение точно не null, и бросает исключение, если ошиблись, — его стоит избегать.

fun greet(name: String?): String {
    // если name == null, используем "гость"
    val safe = name ?: "гость"
    return "Привет, $safe"
}
println(greet("Иван"))  // Привет, Иван
println(greet(null))     // Привет, гость

Частые ошибки

Везде ставить var. Привычка из других языков делает код менее предсказуемым: любая переменная может неожиданно измениться. Иммутабельность через val упрощает чтение и отладку.

Злоупотреблять !!. Каждый !! — это потенциальный NullPointerException. Почти всегда лучше ?. с ?: или нормальная проверка.

Путать пустую строку и null. Строка "" — это не null; для проверок используйте isNullOrEmpty() или isNullOrBlank().

Best practices

  • По умолчанию val; var — только при реальной изменяемости.
  • Делайте тип nullable только тогда, когда отсутствие значения осмысленно.
  • Предпочитайте ?: для значений по умолчанию вместо длинных if-else.
  • Для пустых строк используйте isNullOrBlank(), а не сравнение с null вручную.

Логику оператора Элвиса легко прочувствовать на простом редьюсере значений по умолчанию. Запустите врезку.

# Аналог оператора Элвиса ?: из Kotlin
def elvis(value, default):
    return value if value is not None and value != '' else default

inputs = ['Иван', None, '', 'Мария']
for x in inputs:
    print(repr(x), '->', elvis(x, 'гость'))

Попробуй сам ▶ — поменяйте значение по умолчанию и входные данные. Так же ведёт себя name ?: "гость" в Kotlin.

Итог: val и var задают изменяемость, вывод типов избавляет от лишнего шума, а nullable-типы вместе с операторами ?., ?: и smart cast делают работу с отсутствующими значениями безопасной. Эти базовые кирпичики вы будете использовать в каждом файле проекта.

Проверьте себя
1. В чём разница между val и var?
Aval для чисел, var для строк
Bval создаёт неизменяемую ссылку (одно присваивание), var допускает переприсваивание
Cval работает быстрее, потому что не проверяет типы
DМежду ними нет разницы, это синонимы
2. Что вернёт выражение name ?: "гость", если name равно null?
AБросит NullPointerException
BВернёт null
CВернёт строку "гость"
DВернёт пустую строку