Можно ли объявить локальную final переменную в Dart без немедленного присваивания значения?

Ответ

Да, в Dart можно объявить локальную final переменную без немедленной инициализации, но с важным условием: значение должно быть присвоено ровно один раз до первого использования. Компилятор проводит статический анализ потока управления (flow analysis), чтобы это гарантировать.

Корректные примеры:

// 1. Присваивание в условии
void processUser(bool isPremium) {
  final String accessLevel;

  if (isPremium) {
    accessLevel = 'premium';
  } else {
    accessLevel = 'basic';
  }

  print('Access: $accessLevel'); // OK
}

// 2. Присваивание в try-catch
void loadConfig() {
  final String config;

  try {
    config = _readConfigFile();
  } catch (e) {
    config = 'default';
  }

  print('Config: $config'); // OK
}

// 3. Позднее присваивание в цикле (сложный случай)
void findFirstEven(List<int> numbers) {
  final int firstEven;
  bool found = false;

  for (final number in numbers) {
    if (number % 2 == 0) {
      firstEven = number;
      found = true;
      break;
    }
  }

  if (!found) {
    firstEven = -1; // Обязательно присвоить в ветке else
  }

  print('First even: $firstEven'); // OK
}

Ошибки, которые вызовет компилятор:

// ОШИБКА: использование до присваивания
void errorExample1() {
  final int x;
  print(x); // Error: Non-nullable variable 'x' must be assigned
  x = 10;
}

// ОШИБКА: не все пути выполнения присваивают значение
void errorExample2(bool condition) {
  final String message;

  if (condition) {
    message = 'true';
  }
  // Нет else-ветки - ошибка если condition == false

  print(message); // Error
}

// ОШИБКА: повторное присваивание
void errorExample3() {
  final int value = 10;
  value = 20; // Error: 'value' can only be set once
}

Отличия от полей класса:

class User {
  // Для полей класса final требует немедленной инициализации:
  final String name = 'John'; // OK
  final int id; // Error если не инициализировано

  // Или инициализация в конструкторе:
  User(this.id); // OK

  // Для late final можно отложить инициализацию:
  late final String token;
  void setToken(String t) {
    token = t; // Можно присвоить один раз
  }
}

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

Widget buildUserCard(User user) {
  // Позднее связывание final переменных полезно для вычислений
  final Color cardColor;
  final IconData icon;

  if (user.isAdmin) {
    cardColor = Colors.red[100]!;
    icon = Icons.admin_panel_settings;
  } else if (user.isPremium) {
    cardColor = Colors.blue[100]!;
    icon = Icons.star;
  } else {
    cardColor = Colors.grey[100]!;
    icon = Icons.person;
  }

  return Card(
    color: cardColor,
    child: ListTile(
      leading: Icon(icon),
      title: Text(user.name),
    ),
  );
}

Это мощная особенность Dart, которая повышает безопасность кода, гарантируя, что final переменные будут инициализированы перед использованием, но давая гибкость в выборе момента инициализации.

Ответ 18+ 🔞

А, ну вот, смотри, тут про Dart разговор. Так, значит, можно объявить final переменную локально и не сразу её инициализировать, да? Ну типа, ёпта, можно, но с одной важной приблудой: значение ей надо присвоить ровно один раз, и сделать это до того, как ты её первый раз попробуешь использовать. Компилятор, хитрая жопа, следит за этим через анализ потока управления. Он там глазами-буравчиками всё выверяет.

Вот как это правильно делается, смотри:

// 1. Присваивание в условии — классика жанра
void processUser(bool isPremium) {
  final String accessLevel; // Объявил, но не дал значения

  if (isPremium) {
    accessLevel = 'premium'; // Вот тут дал
  } else {
    accessLevel = 'basic'; // Или тут, но обязательно где-то
  }

  print('Access: $accessLevel'); // И только теперь юзаешь — всё окей
}

// 2. В try-catch — тоже ок, жизнь непредсказуема
void loadConfig() {
  final String config;

  try {
    config = _readConfigFile(); // Пробуем прочитать
  } catch (e) {
    config = 'default'; // Не получилось — даём дефолт
  }

  print('Config: $config'); // Всё чётко, доверия ебать ноль, но работает
}

// 3. В цикле — уже посложнее, надо головой думать
void findFirstEven(List<int> numbers) {
  final int firstEven;
  bool found = false;

  for (final number in numbers) {
    if (number % 2 == 0) {
      firstEven = number; // Нашли чётное — присвоили
      found = true;
      break; // И свалили
    }
  }

  if (!found) {
    firstEven = -1; // А если не нашли — обязательно присваиваем тут! Иначе писец.
  }

  print('First even: $firstEven'); // Теперь можно
}

А вот так делать НЕ НАДО, получишь по шапке от компилятора:

// ОШИБКА: начал юзать, а значение ещё не дал — сам от себя охуел?
void errorExample1() {
  final int x;
  print(x); // Error: Non-nullable variable 'x' must be assigned. Какого хуя ты от меня хочешь, я пустая!
  x = 10; // Поздно, брат, поздно.
}

// ОШИБКА: не на всех путях дал значение — распиздяйство
void errorExample2(bool condition) {
  final String message;

  if (condition) {
    message = 'true';
  }
  // А если condition == false? Тогда message так и останется неинициализированной, а ты её ниже печатать собрался. Пиздец логике.

  print(message); // Error
}

// ОШИБКА: попытка переприсвоить final — ты чё, совсем, final же!
void errorExample3() {
  final int value = 10;
  value = 20; // Error: 'value' can only be set once. Один раз и навсегда, запомни.
}

И ещё важный момент — с полями класса это не работает так же:

class User {
  // Для полей класса final требует сразу значения:
  final String name = 'John'; // Ок
  final int id; // Error если не инициализировано — тут анализа потока нет

  // Или через конструктор, это классика:
  User(this.id); // Ок

  // А вот late final — это другая песня, можно один раз присвоить потом:
  late final String token;
  void setToken(String t) {
    token = t; // Можно, но только один раз, второй вызов — будет тебе хиросима.
  }
}

Где это на практике пригождается, например, во Flutter:

Widget buildUserCard(User user) {
  // Удобно, когда логика определения цвета и иконки размазана по условиям
  final Color cardColor;
  final IconData icon;

  if (user.isAdmin) {
    cardColor = Colors.red[100]!;
    icon = Icons.admin_panel_settings;
  } else if (user.isPremium) {
    cardColor = Colors.blue[100]!;
    icon = Icons.star;
  } else {
    cardColor = Colors.grey[100]!; // Обязательно все ветки закрыть!
    icon = Icons.person;
  }

  return Card(
    color: cardColor,
    child: ListTile(
      leading: Icon(icon),
      title: Text(user.name),
    ),
  );
}

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