О чем статья
В языке Dart, наиболее известном благодаря фреймворку Flutter, весьма запутанная система объявления переменных: для этого можно использовать такие ключевые слова, как var, final и const. Не все разработчики готовы разбираться в этом и пишут всегда var, так как этот модификатор можно использовать всегда. В то же время на var и final накладываются определенные ограничения, но и применение данных ключевых слов позволяет оптимизировать приложение.
Кроме того, при объявлении классов поля могут быть помечены как final, а сам конструктор — как канонический. Однако не для каждого класса получится так сделать, вдобавок использовать объекты таких классов нужно с осторожностью.
В данной статье даны рекомендации по использованию ключевых слов var, final и const, а также совет, как не попасть в ловушку этой оптимизации.
const-переменные
Данный модификатор означает константу времени компиляции, то есть значение переменной должно быть известно до запуска программы. Очевидно, что const требует неизменности после инициализации. За счет того, что значение переменной известно заранее, Dart оптимизирует ее объявление. Поэтому в случае, когда вы не планируете менять значение переменной и если присваемое ей значение дано на этапе компиляции, следует помечать её как const. Это также положительно влияет на скорость hot reload [1].
Преимущество данного модификатора в том, что если объявить несколько одинаковых const-переменных, это будет один объект в памяти. К сожалению, без указания const, автоматически это не произойдет. Например, данный код породит один массив в памяти:
const myList = [1, 2, 3];
<...> тысяча строк кода <...>
const otherList = [1, 2, 3];
А этот уже два:
final myList = [1, 2, 3];
<...> тысяча строк кода <...>
final otherList = [1, 2, 3];
final-переменные
Модификатор final говорит компилятору, что значение переменной не будет меняться после инициализации. Отличие от const в том, что начальное значение не обязано быть константой времени компиляции. Например, мы используем final в данном случае, так как now не вычисляется заранее:
final now = DateTime.now();
Таким образом, если вы объявляете переменную, которую не планируете менять, но ее нельзя вычислить при компиляции, помечайте её final. Если значение определимо на этапе компиляции, используем const:
const april = DateTime.april;
var-переменные
Модификатор var стоит использовать, только если вы планируете менять значение переменной [2]. В остальных случаях он должен быть заменен на const/final.
Любопытно, если вы объявите переменную как final, вместо const, последние версии анализатора выдадут вам предупреждение, но если объявить как var, никаких сообщений не будет. Старые версии Dart вообще никак не реагируют на эти ситуации. Отсюда следует алгоритм определения модификатора в спорных ситуациях: сначала пишите const, если компилятор ругается, пишите final. И наконец, когда потребуется изменить значение, поправите на var.
Канонические конструкторы
В конструкторе класса поля следует помечать как final, если они не будут меняться. Если все поля являются final, то конструктор рекомендовано объявлять как const, тогда можно создавать канонические экземпляры данного класса. Преимущество таких экземпляров в том, что если объявить несколько раз объект с одними и теми же параметрами, в памяти будет заведен только один (аналогично константным переменным). У многих классов во Flutter есть const-конструкторы, например, у EdgeInsets, используемого для определения отступов, или у TextStyle, применяемого для задания стиля текста [3].
Оптимизация с помощью канонических экземпляров особенно полезна, когда на экране отображается несколько однотипных объектов, например карточек. Тогда отступы, стили и другие объекты, помеченные const, не будут дублироваться в памяти для каждой карточки. Это крайне эффективно, к примеру, при пролистывании списков.
К счастью, последние версии анализатора подсказывают, когда нужно использовать const. Однако в проектах на старой версии Flutter такой возможности нет. Отсюда общая рекомендация писать const при использовании любых встроенных виджетов, если его параметры постоянны. Некоторые стандартные компоненты не имеют канонического конструктора, в таком случае компилятор подскажет вам убрать модификатор.
Помимо оптимизации памяти преимущество использования const-конструктора еще и в том, что при обновлении виджета, например, с помощью setState или Consumer [4], виджеты, помеченные данным модификатором, не будут перестраиваться. Это позволяет ускорить перерисовку экрана.
Опасность оптимизации
Рассмотрим пример:
В данном случае если в модели M изменятся данные, то виджет А начнет перестраиваться и обновит также виджет B, который отобразит новую информацию. Однако этого не произойдет, если мы создадим и используем канонический конструктор для класса B, так как const в дереве виджетов — прямое указание, что объект не должен перестраиваться. Отсюда вывод, что при написании собственных канонических конструкторов следует убедиться, что виджет не должен перестраиваться.
- Виджет A обернут в Consumer, чтобы слушать изменения ViewModel M.
- Виджет А содержит виджет B.
- Виджет B получает из модели M данные и отображает их на экране.
- Виджет B не обернут в Consumer.
- Виджет B имеет только final-поля.
Заключение
Таким образом, имеем такие рекомендации относительно рассмотренных ключевых слов:
- Неизменяемые переменные маркируем const или final (если нельзя вычислить при компиляции) в целях оптимизации. Используем var только когда уже потребовалось изменить значение.
- Всегда используем const для стандартных виджетов, где это возможно.
- Используем канонические конструкторы для своих классов, но только если виджет не должен перестраиваться.
Источники
[1] Рекомендации анализатора Linter for Dart —
https://dart-lang.github.io/linter/lints/prefer_const_declarations.html
[2] Официальная документация Dart. Модификаторы final и const —
https://dart.dev/guides/language/language-tour#final-and-const
[3] Хабр. Статья "Dart. Всё, что надо знать про константы" —
https://habr.com/en/post/501804/
[4] Официальная документация по классу Consumer —
https://pub.dev/documentation/provider/latest/provider/Consumer-class.html