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: что даёт/и~/.