Во что преобразует компилятор Dart модификатор `late`?

Ответ

Компилятор Dart не выполняет глубоких преобразований для late. Его основная задача — отложить проверку инициализации с момента компиляции на момент выполнения.

Что делает late:

  1. Отключает проверку на null при компиляции для не nullable-переменной.
  2. Добавляет runtime-гарантию: При первом чтении переменной среда выполнения проверяет, была ли она проинициализирована. Если нет — выбрасывается LateInitializationError.

Практическое применение в Flutter/Dart:

class DataService {
  // Полезно для зависимостей, которые инжектируются после создания объекта
  late final ApiClient _client; // Инициализируем позже в init()

  void init(ApiClient client) {
    _client = client; // Инициализация
  }

  Future<void> fetchData() async {
    // Первое использование _client. Если init() не вызван — ошибка.
    final data = await _client.get('/data');
  }
}

class MyWidgetState extends State<MyWidget> {
  // Частый кейс: инициализация в initState, использование в build
  late final PageController _pageController;
  late final ComplexData _cachedData;

  @override
  void initState() {
    super.initState();
    _pageController = PageController(); // Инициализация
    _cachedData = _heavyCalculation();
  }

  @override
  Widget build(BuildContext context) {
    // Гарантированно безопасное использование
    return PageView(
      controller: _pageController,
      children: _cachedData.buildWidgets(),
    );
  }
}

Важно: late — это не ленивая инициализация. Для ленивой инициализации при первом обращении используйте late в сочетании с присваиванием геттера:

class Settings {
  late final SharedPreferences _prefs = _initPrefs();
  Future<SharedPreferences> _initPrefs() async => await SharedPreferences.getInstance();
  // _prefs инициализируется только при первом чтении
}

Ответ 18+ 🔞

А, ну это же про late в Dart! Давайте разберём, что это за зверь такой, а то народ часто путает, будто это какая-то магия ленивых вычислений. Нет, блядь, всё проще и одновременно хитрее.

Представьте себе: вы объявляете переменную, говорите компилятору — «чувак, поверь мне на слово, я её потом заполню, она не будет null». А компилятор такой: «окей, я отключаю свою параноидальную проверку на null, но ты у меня потом ответишь, если накосячишь». И вот эта вся история — и есть late.

По факту, late делает всего две вещи:

  1. Выключает назойливую проверку на null во время компиляции для не-nullable переменной. То есть ты как бы даёшь честное пионерское.
  2. Но добавляет свою проверку в рантайме: когда ты в первый раз пытаешься прочитать эту переменную, система дёргает тебя за рукав и спрашивает: «а инициализировал?». Нет? Получай LateInitializationError прямо в лицо. Доверия, блядь, ноль, но справедливо.

Где это в Flutter'е пригождается? Да везде, ёпта!

class DataService {
  // Идеально для сервисов, которые получают зависимости не сразу
  late final ApiClient _client; // Создаём пустышку, но говорим, что она будет

  void init(ApiClient client) {
    _client = client; // А вот теперь реально инициализируем
  }

  Future<void> fetchData() async {
    // Первое обращение к _client. Если забыл вызвать init() — получишь овердохуища ошибок.
    final data = await _client.get('/data');
  }
}

class MyWidgetState extends State<MyWidget> {
  // Классика жанра: создаём в initState, юзаем в build
  late final PageController _pageController;
  late final ComplexData _cachedData;

  @override
  void initState() {
    super.initState();
    _pageController = PageController(); // Всё, проинициализировали
    _cachedData = _heavyCalculation(); // И это тоже
  }

  @override
  Widget build(BuildContext context) {
    // Теперь используем спокойно, без всяких «а вдруг null?»
    return PageView(
      controller: _pageController,
      children: _cachedData.buildWidgets(),
    );
  }
}

Важный момент, который все путают: late — это НЕ ленивая инициализация сама по себе. Это просто отложенная проверка. Если хочешь, чтобы переменная инициализировалась только при первом к ней обращении (то есть лениво), нужно совместить late с присваиванием прямо в объявлении, через геттер или функцию:

class Settings {
  // Вот это — ленивая инициализация. _prefs посчитается только когда впервые спросишь.
  late final SharedPreferences _prefs = _initPrefs();
  Future<SharedPreferences> _initPrefs() async => await SharedPreferences.getInstance();
  // Теперь _prefs — не просто late, а ещё и ленивая. Два в одном, удобно, блядь.
}

Вот и вся магия. Никакого волшебства, просто хитрая жопа у компилятора, которая сначала тебе верит, а потом строго спрашивает. Главное — не подведи.