Const — важное и полезное ключевое слово в Dart, но оно не такое простое, как кажется на первый взгляд. Весь опыт работы с ним собрал Николай Омётов, руководитель Flutter-разработки компании Mad Brains.

Const — это хорошо: для объектов — это возможность быть вычисленными во время компиляции, всегда быть в одном экземпляре и никогда не мутировать/изменяться, так как они immutable. Отсюда и правило Linter'a prefer_const_constructors.

Когда можно забыть поставить const

Чистый класс

Вот вам пример
class SomeModel {
  // все поля final, значит точно можно поставить const
  SomeModel(this.someField); 

  final String someField;
}

// Примеры, где не получится сделать const класс
class SomeModel {
  const SomeModel(); 

  // Поля в классах const не должны иметь инициализаторов.
  final String someField = '';

  // нужен final 
  String _someField2;

  // Следовательно, никаких мутаций в const классе быть не может
  set someField2(String value) => _someField2 = value;
}

Абстрактный класс

Часто вижу на ревью, что не проставлен константный конструктор у абстрактных классов или интерфейсов. А, следовательно, и «дети» не могут стать константными, так как не прописано у их «родителей». 

Пример
abstract class SomeEvent{
  const SomeEvent();
}

final class DoSomethingEvent extends SomeEvent{
  const DoSomethingEvent();
}

Enum

Тут не получится ошибиться. У enum все конструкторы должны быть константными.

Сами посмотрите
enum WorkTaskStatusEnumDao {
  done('Done'),
  inProgress('In progress'),
  notStarted('Not started');

  const WorkTaskStatusEnumDao(this.tableName);

  final String tableName;
}

const factory

Пример
const factory Key(String value) = ValueKey<String>;

В отличие от обычного factory, здесь выполняется redirect на другой конструктор с идентичным набором параметров.

Скрытые константы

Скрытые константные классы

Раньше на ревью я просил не использовать EdgeInsets.zero. Моя ошибка заключалась в том, что я считал неконстантными классы, к которым нельзя применить ключевое слово const. Однако, если у объекта есть как константная, так и неконстантная версии, предпочтение следует отдавать первой.

Но оказалось, что внутри реализации класса они могут быть константными и добавлять const будет избыточно. Так получилось с известным классом отступа EdgeInsets.

static const EdgeInsets zero = EdgeInsets.only();

Когда я посмотрел на реализацию, понял, что был не прав. Теперь всегда проверяю, как реализован объект.

Скрытые константные объекты

Как пример, можно использовать в коде такие удобные виджеты, как empty, sliverEmpty и nil. Они идут без приписки const, но являются таковыми внутри.

const nil = Nil();
const Widget empty = SizedBox.shrink();
const Widget sliverEmpty = SliverToBoxAdapter();

Проблемы с const

Как и у всего, есть свои минусы при работе с константами.

Скрытый константный объект

Иммутабельность — это и дар, и проклятие. Если где-то в коде создаётся константный объект, изменять его состояние нельзя. Это может привести к неочевидным багам в runtime. Очень показателен частый случай с объектом List. Мы можем попытаться добавить в него что-то, удалить, заменить, но ничего не получится.

Доступно только чтение
void main() {
  // запустите код сначала с const, 
  // а потом уберите его
  final List<int> nums = const [1, 2, 3]; 
  
  changeList(nums);
}

void changeList(List<int> nums){
  nums[1] = 0; // Unsupported operation: indexed set
  nums.sort(); // Unsupported operation: sort
  nums.add(4); // Unsupported operation: add
  nums.remove(2); // Unsupported operation: remove
  print(nums);
}

Поэтому важно помнить об ограничениях во взаимодействии при создании объекта. Теперь «куда попало» отдать мы его не сможем.

Hаследование от неконстантного класса

Нередко сталкивался ситуацией, когда класс выглядит как явный кандидат, чтобы быть const, но нет. Его «родитель» (extends) имеет неконстантный конструктор, поэтому и «ребёнок» не может стать константным.

Возможно, вы когда-то пробовали расширить класс Exception (именно с ним такая ситуация). Он не имеет константного конструктора, что распространяется и на его «детей».

Что ещё может помешать константности всего поколения? (Чувствую себя генным инженером ?) Если вы добавите в конструктор выполнение команд.

abstract class Interactor extends Injectable with DisposableDump {
  Interactor() {
    log('Init Interactor');
    init();
  }}

Если класс константный, то всегда один экземпляр?

Один экземпляр объекта — да, один экземпляр класса — чаще всего, нет. Всё зависит от параметров класса: const SizedBox(width: 4)и const SizedBox(width: 2) будут разными объектами. Поэтому чем больше у вас классов с разными параметрами, тем больше объектов. Страшного в этом ничего нет, но и иллюзии питать по поводу одного объекта на все варианты класса не надо.

Пример
class Test{
  const Test(this.count);
  final int count;
}

void main() {
  const test1 = Test(1);
    print(test1.hashCode);
  const test2 = Test(2);
    print(test2.hashCode);
  const test3 = Test(1);
    print(test3.hashCode);
  // hashCode у test1 и test3 совпадут
}

Проблемы Hot reload'a

Процесс дебага на Dart приятный. И это происходит за счёт обновления кода в виртуальной машине dart. Когда изменения вносятся в объекты константных классов, то может произойти неприятная ситуация, когда hot reload не сможет выполниться. Вас попросят выполнить hot restart: Hot reload was rejected: Const class cannot become non-const. Try performing a hot restart instead.

Это неудивительно, ведь виртуальная машина изначально вычислила состояние объектов с константами при запуске, а теперь изменения для них стали возможны. Существует и другая ситуация: например, добавилось новое поле, но в текущем объекте его ещё нет, и оно остаётся неинициализированным. Чтобы избежать хаоса, машине приходится сбросить состояние и проинициализировать всё заново.

В заключение

Ключевое слово const очень многогранно в языке Dart. Надеюсь, теперь вы сможете более уверенно использовать его в своих проектах.

Комментарии (0)