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

Коллекции и функциональный стиль

Коллекции Dart и выросший на них стиль преобразований — map, where, fold и всё остальное. Хорошая цепочка в одну читаемую строку сама говорит, что она делает. Небрежная — проходит по одному и тому же списку пять раз и создаёт четыре копии, которые тут же выбрасываются.

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

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

  • Что на самом деле говорит преобразование, если прочитать его вслух
  • Какие операции ленивы, а какие создают новую коллекцию
  • Сколько раз обходятся данные
  • Подходит ли тип коллекции тому, как к ней обращаются
  • Когда обычный цикл понятнее цепочки

Основные темы

Типы

  • List, Set, Map и выбор по тому, как данные читаются
  • Iterable как рецепт, а не как контейнер
  • Растущие списки против списков фиксированной длины, неизменяемые представления
  • Равенство: по умолчанию по идентичности — и что это значит для Set и ключей Map

Преобразования

  • map, where, expand, fold, reduce, any, every
  • Ленивость: почему цепочка не делает ничего, пока результат никто не запросит
  • toList() — где он необходим, а где это просто привычка
  • Сортировка, компараторы и устойчивый порядок
  • Группировка и индексация: почему Map выигрывает у повторяющегося firstWhere

Стиль

  • Цепочки, которые читаются как фразы, и момент, когда пора перестать их наращивать
  • Каскады и паттерн «строитель»
  • Неизменяемость по умолчанию, изменение — как осознанное решение
  • const-коллекции и что они экономят

Цена

  • Повторяющийся contains на List против Set
  • Вложенные циклы, замаскированные под изящные цепочки
  • Выделение памяти в коде, который выполняется на каждом кадре

Уровни

УровеньКак это выглядит
JuniorСвободно пользуется map, where и forEach. Ставит toList() рефлекторно.
MiddleВыбирает тип коллекции по тому, как к ней обращаются, понимает ленивость и не обходит одни и те же данные по нескольку раз.
SeniorПишет цепочки, которые одновременно очевидны и дёшевы, и точно знает, когда отказаться от этого стиля в пользу цикла.

Практика

Для начала

  • Замените цикл цепочкой Перепишите императивное накопление через where и fold — так, чтобы читалось не хуже.

  • Почините поиск Найдите firstWhere внутри цикла и замените его на Map, построенный один раз.

  • Возьмите множество Найдите List.contains, которым проверяют принадлежность, и смените тип.

Глубже

  • Посчитайте проходы Возьмите цепочку из пяти операций и выясните, сколько раз обходятся данные. Затем сократите это число.

  • Заставьте равенство работать Положите объект-значение в Set и добейтесь, чтобы дедупликация вела себя так, как вы задумали.

  • Поймите, где остановиться Найдите цепочку, которая стала нечитаемой, и перепишите её обычным циклом с внятными именами.

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

  • У каких ваших коллекций тип не соответствует тому, как их читают?
  • Когда toList() действительно нужен, а когда это шум?
  • Сколько раз самая длинная ваша цепочка обходит данные?
  • Что произойдёт, если положить в Set два одинаковых на вид объекта?
  • Где цепочка перестаёт быть понятнее цикла?
  • Что в коде, работающем на каждом кадре, выделяет память без необходимости?

Ресурсы

  • Iterable collections codelab — официальный интерактивный разбор, в том числе ленивости, то есть ровно той части, которую большинство разработчиков понимает неправильно.
  • dart:collection — очереди, связные списки, splay-деревья, неизменяемые представления. Удивительно много кода, написанного руками, здесь уже есть.
  • Effective Dart: Usage — особенно раздел про коллекции: какую идиому предпочесть и почему.
  • package:collection — группировка, помощники для равенства, firstWhereOrNull и прочее, что каждый пишет заново и каждый раз слегка неверно.
  • Dart 3 records and patterns — деструктуризация меняет то, как читается код преобразований, особенно когда дело доходит до пар ключ–значение и кортежей результатов.