Null Safety в Dart

Ошибка на миллиард долларов

Представьте себе большую коробку с чипсами. Ты с радостью открываешь её, чтобы взять чипсинку, но …

Но вместо неё находишь пустоту … Это очень разочаровывает, правда?

Пустая коробка с чипсами Разочарование от пустой коробки

В программировании есть похожая ситуация, только вместо упаковки с чипсами есть переменная.
А вместо чипсинки – какое-то значение.

Иногда эта переменная может быть пустой, то есть не содержать никакого значения. Это как пустая упаковка.

NullPointerException – это та исключительная ситуация, когда программа пытается использовать пустую переменную. Такая переменная содержит значение null

Это как попытаться съесть чипсинку, которой нет. Программа не знает, что делать в такой ситуации, и поэтому выдает ошибку – NullPointerException

Почему NullPointerException это очень плохо

Программа не знает длину null

Программа не знает, какая длина у ничего

История создания null

Тони Хоар

Создатель концепции null, Тони Хоар, назвал своё изобретение "ошибкой на миллиард долларов" из-за неисчислимых убытков, которые она принесла за десятилетия в виде сбоев программ, уязвимостей и потраченного времени разработчиков.

В 1965 году Тони Хоар (Sir Tony Hoare), работая над языком программирования ALGOL W, ввёл концепцию null reference — специального значения, обозначающего "отсутствие объекта" или "ничего" для ссылочных типов.

Позже, выступая на конференции в 2009 году, Хоар публично извинился за это решение и назвал его "ошибкой на миллиард долларов" ("billion-dollar mistake")

Прямая цитата Тони Хоара:

"Я называю это своей ошибкой на миллиард долларов. Это было изобретение null-ссылки в 1965 году. В то время я разрабатывал первую комплексную систему типов для ссылок в объектно-ориентированном языке (ALGOL W). Моя цель была — сделать все использования ссылок абсолютно безопасными, с автоматической проверкой компилятором. Но я не смог устоять перед искушением добавить null-ссылку, просто потому что это было так легко реализовать. Это привело к бесчисленным ошибкам, уязвимостям и сбоям систем, которые, вероятно, причинили боли и убытков на миллиард долларов за последние сорок лет."

Как справлялись раньше?

Программистам приходилось вручную писать бесчисленные проверки.
Это работало, но было громоздко, и главное — можно было легко забыть про проверку.

Dart - Старый подход

Светлая тема Темная тема
if (name != null) {
  print(name.length);
} else {
  print(0);
}

Философия Null Safety в Dart

Начиная с версии 2.12, Dart ввёл строгую Null Safety (нулевую безопасность).

Основная идея гениальна и проста:

По умолчанию, ни одна переменная не может быть null.

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

Компилятор защищает от null

Dart - Null Safety в действии

Светлая тема Темная тема
int a = 42;
a = null; // ОШИБКА КОМПИЛЯЦИИ! 'a' не может быть null.

String text; // ОШИБКА КОМПИЛЯЦИИ! Переменная должна быть инициализирована.

Преимущества этого подхода

  1. Надёжность: Огромный класс ошибок просто исчезает. Ваш код становится предсказуемым.
  2. Производительность: Компилятор уверен, что переменные не null, и может проводить оптимизации, делая ваш код быстрее и компактнее.

Nullable и Non-Nullable типы

В Dart есть два вида типов:

  1. Non-nullable (не могут null): Все типы по умолчанию. int, String, List<double>
  2. Nullable (могут null): Те же типы, но с добавлением знака ? в конце. int?, String?, List<double>?

Например

Dart - Nullable и Non-Nullable типы

Светлая тема Темная тема
int nonNullableInt = 10;
// nonNullableInt = null; // Ошибка

int? nullableInt = 10;
nullableInt = null; // Всё в порядке

Даже var подчиняется этому правилу. Если вы не инициализируете var сразу, она считается dynamic, которая может быть null.

Dart - var и dynamic

Светлая тема Темная тема
var someVar; // Тип dynamic, значение - null
print(someVar); // Выведет: null

Работа с Nullable-переменными

1. Прямая проверка на null

Это самый понятный способ.
Компилятор Dart умён и понимает, что внутри блока if (variable != null) переменная точно не null.

Проверка на null

Dart - Проверка на null

Светлая тема Темная тема
int getLength(String? str) {
  if (str != null) {
    // Внутри этого блока str считается non-nullable String
    return str.length;
  }
  return 0;
}

