Змінні та незмінні об'єкти


173

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


stringє незмінним, принаймні в .NET, і я думаю, що і в багатьох інших сучасних мовах.
Доменік

4
це залежить від того, що насправді є String у мові - erlang 'рядки' є лише масивом ints, а haskell "рядки" - це масиви символів.
Chii

4
Рубінні рядки - це змінний масив байтів. Абсолютно заганяє мене, що ви можете змінити їх на місці.
Даніель Шпієк

6
Даніель - чому? Чи трапляєте ви це випадково?

5
@DanielSpiewak Рішення для запускання гайок, що ви можете змінювати рядки на місці, просте: просто не робіть цього. Рішення для приводу гайок, оскільки ви не можете змінити рядки на місці, не так просто.
Каз

Відповіді:


162

Ну, є кілька аспектів цього.

  1. Об'єкти, що змінюються, без посилальної ідентичності можуть викликати помилки у непарний час. Наприклад, розглянемо Personквасоля із equalsметодом на основі значення :

    Map<Person, String> map = ...
    Person p = new Person();
    map.put(p, "Hey, there!");
    
    p.setName("Daniel");
    map.get(p);       // => null
    

    PersonПримірник отримує «втратив» в карті , коли використовується в якості ключа , тому що його hashCodeі рівність були засновані на змінюваних значень. Ці значення змінилися за межами карти, і все хешування стало застарілим. Теоретики люблять робити аргументи з цього приводу, але на практиці я не вважаю, що це занадто велика проблема.

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

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

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

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


11
Великий відгук. Однак одне невелике запитання: чи C ++ не має хорошої підтримки для незмінності? Чи недостатня функція коректності const ?
Димитрій К.

1
@DimitriC.: C ++ насправді має деякі більш фундаментальні особливості, особливо краще розрізнення місць зберігання, які, як передбачається, інкапсулюють неподілений стан, від тих, які інкапсулюють ідентичність об'єкта.
supercat

2
У випадку програмування Java Джошуа Блох дає хороші пояснення на цю тему у своїй знаменитій книзі « Ефективна Java» (Пункт 15)
dMathieuD

27

Незмінні об'єкти та незмінні колекції

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

Незмінна колекція - це колекція, яка ніколи не змінюється.

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

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

Розумним реалізаціям не обов’язково потрібно копіювати (клонувати) всю колекцію, щоб забезпечити цю незмінність. Найпростішим прикладом є стек, реалізований у вигляді окремо пов'язаного списку та операцій push / pop. Ви можете повторно використовувати всі вузли з попередньої колекції в новій колекції, додавши лише один вузол для натискання та не клонувавши вузлів для поп-файлу. Операція push_tail в окремо пов'язаному списку, з іншого боку, не така проста чи ефективна.

Незмінні проти змінних змінних / посилань

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

  • У Erlang це справедливо для всіх "змінних". Я можу призначити об'єкти посилання лише один раз. Якби я працював над колекцією, я не зміг би перенести нову колекцію до старої посилання (назва змінної).
  • Scala також вбудовує це у мову, де всі посилання декларуються за допомогою var або val , при цьому вали мають лише одне призначення та підтримують функціональний стиль, але vars дозволяють створювати більше C-подібну або Java-подібну структуру програми.
  • Декларація var / val необхідна, тоді як у багатьох традиційних мовах використовуються необов'язкові модифікатори, такі як final у java та const у C.

Простота розробки проти продуктивності

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

Основний недолік - продуктивність. Ось запис простий тест, який я зробив на Java, порівнюючи деякі непорушні та незмінні предмети в іграшці.

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


12

Перевірте це повідомлення в блозі: http://www.yegor256.com/2014/06/09/objects-should-be-immutable.html . Це пояснює, чому незмінні предмети краще, ніж незмінні. Коротко:

  • незмінні об'єкти простіші для побудови, тестування та використання
  • справді незмінні об'єкти завжди безпечні для ниток
  • вони допомагають уникнути часової зв’язки
  • їх використання не має побічних ефектів (відсутні захисні копії)
  • Проблема мутаційності особистості уникається
  • вони завжди мають атомну атомність
  • їх набагато простіше кешувати

10

Незмінні об'єкти - це дуже потужне поняття. Вони знімають багато тягаря, намагаючись зберегти об'єкти / змінні послідовними для всіх клієнтів.

Ви можете використовувати їх для об'єктів низького рівня, які не є поліморфними - як клас CPoint - які використовуються здебільшого зі значеннями семантики.

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

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

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

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


Я думаю, що я неправильно зрозумів Розділ 4 для найбільшої переваги: ​​незмінність + семантика об'єкта + розумні покажчики перетворюють право власності на об'єкт у точку «суперечки». Так це дискусійно? Я думаю, що ви неправильно використовуєте "moot" ... Бачачи, що наступне речення є об'єктом, має на увазі "детерміновану поведінку" від його "суперечливої" (дискусійної) поведінки.
Бенджамін

Ви маєте рацію, я неправильно використовував "moot". Вважайте, що це змінилося :)
QBziZ

6

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


Ви припускаєте, що нитки квантово заплутані? Це досить розтягнення :) Нитки насправді, якщо подумати над цим, досить близькі до того, щоб заплутатися. Зміна, яку вносить одна нитка, впливає на іншу. +1
ndrewxie

4

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

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


1

