Перейти к основному содержимому

Система типов и null safety

Система типов Dart и то, что меняется, когда относиться к ней как к инструменту проектирования, а не как к формальности. Строгая (sound) null safety означает, что компилятор способен доказать: значение не равно null — но только если ему это позволить, а большинство кодовых баз за первый же месяц разменивают эту гарантию на !.

Почему это важно. Любое состояние, которое разрешают ваши типы, но запрещает продукт, — это баг, ожидающий подходящего пользователя. В модели, где одновременно могут быть истинны «доставлено» и «дата доставки не задана», рано или поздно так и произойдёт.

Что важно понимать

  • Что обещает тип и правда ли это обещание
  • Почему значение nullable — оно действительно необязательное, ещё не загружено или создаётся лениво
  • Где ! — это доказательство, которое есть у вас и нет у компилятора, а где — просто надежда
  • Можно ли вообще выразить недопустимую комбинацию полей
  • Во что обходится dynamic и где он появляется, не спросив

Ключевые темы

Система типов

  • Строгая типизация и что даёт её строгость
  • Обобщённые типы (generics) и код, который обобщён потому, что иначе нельзя
  • Вариантность в одном предложении: почему в Dart List<Dog> — это List<Animal> и какой проверкой во время выполнения за это приходится платить
  • dynamic против Object? — второй заставляет посмотреть, что внутри

Null safety

  • Nullable- и non-nullable-типы, и late как обещание, за которое платят во время выполнения
  • Null-aware операторы и то, какие из них прячут проблемы
  • ! — четыре честных применения и множество нечестных
  • Миграция кодовой базы и почему уродливое промежуточное состояние временно

Моделирование типами

  • Sealed-классы и исчерпывающий switch
  • Записи (records) для значений, которые ходят вместе, но не заслуживают отдельного класса
  • Сопоставление с образцом и деструктуризация
  • Перечисления с полями и методами вместо разбросанных цепочек if
  • Недопустимые состояния, которые невозможно выразить, — вместо проверок на каждом шагу

Уровни

УровеньКак это выглядит
JuniorПишет корректные аннотации типов. Устраняет ошибки с null, иногда через !.
MiddleОсознанно применяет обобщённые типы и sealed-классы. Может объяснить, почему поле nullable, — или убирает эту nullability.
SeniorПроектирует модели, в которых неверные состояния не компилируются. Использует исчерпываемость так, чтобы новый вариант ломал ровно тот код, который нужно изменить.

Практика

Для начала

  • Уберите восклицательные знаки Возьмите файл, где встречается несколько !, и перестройте код так, чтобы большинство из них стали не нужны.

  • Обоснуйте каждый nullable Пройдите по модели и для каждого nullable-поля напишите, почему оно такое. Те, на которые ответа нет, исправьте.

  • Замените строку перечислением Найдите статус, который хранится как String, сделайте из него enum и дальше идите за компилятором.

Глубже

  • Смоделируйте конечный автомат Замените класс с полями isLoading, error и data на sealed-класс, в котором существуют только допустимые сочетания.

  • Используйте исчерпываемость намеренно Добавьте новый вариант в sealed-тип и дайте компилятору показать все места, которые обязаны его обработать.

  • Напишите настоящий обобщённый код Вынесите повторяющийся паттерн в обобщённую функцию или класс с осмысленным ограничением типа.

Проверьте себя

  • Какие nullable-поля в вашем проекте такие только потому, что загружаются позже?
  • Когда вы пишете !, что вы знаете такого, чего не знает компилятор?
  • Какие сочетания полей разрешает ваша модель, но запрещает продукт?
  • Где dynamic попал в код без вашего решения?
  • Как найти все места, которые придётся поменять при добавлении нового варианта?
  • Когда обобщение делает код переиспользуемым, а когда — нечитаемым?

Материалы

  • The Dart type system — как устроена строгость типизации, включая разделы про обобщённые типы и вывод типов, которые почти никто не читает, а потом неверно ставит диагноз.
  • Sound null safety — официальное объяснение: анализ потока управления и то, во что на самом деле обходится late во время выполнения.
  • Patterns — деструктуризация, switch-выражения и исчерпываемость. Именно эта возможность делает моделирование на sealed-классах практичным, а не многословным.
  • Effective Dart: Design — рекомендации самой команды языка по типам в публичных API. Сжато, и к каждому пункту приложено обоснование.
  • Making illegal states unrepresentable — написано про F#, но это самая ясная формулировка идеи, и она напрямую переносится на sealed-классы в Dart.