Переменные, типы и null-safety

В Dart значение «ничего» (null) нельзя получить случайно — компилятор стоит на страже.

Суть: переменные объявляют через var, final или const. У каждого значения есть тип, а главная защита языка — null-safety: переменная не может быть null, пока вы явно не разрешите это знаком ?.

Чтобы прочувствовать ценность null-safety, вспомните, сколько приложений вылетало у вас на телефоне с внезапным закрытием. Огромная доля таких падений в других экосистемах — это именно обращение к значению, которого не оказалось. Dart переносит эту проблему с этапа работы приложения на этап написания кода: вы видите потенциальную пустоту прямо в редакторе, подчёркнутую красным, и чините её, пока пользователь ещё ничего не заметил. Это не формальность, а реальное снижение числа сбоев в продакшене.

Переменная — это именованная коробка для значения. В Dart есть три способа создать такую коробку, и разница между ними решает половину будущих багов. var — обычная переменная, её значение можно поменять. final — значение присваивается один раз и больше не меняется (но вычисляется во время работы программы). const — значение известно ещё на этапе компиляции и зашито намертво. На практике опытные разработчики по умолчанию пишут final и переходят на var только когда переменную действительно нужно менять.

var age = 20;          // можно менять: age = 21;
final name = 'Аня';    // присвоили один раз
const pi = 3.14159;    // константа на этапе компиляции

age = 21;              // ОК
// name = 'Боря';      // ОШИБКА: final нельзя переприсвоить

Базовые типы Dart: int (целые), double (дробные), String (текст), bool (истина/ложь). Тип можно писать явно (int age = 20;), но чаще его выводит сам компилятор из значения справа — это называется выведение типов.

Как работает null-safety под капотом

В большинстве языков любая переменная может внезапно оказаться null («пусто»), и обращение к ней роняет программу с печально известной ошибкой. Dart 3 решает это sound null-safety: по умолчанию String name никогда не может быть null. Если вы хотите допустить пустоту — добавьте ? к типу: String? name. Тогда компилятор заставит вас проверять значение перед использованием.

  String  name   -->  гарантированно есть значение
  String? name   -->  может быть значением ИЛИ null
                       |
       обращение      v
   name!.length  -->  "я уверен, что не null" (рискованно)
   name?.length  -->  безопасно: вернёт null, если name == null
   name ?? 'нет' -->  подставить запасное значение

Три оператора решают всё. ?. — безопасный доступ: если слева null, вся цепочка вернёт null вместо краха. ?? — значение по умолчанию: «возьми левое, а если оно null — правое». ! — оператор «я точно знаю, что тут не null»; он снимает защиту, и если вы ошиблись, программа упадёт. Поэтому ! используют редко и осознанно.

Ниже — запускаемая модель той же логики на Python: подстановка значения по умолчанию, когда данных нет. Это ровно то, что делает оператор ?? в Dart.

# Аналог Dart-оператора ?? — запасное значение, если данных нет
def display_name(name):
    # name ?? 'Гость'  на Dart
    return name if name is not None else 'Гость'

print(display_name('Аня'))   # Аня
print(display_name(None))    # Гость

# Аналог ?. — безопасный доступ к длине
def safe_length(text):
    return None if text is None else len(text)

print(safe_length('Flutter'))  # 7
print(safe_length(None))       # None

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

  • Лепить ! везде, чтобы заткнуть ошибки компилятора. Это снимает защиту и возвращает старую боль с падениями. Лучше проверьте значение.
  • Путать final и const. const требует значения, известного на этапе компиляции; final допускает вычисление во время работы.
  • Объявлять String? x без необходимости. Если значение всегда есть, делайте тип ненулевым — меньше проверок в коде.

Best practices

  • По умолчанию пишите final; var — только для реально меняющихся переменных.
  • Избегайте !. Заменяйте его на ?., ?? или явную проверку if (x != null).
  • Давайте переменным понятные имена в стиле lowerCamelCase: userName, а не un.

Полезно держать в голове и третий оператор — ??=, который присваивает значение только если переменная сейчас равна null. Он пригодится для ленивой инициализации: cache ??= computeExpensive() посчитает дорогое значение лишь однажды. Вместе с ?., ?? и продуманным выбором между ненулевым и нулевым типом эти инструменты дают полный контроль над пустотой, и привычка ими пользоваться отличает аккуратный Dart-код от случайного.

Итог: используйте final по умолчанию, доверяйте выведению типов и относитесь к null-safety как к другу: знак ? в типе — это явное «здесь может быть пусто», а операторы ?. и ?? позволяют работать с пустотой безопасно.

Проверьте себя
1. Что произойдёт при попытке переприсвоить переменную, объявленную через final?
AЗначение тихо поменяется
BОшибка компиляции — final нельзя переприсвоить
CПрограмма упадёт в рантайме
DЗначение станет null
2. Что делает оператор ?? в выражении name ?? 'Гость'?
AВсегда возвращает 'Гость'
BПадает, если name == null
CВозвращает name, а если он null — то 'Гость'
DПроверяет тип переменной