Якщо тип класу є змінним, змінна цього типу класу може мати декілька різних значень. Наприклад, припустимо, що об’єкт fooмає поле int[] arr, і він містить посилання на int[3]проведення чисел {5, 7, 9}. Незважаючи на те, що тип поля відомий, є щонайменше чотири різні речі, які він може представляти:

  • Потенційно спільна посилання, усіх власників якої хвилює лише те, що вона інкапсулює значення 5, 7 та 9. Якщо fooхоче arrінкапсулювати різні значення, вона повинна замінити її на інший масив, який містить бажані значення. Якщо ви хочете зробити копію foo, можна надати копії або посилання на arrновий масив із значеннями {1,2,3}, залежно від того, що зручніше.

  • Єдина посилання в будь-якій точці Всесвіту на масив, який інкапсулює значення 5, 7 та 9. набір з трьох місць зберігання, які на даний момент містять значення 5, 7 та 9; якщо fooвін хоче інкапсулювати значення 5, 8 і 9, він може або змінити другий елемент у цьому масиві, або створити новий масив із значеннями 5, 8 і 9 і відмовитися від старого. Зауважте, що якщо хтось хотів зробити копію foo, її потрібно замінити arrпосиланням на новий масив, foo.arrщоб він залишався єдиним посиланням на цей масив у будь-якій точці Всесвіту.

  • Посилання на масив, який належить якомусь іншому об'єкту, який його fooчомусь оголив (наприклад, можливо, він хоче fooзберегти там деякі дані). У цьому випадку сценарій arrне інкапсулює вміст масиву, а його ідентичність . Оскільки заміна arrна посилання на новий масив повністю змінила його значення, копія fooмає містити посилання на той самий масив.

  • Посилання на масив, який fooє єдиним власником, але на який посилання з якихось причин утримує інший об’єкт (наприклад, він хоче мати інший об'єкт для зберігання там даних - зворотний бік попереднього випадку). У цьому сценарії arrінкапсулює як ідентифікацію масиву, так і його вміст. Заміна arrпосиланням на новий масив повністю змінила б його значення, але наявність arrпосилання на foo.arrклонінг порушила б припущення, що він fooє єдиним власником. Таким чином, немає можливості скопіювати foo.

Теоретично, це int[]повинен бути хороший простий чітко визначений тип, але він має чотири дуже різні значення. Навпаки, посилання на незмінний об'єкт (наприклад String), як правило, має лише одне значення. Значна частина "сили" незмінних предметів випливає з цього факту.


1

Змінні екземпляри передаються шляхом посилання.

Невідмінні екземпляри передаються за значенням.

Абстрактний приклад. Припустимо, що на моєму жорсткому диску існує файл з назвою txtfile . Тепер, коли ви запитаєте txtfile у мене, я можу повернути його у двох режимах:

  1. Створіть ярлик до txtfile та пасі ярлика до вас, або
  2. Візьміть копію для txtfile і вставте копію.

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

У другому режимі повернений txtfile є незмінним файлом, оскільки всі зміни в отриманому файлі не стосуються вихідного файлу. Перевагою цього режиму є те, що тільки я (власник) може змінювати оригінальний файл, а недоліком є ​​те, що кожна повернута копія вимагає пам'яті (в оперативній пам'яті або на жорсткому диску).


Це не зовсім вірно. Невідмінні екземпляри дійсно можуть передаватися посиланням, і часто є. Копіювання в основному робиться тоді, коли вам потрібно оновити об’єкт або структуру даних.
Джеймон Холмгрен

0

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


0

Незмінні засоби неможливо змінити, а змінні засоби - ви можете змінити.

Об'єкти відрізняються від примітивів на Java. Примітиви будуються за типами (булева, int тощо), а об'єкти (класи) - типи, створені користувачем.

Примітиви та об'єкти можуть бути змінними або незмінними, коли їх визначають як змінні учасники в рамках реалізації класу.

Дуже багато людей вважають, що примітиви та змінні об'єктів, що мають перед собою остаточний модифікатор, є незмінними, однак це не зовсім так. Отже, остаточне майже не означає незмінного для змінних. Дивіться приклад тут
http://www.siteconsortium.com/h/D0000F.php .


0

Immutable object- це стан об'єкта, який не може бути змінений після створення. Об'єкт є незмінним, коли всі його поля незмінні

Нитка безпечна

Основна перевага Immutable об'єкта полягає в тому, що це природно для одночасного середовища. Найбільша проблема паралельності - це те, shared resourceщо можна змінити будь-яку нитку. Але якщо об'єкт є незмінним, це read-onlyбезпечна експлуатація ниток. Будь-яка модифікація оригінального незмінного об'єкта повертає копію

побічні ефекти безкоштовні

Як розробник, ви повністю впевнені, що стан незмінного об'єкта неможливо змінити з будь-якого місця (спеціально чи ні)

скласти оптимізацію

Підвищення продуктивності

Недолік:

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

Для створення immutableоб'єкта слід використовувати:

  1. Мовний рівень. Кожна мова містить інструменти, які допоможуть вам у цьому. Наприклад, у Java є finalі primitives, у Swift є letі struct[About] . Мова визначає тип змінної. Наприклад, Java має primitiveта referenceвведіть, Swift має valueта referenceвведіть [About] . Для незмінних об'єктів зручнішим є primitivesі valueтип, який робить копію за замовчуванням. Що стосується referenceтипу, то це складніше (тому що ви можете змінити стан об'єкта з нього), але можливо. Наприклад, ви можете використовувати cloneшаблон на рівні розробника

  2. Рівень розробника. Як розробник, ви не повинні надавати інтерфейс для зміни стану

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