Урок по треку Flutter и Dart 7 мин

Dart: типы, null-safety, records и коллекции

var, final, const, late, null-safety, records, patterns и коллекции.

Dart строго типизирован, но типы часто выводятся сами. Всё в Dart объект, включая числа, null и функции. С Dart 3 null-safety обязательна: старый код без неё больше не собирается.

var, final, const, late

var count = 0;            // тип выведен: int, переприсвоить можно
final now = DateTime.now(); // присвоить один раз, значение в рантайме
const pi = 3.14;          // константа времени компиляции
late final String token;  // инициализируем позже, проверка в рантайме

Разница final и const любимый вопрос. final про переменную: её нельзя переприсвоить, но объект внутри может меняться (final list = [1]; list.add(2); работает). const про значение: оно вычислено при компиляции и глубоко неизменяемо, а одинаковые const-объекты канонизируются, то есть это один экземпляр в памяти. Поэтому identical(const [1], const [1]) даёт true, а для [1] без const будет false.

late откладывает инициализацию. Если прочитать late-переменную до присвоения, будет LateInitializationError. У late с инициализатором (late final x = heavy();) значение вычисляется лениво при первом обращении.

Null-safety

Тип String не может хранить null, String? может. Компилятор не даст вызвать метод у String? без проверки.

String? name;
print(name?.length);      // null-aware доступ: null
print(name ?? 'гость');   // значение по умолчанию
name ??= 'гость';         // присвоить, если null
print(name!.length);      // «я уверен, что не null»: при null будет исключение

Компилятор сужает тип после проверки (if (name != null)), но только для локальных переменных и приватных final-полей (с Dart 3.2). Для обычного поля класса сужения нет: геттер мог бы вернуть другое значение при втором чтении. Поэтому поле копируют в локальную переменную.

! это обещание, а не проверка. Частая ошибка на ревью: ! везде, где компилятор ругается. Правильнее понять, откуда может прийти null.

Числа и строки

int и double наследуются от num. На нативных платформах int 64-битный, в вебе это число JavaScript. Деление / всегда даёт double, целочисленное деление ~/. Строки неизменяемые, интерполяция '$name, ${a + b}'.

Records и patterns (Dart 3)

Record это анонимный неизменяемый кортеж со структурным равенством:

(int, String) pair = (1, 'a');
({int x, int y}) point = (x: 1, y: 2);
print(pair.$1);      // 1
print(point.x);      // 1

Records удобны, чтобы вернуть из функции несколько значений без отдельного класса. Patterns их разбирают:

var (a, b) = pair;
switch (value) {
  case int n when n > 0: print('положительное');
  case [var first, ...]: print('список, первый $first');
  case {'id': String id}: print('мапа с id');
  default: print('другое');
}

switch в Dart 3 бывает выражением (final s = switch (x) { 0 => 'ноль', _ => 'другое' };) и проверяет полноту для sealed-иерархий и enum.

Коллекции

List, Set, Map и ленивый Iterable. Литералы поддерживают if, for и spread прямо внутри:

final items = [
  'шапка',
  if (isAdmin) 'админка',
  for (final p in pages) p.title,
  ...?extra,
];

map, where, expand возвращают ленивый Iterable: функция вызовется только при обходе, и при каждом новом обходе снова. Нужен список: добавьте .toList().

Сравнение == у списков и мап по ссылке, а не по содержимому. Для глубокого сравнения есть listEquals во Flutter или пакет collection.

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

  • Путать final и const, считать final-список неизменяемым.
  • Ставить ! вместо обработки null.
  • Ждать, что map выполнится сразу, и вешать на него побочные эффекты.
  • Сравнивать списки через ==.

Что спросят на собесе

  • Чем final отличается от const, что такое канонизация const.
  • Как устроена null-safety: ?, !, ??, late, почему не сужается тип поля.
  • Что такое records и patterns, где они полезны.
  • Чем Iterable отличается от List, когда выполняется map.
  • int и double: что даёт / и ~/.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.