Невже незмінна цінність, коли немає одночасності?


53

Здається, що безпека ниток завжди / часто згадується як головна перевага використання незмінних типів і особливо колекцій.

У мене є ситуація, коли я хотів би переконатися, що метод не змінює словник рядків (які незмінні в C #). Я хотів би якомога більше обмежувати речі.

Однак я не впевнений, чи варто додати залежність до нового пакету (Microsoft Immutable Collections). Продуктивність теж не є великою проблемою.

Так що я думаю , моє запитання , чи є незмінні колекції настійно рекомендується, коли немає жорстких вимог до продуктивності і немає ніяких проблем безпеки потоку? Врахуйте, що семантика значень (як у моєму прикладі) може бути, а може і не бути вимогою.


1
Одночасна модифікація не повинна означати потоки. Подивіться на влучно названий, ConcurrentModificationExceptionякий, як правило, викликаний тим самим потоком, що мутує колекцію в тій же нитці, в тілі foreachпетлі над тією ж колекцією.

1
Я маю на увазі, що ви не помиляєтесь, але це інше, ніж те, що просить ОП. Цей виняток кидається, оскільки зміни під час перерахування не дозволяються. Наприклад, використовуючи ConcurrentDictionary, ця помилка все-таки буде.
edthethird

13
Можливо, ви також повинні задати собі протилежне запитання: коли варто змінюватися?
Джорджіо

2
У Java, якщо зміни мутабельності впливають hashCode()або equals(Object)результат змінюється, це може спричинити помилки при використанні Collections(наприклад, в HashSetоб'єкті зберігався у «відрі», а після його вимкнення він повинен перейти до іншого).
SJuan76

2
@ DavorŽdralo Що стосується абстракцій мов високого рівня, то повсюдна незмінність досить приручена. Це лише природне продовження дуже поширеної абстракції (присутньої навіть у С) створення та мовчки відкидання "тимчасових цінностей". Можливо, ви хочете сказати, що це неефективний спосіб використання центрального процесора, але і цей аргумент має вади: дуже радісні, але динамічні мови часто працюють гірше, ніж незмінні, але статичні мови, почасти тому, що є кілька розумних (але в кінцевому підсумку досить простих) прийоми оптимізації програм жонглювання незмінними даними: лінійні типи, вирубка лісів тощо

Відповіді:


101

Незмінюваність спрощує кількість інформації, яку потрібно подумки відстежувати, читаючи код пізніше . Для змінних змінних, і особливо членів класу, що змінюються, дуже важко знати, у якому стані вони будуть знаходитись у конкретному рядку, про який ви читаєте, не пробігаючи код з відладчиком. Невідмінні дані легко обґрунтувати - вони завжди будуть однаковими. Якщо ви хочете змінити це, вам потрібно внести нове значення.

Я б віддавав перевагу робити речі незмінними за замовчуванням , а потім змінювати їх на змінні там, де доведено, що вони повинні бути, чи це означає, що вам потрібна продуктивність, чи алгоритм, який у вас немає, не має сенсу для незмінності.


23
+1 Паралельність - це одночасна мутація, але про мутацію, яка поширюється з часом, може бути так само важко міркувати
guillaume31

20
Для розширення цього питання: функцію, що спирається на змінну змінну, можна вважати прийняттям додаткового прихованого аргументу, який є поточним значенням змінної змінної. Будь-яка функція, яка змінює змінну змінну, також може вважатися такою, що створює додаткове повернене значення, тобто нове значення стану, що змінюється. Дивлячись на фрагмент коду, ви не маєте поняття, чи залежить він або змінить стан, що змінюється, тому вам доведеться з’ясувати, а потім подумки відстежувати зміни. Це також вводить з'єднання між будь-якими двома фрагментами коду, які поділяють стан, що змінюється, і зв'язок поганий.
Doval

29
@Mehrdad Люди також встигали десятиліттями збирати великі програми в зборах. Потім ми зробили пару десятиліть C.
Doval

11
@Mehrdad Копіювання цілих об'єктів не є хорошим варіантом, коли об’єкти великі. Я не розумію, чому має значення порядок участі в поліпшенні. Чи відмовитесь ви на 20% покращення (зауважте: довільне число) в продуктивності просто тому, що це не було тризначне покращення? Незмінність - це розумний стандарт ; ви можете збитися з цього, але вам потрібна причина.
Doval

9
@ Джорджіо Скала змусив мене зрозуміти, як нечасто потрібно навіть зробити змінне значення. Щоразу, коли я використовую цю мову, я все роблю val, і лише за дуже, дуже рідкісних випадків я виявляю, що мені потрібно щось змінити на var. Дуже багато «змінних», які я визначаю в будь-якій мові, якраз там, щоб містити значення, яке зберігає результат деяких обчислень, і не потребує оновлення.
KChaloux

22

Ваш код повинен виражати ваш намір. Якщо ви не хочете, щоб об’єкт змінювався після створення, унеможливлюйте його зміни.

Незмінність має ряд переваг:

  • Намір оригіналу автора виражається краще.

    Як ви могли б знати, що в наведеному нижче коді зміна імені призведе до того, що програма буде генерувати виняток десь пізніше?

    public class Product
    {
        public string Name { get; set; }
    
        ...
    }
    
  • Простіше переконатися, що об’єкт не з’явився б у недійсному стані.

    Ви повинні контролювати це в конструкторі, і тільки там. З іншого боку, якщо у вас є купа сетерів і методів, які змінюють об'єкт, такі елементи управління можуть стати особливо складними, особливо коли, наприклад, два поля повинні змінюватися одночасно для того, щоб об’єкт був дійсним.

    Наприклад, об’єкт дійсний, якщо адреси немає null або координати GPS немає null, але він недійсний, якщо вказані і адреса, і координати GPS. Чи можете ви уявити собі чорт, щоб підтвердити це, якщо і адреса, і координати GPS мають сеттер, або обидва є змінними?

  • Паралельність.

До речі, у вашому випадку вам не потрібні сторонні пакети. .NET Framework вже включає ReadOnlyDictionary<TKey, TValue>клас.


1
+1, особливо для "Ви повинні контролювати це в конструкторі, і тільки там". ІМО - це величезна перевага.
Джорджіо

10
Ще одна перевага: копіювання об’єкта безкоштовне. Просто вказівник.
Роберт Грант

1
@MainMa Дякую за вашу відповідь, але, наскільки я розумію, ReadOnlyDictionary не дає жодних гарантій, що хтось інший не змінить основний словник (навіть без одночасності я можу захотіти зберегти посилання на оригінальний словник в межах об'єкта, яким належить метод для подальшого використання). ReadOnlyDictionary навіть оголошений у дивному просторі імен: System.Collections.ObjectModel.
День

2
@Den: Це стосується одного з моїх домашніх вихованців: людей, які відносяться до "лише для читання" та "незмінних" як синоніми. Якщо об’єкт інкапсульований у обгортці, що працює лише для читання, і жодної іншої посилання не існує або зберігається в будь-якому місці Всесвіту, то загортання об'єкта зробить його непорушним, і посилання на обгортку може бути використана як скорочення для інкапсуляції стану об'єкт, що міститься в ньому. Однак не існує механізму, за допомогою якого код може встановити, чи так це. Навпаки, тому що обгортка приховує тип обгорнутого предмета, обгортаючи непорушний предмет ...
supercat

2
... унеможливить код не знати, чи можна обгортку, що отримана в результаті, безпечно вважати непорушною.
supercat

13

Є багато однониткових причин використовувати незмінність. Наприклад

Об'єкт A містить Об'єкт B.

Зовнішній код запитує ваш об’єкт B, і ви повертаєте його.

Зараз у вас є три можливі ситуації:

  1. B непорушний, немає проблем.
  2. B є змінним, ви робите захисну копію і повертаєте її. Продуктивність хіт, але без ризику.
  3. B - змінна, ти повертаєш її.

У третьому випадку код користувача може не усвідомлювати, що ви зробили, і може вносити зміни в об'єкт, і тим самим змінювати внутрішні дані вашого об'єкта, не маючи ніякого контролю або видимості того, що відбувається.


9

Незмінність також може значно спростити реалізацію сміттєзбірників. З вікі GHC :

[...] Незмінність даних змушує нас створювати багато тимчасових даних, але це також допомагає швидко збирати це сміття. Хитрість полягає в тому, що незмінні дані НІКОЛИ не вказують на молодші значення. Дійсно, молодші значення ще не існують у той момент, коли створюється старе значення, тому його не можна вказувати з нуля. А оскільки значення ніколи не змінюються, їх не можна вказувати і пізніше. Це ключова властивість незмінних даних.

Це значно спрощує збирання сміття (GC). У будь-який час ми можемо сканувати останні створені значення та звільнити ті, на які не вказано, з одного набору (звичайно, справжні корені ієрархії живих значень живуть у стеку). [...] Отже, вона має контрінтуїтивну поведінку: чим більший відсоток ваших цінностей - сміття - тим швидше це працює. [...]


5

Розгортання того, що KChaloux підсумував дуже добре ...

В ідеалі у вас є два типи полів, а отже, і два типи коду з їх використанням. Будь-які поля незмінні, і код не повинен враховувати змінність; або поля є змінними, і нам потрібно написати код, який або робить знімок ( int x = p.x), або витончено обробляє такі зміни.

На мій досвід, більшість кодів знаходиться між двома, будучи оптимістичним кодом: він вільно посилається на змінні дані, припускаючи, що перший виклик p.xматиме такий же результат, як і другий виклик. І більшу частину часу це правда, за винятком випадків, коли виявляється, цього більше немає. На жаль

Тож справді розверніть це питання: які мої причини зробити цю зміну ?

  • Зменшення алокації пам'яті / безкоштовно?
  • Змінний від природи? (наприклад, лічильник)
  • Економить модифікатори, горизонтальний шум? (const / final)
  • Чи робить код коротшим / простішим? (init за замовчуванням, можливо перезаписати після)

Ви пишете захисний код? Незмінність заощадить ваші копії. Ви пишете оптимістичний код? Незмінюваність позбавить вас від божевілля цього дивного, неможливого помилки.


3

Ще одна перевага непорушності полягає в тому, що це перший крок в округленні цих незмінних об'єктів до пулу. Тоді ви можете керувати ними, щоб не створювати кілька об'єктів, які концептуально та семантично представляють одне і те ж. Хорошим прикладом може бути струна Java.

Загальновідоме в лінгвістичному явищі те, що кілька слів з’являється багато, може з'являтися і в іншому контексті. Тож замість створення кількох Stringоб'єктів можна використовувати один незмінний. Але тоді вам потрібно тримати менеджера басейну, щоб опікуватися цими незмінними об’єктами.

Це заощадить багато пам’яті. Цю цікаву статтю можна прочитати також: http://en.wikipedia.org/wiki/Zipf%27s_law


1

У Java, C # та інших подібних мовах поля класового типу можуть використовуватися або для ідентифікації об'єктів, або для інкапсуляції значень або стану в цих об'єктах, але мови не відрізняють таких звичаїв. Припустимо, об’єкт класу Georgeмає поле типу char[] chars;. Це поле може інкапсулювати послідовність символів у будь-якому:

  1. Масив, який ніколи не буде модифікований і не піддається впливу будь-якого коду, який може його змінити, але до якого можуть існувати зовнішні посилання.

  2. Масив, на який не існує зовнішніх посилань, але який Джордж може вільно змінювати.

  3. Масив, який належить Джорджу, але якому можуть існувати зовнішні погляди, які, як очікується, представлятимуть сучасний стан Джорджа.

Крім того, змінна може замість інкапсуляції послідовності символів інкапсулювати живий вигляд у послідовність символів, що належить якомусь іншому об'єкту

Якщо в charsданий час інкапсулює послідовність символів [вітер], а Джордж хоче charsінкапсулювати послідовність символів [паличку], то Джордж міг би зробити:

A. Побудуйте новий масив, що містить символи [паличку] та змініть, charsщоб ідентифікувати цей масив, а не старий.

B. Визначте якось наявний масив символів, який завжди буде містити символи [паличку] та змінити, charsщоб ідентифікувати цей масив, а не старий.

C. Змініть другий символ масиву, ідентифікований символом charsна a.

У випадку 1 (А) та (В) - безпечні способи досягнення бажаного результату. У випадку, якщо (2), (A) і (C) є безпечними, але (B) не було б [це не спричинило б негайних проблем, але оскільки Джордж припустив, що володіє масивом, він вважає, що він може змінити масив за бажанням]. У випадку (3) вибір (A) і (B) порушить будь-які зовнішні погляди, і таким чином правильний лише вибір (C). Таким чином, знати, як змінити послідовність символів, інкапсульовану полем, потрібно знати, який це семантичний тип поля це.

Якщо замість того, щоб використовувати поле типу char[], яке інкапсулює потенційно змінену послідовність символів, у коді використовується тип String, який інкапсулює незмінну послідовність символів, усі вищезазначені проблеми відпадають. Усі поля типу Stringінкапсулюють послідовність символів, використовуючи спільний об'єкт, який ніколи не зміниться. Отже, якщо поле типуStringінкапсулює "вітер", єдиний спосіб змусити його капсулювати "паличку" - це змусити її ідентифікувати різний об'єкт - той, який утримує "паличку". У випадках, коли в коді є єдине посилання на об'єкт, мутація об'єкта може бути ефективнішою, ніж створення нового, але в будь-який час, коли клас змінюється, слід розрізняти різні способи, за допомогою яких він може інкапсулювати значення. Особисто я думаю, що для цього мали використовуватись угорські програми (я б вважав чотири варіанти char[]семантично виразними типами, хоча система типів вважає їх однаковими - саме таку ситуацію, де світить Apps Hungarian), але оскільки Не найпростішим способом уникнути таких неоднозначностей є створення незмінних типів, які лише інкапсулюють значення в один бік.


Це виглядає як розумна відповідь, але трохи важко читати і розуміти.
День

1

Тут є кілька приємних прикладів, але я хотів заскочити з деякими особистими, де незмінність допомагала тоні. У моєму випадку я почав розробляти незмінну одночасну структуру даних, головним чином з надією на те, що я зможу впевнено запустити код паралельно з перекриттям читання та запису та не турбуватися про умови перегонів. Була розмова, Джон Кармак дав мені таке натхнення, щоб я це робив там, де він говорив про таку ідею. Це досить основна структура і досить тривіальна для реалізації така:

введіть тут опис зображення

Звичайно, з ще кількома дзвіночками, як би в змозі вилучати елементи за постійний час і залишати відновлювані отвори позаду, а блоки забруднені, якщо вони стануть порожніми та потенційно звільненими для даного непорушного екземпляра. Але в основному, щоб змінити структуру, ви модифікуєте "тимчасову" версію і атомарно здійснюєте внесені вами зміни, щоб отримати нову незмінну копію, яка не торкається старої, при цьому нова версія створює лише нові копії блоків, які повинні бути унікальними, тоді як дрібне копіювання та посилання інших рахунків.

Однак я не знайшов цьогокорисний для багатопотокових цілей. Зрештою, існує ще концептуальна проблема, коли, скажімо, фізична система застосовує фізику одночасно, поки гравець намагається переміщати елементи у світі. З якою незмінною копією перетворених даних ви збираєтесь перетворити програвач, або з перетвореною фізичною системою? Тому я не знайшов приємного і простого вирішення цієї основної концептуальної проблеми, за винятком змінних структур даних, які просто фіксуються розумнішим способом і не дозволяють перекриватись читанням і записом у ті самі розділи буфера, щоб уникнути затримки потоків. Це, здається, Джон Кармак, можливо, придумав, як вирішити свої ігри; принаймні, він про це говорить так, що майже бачить рішення, не відкриваючи машину глистів. Я в цьому відношенні не зрозумів його. Все, що я бачу, - це нескінченні дизайнерські запитання, якщо я намагався просто паралельно встановити все навколо незмінного. Я б хотів, щоб я міг провести день, вибираючи його мозок, оскільки більшість моїх зусиль почалися з тих ідей, які він викинув.

Тим не менш, я знайшов величезну цінність цієї непорушної структури даних в інших областях. Я навіть зараз використовую його для зберігання зображень, що насправді дивно, і робить випадковий доступ вимагає ще декількох інструкцій (зсув праворуч і побіжно 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, і, можливо, це не виграє від цієї структури даних, оскільки багато операцій можуть просто хочуть перетворити кожен піксель всього зображення.

Однак, наприклад, при неруйнівному редагуванні сітки, багато операцій часто хочуть перетворити лише частину сітки. Однією операцією може просто захотіти перемістити тут деякі вершини. Інший, можливо, просто захоче там поділити кілька полігонів. Тут незмінна структура даних допомагає уникнути необхідності робити цілу копію всієї сітки лише для повернення нової версії сітки з невеликою частиною її зміненої.

Мінімізація побічних ефектів

Маючи ці структури в руці, це також дозволяє легко писати функції, що мінімізують побічні ефекти, не несучи за це величезну штрафну ефективність. У наші дні я можу писати все більше і більше функцій, які просто повертають цілі незмінні структури даних за значенням, не викликаючи побічних ефектів, навіть коли це здається трохи марнотратним.

Наприклад, як правило, спокуса перетворити купу позицій може полягати в тому, щоб прийняти матрицю і список об'єктів і перетворити їх на змінне. У ці дні я вважаю, що просто повертаю новий список об’єктів.

Якщо у вас в системі є більше подібних функцій, які не викликають побічних ефектів, це, безумовно, полегшує міркування про її правильність, а також перевірити її правильність.

Переваги дешевих копій

Отож, у будь-якому випадку це ті сфери, де я знайшов найбільше використання непорушних структур даних (або стійких структур даних). Спочатку я трохи перестарався і склав незмінне дерево та незмінний зв'язаний список та незмінну хеш-таблицю, але з часом я рідко знаходив стільки користі для них. В основному я знайшов найбільше використання кучерявого незмінного масиву, подібного до контейнера, на схемі вище.

У мене ще є багато коду, що працює з мутабелями (вважаю це практичною необхідністю принаймні для коду низького рівня), але основним станом програми є незмінна ієрархія, що перетворюється з незмінної сцени на незмінні компоненти всередині неї. Деякі з дешевих компонентів все ще копіюються в повному обсязі, але найдорожчі, такі як сітки та зображення, використовують незмінну структуру, щоб дозволити ті часткові дешеві копії лише тих частин, які потрібно було перетворити.


0

Вже багато хороших відповідей. Це лише додаткова інформація, дещо характерна для .NET. Я копав старі публікації блогу .NET і знайшов хороший підсумок переваг з точки зору розробників Microsoft Immutable Collections:

  1. Семантика знімків дозволяє вам ділитися своїми колекціями таким чином, що приймач може розраховувати на те, що він не зміниться.

  2. Неявна безпека потоків у багатопотокових програмах (для доступу до колекцій не потрібні блоки).

  3. Кожен раз, коли у вас є член класу, який приймає або повертає тип колекції і ви хочете включити в контракт семантику лише для читання.

  4. Функціональне програмування.

  5. Дозволити модифікацію колекції під час перерахування, дотримуючись того, щоб оригінальна колекція не змінювалася.

  6. Вони реалізують ті ж інтерфейси IReadOnly *, з якими вже працює ваш код, тому міграція проста.

Якщо хтось передає вам ReadOnlyCollection, IReadOnlyList або IEnumerable, єдиною гарантією є те, що ви не можете змінити дані - немає гарантії, що особа, яка передала вам колекцію, не змінить її. Однак вам часто потрібна певна впевненість, що вона не зміниться. Ці типи не пропонують події, щоб сповіщати вас про зміну їх вмісту, і якщо вони змінюються, можуть виникнути в іншому потоці, можливо, під час перерахування його вмісту? Така поведінка призведе до пошкодження даних та / або випадкових винятків у вашій програмі.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.