Свистоперделка #3 | Велосипедим read-only Map

Абсолютно любой программист знаком с такой структурой данных, как Map (она же HashMap, она же Dictionary, она же NSDictionary, она же std::map, Hashtable). В основе - хеш-таблица. Важно знать и помнить, что все экземпляры Map в Dart изменяемые, кроме const {}, конечно же. А что делать, если мы не хотим допустить изменения текущих значений или добавления новых?

Всего у нас есть два путЯ. Map в Dart обладает специальным конструктором unmodifiable, который позволяет сделать из обычной мапы неизменяемую, но не константную. Сам конструктор является external factory, что как бы намекает нам на то, что это будет новая Map на основе старой, так ее неизменяемость будет реализована где-то в недрах DartVM. То, что мы получаем разные объекты подтверждается простой проверкой хешей старой и новой мапы - они не равны. Второй способ создания неизменяемой мапы - класс UnmodifiableMapView. Он также создает новый объект на основе старого, но под капотом реализован иначе - с помощью перегрузки операторов. Важное свойство обоеих реализаций - объект, полученный таким путем будет неизменяемым всегда.

Итак, чтобы хорошо представлять себе, чем же примечательна наша свистоперделка, надо ввести новую вводную в наше ТЗ. Неизменяемая мапа должна ссылаться на оригинальный объект.

В рамках нашей реализации мы используем относительно новые возможности Dart, а именно extension types. Напишем расширение типа для Map. И самое главное, что мы делаем - подсматриваем в код UnmodifiableMapView перегружаем оператор присваивания по индексу. `extension type ReadOnlyMap(Map map) implements Map { operator []=(K k, V v) => throw UnsupportedError("This map is read only, Stupid"); }

//Пример использования void main() { final original = {"a": "A", "b": "B"}; final map = ReadOnlyMap(original); //В выводе один и тот же хеш print(identityHashCode(original)); print(identityHashCode(map)); }

Подздравляю, мы получили неизменяемую Map, которая ссылается на оригинальный объект! Поскольку extension types это zero-cost абстракция, мы получаем бесплатную статическую обертку над типом данных Map. Но свистоперделка не была бы таковой, если бы у такой реализации не было бы интересных свойств. Итак, следите за руками.

Возьмем наш пример кода и немного его перепишем, добавив новую функцию. `void func1(Map map) { map["You"] = "Aboba"; }

void main() { final original = {"a": "A", "b": "B"}; final map = ReadOnlyMap(original); func1(map); print(map["You"]); }

Какой ожидаемый результат вызова func1? Конечно же ошибка в рантайме. Но мы успешно изменяем, казалось бы, неизменяемую мапу... Ведь я не просто так подсветил тот факт, что наша обертка является статической, правда? Оригинальный тип Map, который является типом аргумента функции, не включает в себя написанные нами перегрузки! Фиксится такое поведение просто - тип аргумента функии должен быть типом расширения. А также это открывает совершенно необычные способы применения для такого поведения. `void func1(ReadOnlyMap map) { map["You"] = "Aboba"; //error }

Представим себе ситуацию: наша программа выполняет некую бизнес-логику и в ходе своей работы использует некий объект Map и модифицирует его. Но в определенном месте программы мы должны выполнить функцию, которая была передана пользователем извне, где в свою очередь задействован наш объект. Мы помним, что все экземпляры Map, (кроме вариантов const, Map.unmodifiable и UnmodifiableMapView) изменяемы, но при этом мы очень хотим защитить наш объект от изменений извне. При последующих вызовах, которые будут работать с нашей мапой, она должна быть изменяемой. Посмотреть на реализацю можно тут.

P.S. А еще можно написать WriteOnlyMap, чисто по приколу😁 Как тебе такое, Илон Маск?!) `extension type WriteOnlyMap(Map map) implements Map { void operator [](K k) => throw UnsupportedError("Ha-ha, classic"); }````