Здесь есть несколько хороших примеров, но я хотел бы перейти к некоторым личным, где неизменность помогла очень много. В моем случае я начал с разработки неизменяемой параллельной структуры данных, в основном с надеждой на то, что мы сможем уверенно выполнять код параллельно с перекрывающимися операциями чтения и записи и не беспокоиться о состоянии гонки. Был разговор, который Джон Кармак дал мне, вдохновил меня сделать это, где он говорил о такой идее. Это довольно простая структура и довольно тривиальная реализация:
Конечно, с еще несколькими наворотами, такими как возможность удалять элементы в постоянное время и оставлять исправимые дыры позади, а блоки смещаются, если они становятся пустыми и потенциально освобождаются для данного неизменяемого экземпляра. Но в основном, чтобы изменить структуру, вы изменяете «временную» версию и атомарно фиксируете внесенные в нее изменения, чтобы получить новую неизменяемую копию, которая не касается старой, а новая версия создает только новые копии блоков, которые должны быть сделаны уникальными при мелком копировании и подсчете ссылок на другие.
Тем не менее, я не нашел это, чтополезно для многопоточности. В конце концов, все еще существует концептуальная проблема, когда, скажем, физическая система применяет физику одновременно, пока игрок пытается перемещать элементы в мире. С какой неизменной копией преобразованных данных вы идете, той, которую преобразовал игрок, или той, которую преобразовала физическая система? Так что я действительно не нашел хорошего и простого решения этой основной концептуальной проблемы, кроме наличия изменяемых структур данных, которые просто блокируют более разумным способом и не допускают перекрывающихся операций чтения и записи в одних и тех же разделах буфера, чтобы избежать зависания потоков. Это то, что Джон Кармак, возможно, выяснил, как решить в своих играх; по крайней мере, он говорит об этом так, будто почти видит решение, не открывая червячную машину. Я не дошел до него в этом отношении. Все, что я вижу, - это бесконечные вопросы дизайна, если я попытаюсь просто распараллелить все вокруг неизменяемых. Хотел бы я потратить день на то, чтобы поразмыслить над его мозгом, поскольку большинство моих усилий начиналось с тех идей, которые он выбрасывал.
Тем не менее, я нашел огромное значение этой неизменной структуры данных в других областях. Я даже использую его сейчас для хранения изображений, что действительно странно и требует, чтобы произвольный доступ требовал еще нескольких инструкций (сдвиг вправо и побитовый, and
а также слой косвенного указателя), но я расскажу о преимуществах ниже.
Отменить систему
Одним из самых непосредственных мест, которые я нашел, чтобы извлечь выгоду из этого, была система отмены. Системный код отмены был одной из наиболее подверженных ошибкам вещей в моей области (индустрия визуальных эффектов), причем не только в продуктах, над которыми я работал, но и в конкурирующих продуктах (их системы отмены также были ненадежными), потому что было очень много разных типы данных, о которых нужно беспокоиться об отмене и повторном редактировании (система свойств, изменения данных сетки, изменения шейдеров, которые не были основаны на свойствах, такие как замена одного на другой, изменения иерархии сцены, такие как смена родителя дочернего элемента, изменения изображения / текстуры, и тд и тп)
Таким образом, объем требуемого кода отмены был огромен, часто соперничая с объемом кода, реализующего систему, для которой система отмены должна была регистрировать изменения состояния. Опираясь на эту структуру данных, я смог свести систему отмены к следующему:
on user operation:
copy entire application state to undo entry
perform operation
on undo/redo:
swap application state with undo entry
Обычно этот код, приведенный выше, будет чрезвычайно неэффективным, когда данные вашей сцены занимают гигабайты, чтобы полностью скопировать их. Но эта структура данных копирует только те вещи, которые не были изменены, и это фактически сделало его достаточно дешевым для хранения неизменной копии всего состояния приложения. Так что теперь я могу реализовать системы отмены так же легко, как и приведенный выше код, и просто сосредоточиться на использовании этой неизменной структуры данных, чтобы копирование неизмененных частей состояния приложения становилось все дешевле, дешевле и дешевле. С тех пор, как я начал использовать эту структуру данных, все мои личные проекты имеют системы отмены, использующие этот простой шаблон.
Теперь здесь все еще есть некоторые накладные расходы. В прошлый раз, когда я измерил, было около 10 килобайт, просто для поверхностного копирования всего состояния приложения без внесения в него каких-либо изменений (это не зависит от сложности сцены, поскольку сцена организована в иерархии, поэтому, если ничего не меняется под корнем, только корень мелко скопировано без необходимости спускаться в детей). Это далеко от 0 байтов, что необходимо для системы отмены, хранящей только дельты. Но при 10 килобайтах накладных расходов на отмену на операцию, это все равно только мегабайт на 100 пользовательских операций. Кроме того, я все еще мог бы в будущем, если понадобится, раздавить это дальше.
Исключение-безопасность
Исключительная безопасность при сложном применении не является тривиальным вопросом. Тем не менее, когда состояние вашего приложения является неизменным, а вы используете временные объекты только для того, чтобы попытаться зафиксировать транзакции атомарного изменения, это по своей сути безопасно для исключений, поскольку, если какая-либо часть кода выбрасывается, переходный процесс отбрасывается перед предоставлением новой неизменной копии. , Так что это упрощает одну из самых сложных вещей, которые я всегда находил в сложной кодовой базе C ++.
Слишком много людей часто просто используют RAII-совместимые ресурсы в C ++ и думают, что этого достаточно, чтобы быть безопасными для исключений. Часто это не так, так как функция обычно может вызывать побочные эффекты для состояний, выходящих за пределы области действия. Как правило, в этих случаях вам нужно начинать работать с защитой области видимости и сложной логикой отката. Эта структура данных сделала это таким образом, что мне часто не нужно беспокоиться об этом, поскольку функции не вызывают побочных эффектов. Они возвращают преобразованные неизменные копии состояния приложения вместо преобразования состояния приложения.
Неразрушающее редактирование
Неразрушающее редактирование - это, в основном, операции наложения / укладки / соединения, не затрагивая исходные данные пользователя (просто ввод данных и вывод данных, не касаясь ввода). Обычно это просто реализовать с помощью простого графического приложения, такого как Photoshop, и эта структура данных может не принести особой выгоды, поскольку многие операции могут просто захотеть преобразовать каждый пиксель всего изображения.
Однако, например, при неразрушающем редактировании сетки многие операции часто хотят преобразовать только часть сетки. Одна операция может просто захотеть переместить некоторые вершины сюда. Другой может просто захотеть разделить некоторые полигоны там. В этом случае неизменяемая структура данных помогает избежать необходимости делать полную копию всей сетки только для того, чтобы вернуть новую версию сетки с небольшой измененной частью.
Минимизация побочных эффектов
Имея эти структуры в руках, он также позволяет легко писать функции, которые сводят к минимуму побочные эффекты без значительного снижения производительности. Я обнаружил, что пишу все больше и больше функций, которые просто возвращают целые неизменяемые структуры данных по значению в наши дни, не вызывая побочных эффектов, даже когда это кажется немного расточительным.
Например, обычно искушение преобразовать группу позиций может состоять в том, чтобы принять матрицу и список объектов и преобразовать их в изменяемый способ. В эти дни я просто возвращаю новый список объектов.
Когда в вашей системе есть больше подобных функций, которые не вызывают побочных эффектов, это определенно облегчает рассуждение о его правильности, а также проверку его правильности.
Преимущества дешевых копий
Так или иначе, именно в этих областях я нашел наибольшее применение неизменяемых структур данных (или постоянных структур данных). Я также немного переусердствовал изначально и создал неизменное дерево, неизменный связанный список и неизменную хеш-таблицу, но со временем я редко находил такую возможность. В основном я обнаружил, что наибольшее использование массивного неизменяемого массива-подобного контейнера показано на диаграмме выше.
У меня также все еще есть много кода, работающего с изменяемыми (нахожу это практической необходимостью, по крайней мере, для низкоуровневого кода), но основное состояние приложения - это неизменяемая иерархия, переходящая от неизменяемой сцены к неизменяемым компонентам внутри нее. Некоторые из более дешевых компонентов все еще копируются полностью, но самые дорогие, такие как сетки и изображения, используют неизменяемую структуру, чтобы позволить этим частичным дешевым копиям только те части, которые необходимо преобразовать.
ConcurrentModificationException
которое обычно вызывается тем же потоком, мутирующим коллекцию в том же потоке, в телеforeach
цикла над той же коллекцией.