print(getLength("Hello")); // 5
print(getLength(null));    // 0

2. Оператор ??

Очень удобен для предоставления значения по умолчанию.
Читается как "если слева null, то взять то, что справа".

Оператор ??

Dart - Оператор ??

Светлая тема Темная тема
int? nullableInt;
int value = nullableInt ?? 0; // Если nullableInt равен null, value станет 0.
print(value); // 0

Есть также оператор ??=, который присваивает значение, только если переменная null.

Dart - Оператор ??=

Светлая тема Темная тема
int? value2 = null;
value2 ??= 42; // value2 был null, поэтому ему присвоили 42
print(value2); // 42

value2 ??= 99; // value2 уже не null, ничего не произойдет
print(value2); // 42

3. Оператор ?.

Позволяет вызвать метод или получить свойство, только если объект не null. Если объект null, всё выражение вернёт null и программа не упадет.

Dart - Оператор ?.

Светлая тема Темная тема
String? text; // null

// Вместо того чтобы упасть, это выражение просто вернёт null
print(text?.length); // Выведет: null

text = "Dart";
print(text?.length); // Выведет: 4

Операторы ! и late

4. Оператор !

Этот оператор — способ сказать компилятору:
"Я на 100% уверен, что в этом месте переменная не null. Поверь мне и позволь мне её использовать".

Dart - Оператор !

Светлая тема Темная тема
List<int?> listWithNulls = [2, null, 4];
int firstItem = listWithNulls.first!; // Мы уверены, что первый элемент не null
print(firstItem); // 2

Используйте с огромной осторожностью! Если вы ошиблись и переменная окажется null, ваша программа упадёт с ошибкой. Оператор ! — это, по сути, отключение защиты для одной конкретной операции.

Осторожно с оператором !

5. Модификатор late

late говорит компилятору: "Обещаю, я инициализирую эту non-nullable переменную позже, но точно до того, как попытаюсь её использовать".

У late есть два основных применения:

а) Отложенная инициализация: Полезно для полей класса, которые не могут быть инициализированы сразу.

Dart - Отложенная инициализация

Светлая тема Темная тема
class MyClass {
  late String description; // Обещаем инициализировать позже

  MyClass(String initialText) {
    // Выполняем какую-то сложную логику и только потом инициализируем
    description = 'This is a description: $initialText';
  }
}

б) Ленивая инициализация: Переменная будет инициализирована не в момент объявления, а только при первом обращении к ней. Это полезно, если инициализация — ресурсозатратная операция.

Dart - Ленивая инициализация

Светлая тема Темная тема
int _calculateHeavyValue() {
  print('Выполняется очень сложный расчет...');
  return 42;
}

// как будто вызывает функцию _calculateHeavyValue()
// но нет ... пока не вызовим heavyValue функция не сработает
late int heavyValue = _calculateHeavyValue();

void main() {
  print('Программа запущена.');
  // Функция _calculateHeavyValue() еще не вызвана
  print('Сейчас мы обратимся к переменной...');
  print(heavyValue); // Вот здесь будет вызван _calculateHeavyValue()
}

Внимание! Если вы пометили переменную как late, но забыли её инициализировать и попытались прочитать, вы получите ошибку времени выполнения (LateInitializationError).

Null Safety и коллекции (List, Map)

Null Safety позволяет очень гибко настраивать коллекции. Вы можете контролировать, может ли сама коллекция быть null и могут ли её элементы быть null.

Списки List

Dart - Null Safety в списках

Светлая тема Темная тема
// ни список, ни его элементы не могут быть null
List<String> = ['Dart', 'Swift'];

// список не может быть null, но его элементы могут
List<String?> = ['Dart', null, 'Swift'];

// список может быть null
List<String>? = null;

// и сам список, и его элементы могут быть null
List<String?>? = ['Dart', null, 'Swift'];

Особый случай Словари Map

При обращении к Map по ключу, которого там нет, он возвращает null. Поэтому результат такого обращения всегда nullable.

Dart - Null Safety в Map

Светлая тема Темная тема
var myMap = <String, int>{'one': 1};
print(myMap['two']); // Выведет: null

// int value = myMap['one']; // ОШИБКА! Вдруг ключа 'one' не будет?
int? value1 = myMap['one']; // Правильно. value1 имеет тип int?
int value2 = myMap['one']!;   // Уверенность, что ключ есть.
int value3 = myMap['two'] ?? 0; // Безопасно с значением по умолчанию.