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

Ответ

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-проекте.

Ответ 18+ 🔞

Слушай, а вот эта ваша null safety в Dart — это вообще, блядь, огонь, ядрёна вошь! Раньше-то как было? Ловишь NullPointerException в рантайме, как дурак, а потом полдня дебажишь, кто тебе null в переменную сунул. А теперь, ёпта, компилятор сам тебя за руку ловит и орёт: "Мужик, ты куда лезешь, там же null может быть, ёб твою мать!".

Вот смотри, как теперь жизнь изменилась:

  1. Теперь всё по-честному, не-nullable по умолчанию. Раньше каждая переменная была как хитрая жопа — в любой момент могла оказаться пустой. А теперь если ты написал String, то это строка, а не манда с ушами. Попробуй запихнуть туда null — получишь по шапке сразу, на этапе компиляции.

    String name = 'Flutter'; // Всё чики-пуки
    String name = null; // Компилятор: "Какого хуя? Сам от себя охуел? Я тебе разрешал?"
  2. Хочешь по-старому, жить опасно? Пожалуйста, но явно пометь (?). Сказал, что переменная String? — ну окей, значит, можешь быть героем и хранить там null. Но теперь компилятор тебе не даст просто так взять и обратиться к её свойствам. Защита от дурака, блядь.

    String? nickname = null; // Ну ладно, храни свою пустоту.
    print(nickname.length); // А вот тут компьютор тебя останавливает: "Чувак, а если там null? Ты уверен? Доверия ебать ноль".
  3. Инструменты для работы с этой nullable-хренью.

    • Безопасный доступ (?.) — типа, "если не null, то сделай, а если null, то да похуй, верни null и иди дальше". Удобно, не надо городить if.
      int? length = nickname?.length; // length либо число, либо тоже null. Чётко.
    • Костыль на случай пиздеца (??) — "если слева хуйня (null), то бери то, что справа". Спасение для дефолтных значений.
      String greeting = 'Hello, ' + (nickname ?? 'Guest'); // Если nickname пустой, гость будет. Красота!
    • Взрывная кнопка (!) — "Я, блядь, на 146% уверен, что тут не null! Если я ошибаюсь — пусть всё ебётся конём!". Рискованно, но иногда надо.
      print(nickname!.length); // Программист клянётся, что nickname не null. Если соврал — runtime исключение, всем пи*дец.
  4. required — это теперь не просто аннотация, а закон. Раньше @required можно было проигнорировать. Теперь если в параметрах функции стоит required, то ты обязан передать значение. Не передашь — компилятор тебя не пропустит, будет орать, как резаный. Порядок наводит.

    void createUser({required String name, required int age}) { ... }
    // createUser(age: 25); // Ошибка компиляции: "Мужик, а где name? required же, ёбаный насос!"
  5. Ленивая инициализация (late) — для умных. Бывает, переменную non-nullable объявить надо сразу, а инициализировать — позже, в конструкторе или в initState. Раньше приходилось пихать туда null и делать вид, что так и надо. Теперь говоришь late — и всё, типа "я ручаюсь, что к моменту использования она будет готова". Обманешь — получишь LateInitializationError и будешь сам виноват.

    class ApiService {
      late final Dio _client; // Не-null, но повиси пока в ожидании.
    
      ApiService(String baseUrl) {
        _client = Dio(BaseOptions(baseUrl: baseUrl)); // А вот теперь инициализирую, всё честно.
      }
    }
    // Если попробовать использовать _client до этой строчки — будет тебе хиросима, сам напросился.

Короче, null safety — это пизда рулю. Сначала, конечно, волнение ебать, привыкать надо, кажется, что везде надо ? ставить. Но потом, когда проект растёт, а ошибок "Cannot read property of null" — ноль ебать, понимаешь, что это гениально. Код становится предсказуемым, как удар вилкой в глаз.