Как работает null safety в Dart? В чём разница между ?, ! и ???
? делает тип nullable и включает null-aware операторы (?., ??), ! утверждает ненулевое значение в рантайме (UnsafeAssert), ?? возвращает правый операнд если левый null.
Null Safety в Dart
Null safety (sound null safety) — система типов, при которой переменная не может быть null по умолчанию. Чтобы разрешить null, тип помечается символом ?. Компилятор отслеживает nullability статически и не допускает обращения к nullable-переменной без явной проверки.
Оператор ? — nullable type и null-aware доступ
String? означает «строка или null». Для nullable-переменных также доступен null-aware оператор доступа ?.:
String? name = null;
print(name?.length); // null, а не NPE
name = 'Alice';
print(name?.length); // 5
// Каскадный null-aware
List<int>? nums;
nums?..add(1)..add(2); // не выполнится, если nums == null
Оператор ! — null assertion
! говорит компилятору: «я уверен, что значение не null». Если в рантайме значение всё же null, бросается Null check operator used on a null value.
String? maybeNull = getValue();
// Используем ! только если уверены:
String definitelyNotNull = maybeNull!;
print(definitelyNotNull.length);
// Частый кейс с GlobalKey во Flutter
final key = GlobalKey<FormState>();
bool isValid() => key.currentState!.validate();
Оператор ?? — if-null (null coalescing)
a ?? b возвращает a, если оно не null, иначе — b.
String? input = null;
String result = input ?? 'default';
print(result); // default
int? count;
count = count ?? 0;
// Эквивалентная запись:
count ??= 0;
print(count); // 0
Late переменные
late сообщает компилятору, что переменная будет инициализирована до первого обращения — пригодится для зависимостей, инициализируемых в initState:
class MyService {
late final DatabaseClient _db;
void init(DatabaseClient db) {
_db = db; // единственное присваивание
}
Future<List> query() => _db.select('SELECT 1');
}
Flow analysis — умные проверки
Компилятор Dart понимает if-проверки и автоматически сужает тип:
String? value = fetchValue();
if (value != null) {
// здесь value имеет тип String, не String?
print(value.toUpperCase());
}
// Также работает с assert:
assert(value != null);
print(value.length); // OK в debug-режиме
Подводные камни
- Злоупотребление
!. Каждый!— потенциальный crash. Предпочитайте??,?.или ранний возврат с проверкой. lateбез инициализации бросает LateInitializationError при первом обращении, если забыть присвоить значение.- Nullable generics.
List<String?>(список допускает null-элементы) иList<String>?(сам список может быть null) — совершенно разные типы. ??vs||.value ?? 'default'срабатывает только наnull;value.isEmpty ? 'default' : valueнужен для пустых строк — не путайте.- Interop с legacy-кодом. При вызове platform channel или FFI результат может быть
dynamic— нужен явный каст с проверкой, иначе null safety не поможет. ??=и side effects.a ??= expensiveCall()вызоветexpensiveCall()только еслиa == null, но это не всегда очевидно при чтении кода.- Sound vs unsound. Миксование null-safe и legacy-пакетов переводит программу в unsound-режим, где компилятор не может дать полных гарантий — это временная мера при миграции.
Common mistakes
- Сводить «работает null safety в Dart? В чём разница между
?,!и??» к синтаксису и не объяснять null safety. - Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии dart-4.
- Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.
What the interviewer is testing
- Формулирует точную модель для «работает null safety в Dart? В чём разница между
?,!и??» и подтверждает ее корректным примером. - Умеет связать ответ с sound type system, тестированием и отладкой на устройстве.
- Называет ограничения подхода dart-4, включая производительность, память и сопровождение.