Тут є кілька приємних прикладів, але я хотів заскочити з деякими особистими, де незмінність допомагала тоні. У моєму випадку я почав розробляти незмінну одночасну структуру даних, головним чином з надією на те, що я зможу впевнено запустити код паралельно з перекриттям читання та запису та не турбуватися про умови перегонів. Була розмова, Джон Кармак дав мені таке натхнення, щоб я це робив там, де він говорив про таку ідею. Це досить основна структура і досить тривіальна для реалізації така:
Звичайно, з ще кількома дзвіночками, як би в змозі вилучати елементи за постійний час і залишати відновлювані отвори позаду, а блоки забруднені, якщо вони стануть порожніми та потенційно звільненими для даного непорушного екземпляра. Але в основному, щоб змінити структуру, ви модифікуєте "тимчасову" версію і атомарно здійснюєте внесені вами зміни, щоб отримати нову незмінну копію, яка не торкається старої, при цьому нова версія створює лише нові копії блоків, які повинні бути унікальними, тоді як дрібне копіювання та посилання інших рахунків.
Однак я не знайшов цьогокорисний для багатопотокових цілей. Зрештою, існує ще концептуальна проблема, коли, скажімо, фізична система застосовує фізику одночасно, поки гравець намагається переміщати елементи у світі. З якою незмінною копією перетворених даних ви збираєтесь перетворити програвач, або з перетвореною фізичною системою? Тому я не знайшов приємного і простого вирішення цієї основної концептуальної проблеми, за винятком змінних структур даних, які просто фіксуються розумнішим способом і не дозволяють перекриватись читанням і записом у ті самі розділи буфера, щоб уникнути затримки потоків. Це, здається, Джон Кармак, можливо, придумав, як вирішити свої ігри; принаймні, він про це говорить так, що майже бачить рішення, не відкриваючи машину глистів. Я в цьому відношенні не зрозумів його. Все, що я бачу, - це нескінченні дизайнерські запитання, якщо я намагався просто паралельно встановити все навколо незмінного. Я б хотів, щоб я міг провести день, вибираючи його мозок, оскільки більшість моїх зусиль почалися з тих ідей, які він викинув.
Тим не менш, я знайшов величезну цінність цієї непорушної структури даних в інших областях. Я навіть зараз використовую його для зберігання зображень, що насправді дивно, і робить випадковий доступ вимагає ще декількох інструкцій (зсув праворуч і побіжно and
разом із шаром непрямості вказівника), але я висвітлю переваги нижче.
Скасувати систему
Одним з найбільш безпосередніх місць, які я знайшов користь від цього, була система скасування. Скасувати системний код, який був одним із найбільш схильних до помилок у моєму регіоні (візуальна індустрія FX), і не тільки в продуктах, над якими я працював, але і в конкуруючих продуктах (їхні системи скасування також були невмілими), оскільки було так багато різних типи даних, які турбуються про те, щоб скасувати та переробити належним чином (система властивостей, зміна даних в сітці, зміни шейдерів, які не базувались на властивості, як, наприклад, заміни однієї на іншу, зміни в ієрархії сцен, як-от зміна батька дитини, зміна зображення / текстури, і т. д. тощо).
Таким чином, необхідний обсяг скасування коду був величезним, часто суперечив кількості коду, що реалізує систему, для якої система скасування повинна була фіксувати зміни стану. Спираючись на цю структуру даних, я зміг звести систему скасування лише до цього:
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
петлі над тією ж колекцією.