Какие возможности появились с введением null safety в Dart 2.12?

«Какие возможности появились с введением null safety в Dart 2.12?» — вопрос из категории Dart Core, который задают на 29% собеседований Flutter Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Null safety, стабильно представленная в Dart 2.12, — это одно из самых значимых обновлений языка. Она переносит обнаружение ошибок, связанных с null, со времени выполнения (runtime) на время компиляции. Вот как я её использую:

  1. Non-nullable по умолчанию

    • Все типы по умолчанию не могут содержать null. Это кардинально меняет ментальную модель и устраняет целый класс ошибок.
      String name = 'Flutter'; // OK
      String name = null; // Ошибка компиляции: значение не может быть null.
  2. Nullable типы (с суффиксом ?)

    • Чтобы переменная могла быть null, это нужно указать явно.
      String? nickname = null; // OK, может быть null
      nickname = 'Bob'; // OK
      print(nickname.length); // Ошибка компиляции: переменная может быть null.
  3. Операторы для безопасной работы с nullable-переменными

    • Оператор безопасного доступа (?.)
      int? length = nickname?.length; // length будет типа int? (null, если nickname null)
    • Оператор объединения с null (??)
      String greeting = 'Hello, ' + (nickname ?? 'Guest'); // Использует 'Guest', если nickname null
    • Оператор утверждения, что не-null (!)
      // Используется, когда вы логически уверены, что значение не null.
      // Если оно всё же null, выбросится runtime-исключение.
      print(nickname!.length);
  4. Ключевое слово required для named-параметров

    • Заменяет старый @required аннотацию и делает проверку обязательности на уровне языка.
      void createUser({required String name, required int age}) { ... }
      // createUser(age: 25); // Ошибка компиляции: параметр 'name' обязателен.
  5. Поздняя инициализация (late)

    • Позволяет объявить non-null переменную, инициализация которой будет гарантированно произведена до первого использования, но не сразу.

      class ApiService {
      late final Dio _client; // Не-null, но инициализируется позже
      
      ApiService(String baseUrl) {
      _client = Dio(BaseOptions(baseUrl: baseUrl)); // Инициализация в конструкторе
      }
      }
      // Использование _client до инициализации вызовет LateInitializationError.

Эта система заставляет явно обрабатывать случаи с null, что делает код значительно более предсказуемым и надежным, и это чувствуется в каждом Flutter-проекте.