Изменчивые против неизменных объектов

173

Я пытаюсь разобраться с изменчивыми и неизменными объектами. Использование изменяемых объектов вызывает много проблем (например, возвращает массив строк из метода), но у меня возникают проблемы с пониманием того, как это негативно влияет. Каковы лучшие практики использования изменяемых объектов? Вы должны избегать их, когда это возможно?

Алекс Ангас
источник
stringявляется неизменным, по крайней мере, в .NET, и я думаю, во многих других современных языках.
Доменик
4
это зависит от того, что String на самом деле есть в языке - erlang 'strings' - это просто массив целых чисел, а haskell 'strings' - это массивы символов.
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
Отличный ответ. Однако один маленький вопрос: разве в С ++ нет хорошей поддержки неизменности? Разве функция const -корректности не достаточна?
Дмитрий С.
1
@DimitriC .: C ++ на самом деле имеет некоторые более фундаментальные особенности, в частности, лучшее различие между местами хранения, которые должны инкапсулировать неразделенное состояние, и теми, которые инкапсулируют идентичность объекта.
суперкат
2
В случае программирования на Java Джошуа Блох привел хорошее объяснение на эту тему в своей знаменитой книге « Эффективная Java» (пункт 15)
dMathieuD
27

Неизменяемые предметы против неизменных коллекций

Одним из лучших моментов в дискуссии о изменчивых и неизменных объектах является возможность распространения концепции неизменности на коллекции. Неизменяемый объект - это объект, который часто представляет единую логическую структуру данных (например, неизменяемая строка). Когда у вас есть ссылка на неизменный объект, содержимое объекта не изменится.

Неизменная коллекция - это коллекция, которая никогда не меняется.

Когда я выполняю операцию с изменяемой коллекцией, я изменяю коллекцию на месте, и все сущности, которые имеют ссылки на коллекцию, увидят это изменение.

Когда я выполняю операцию с неизменяемой коллекцией, в новую коллекцию возвращается ссылка, отражающая это изменение. Все объекты, которые имеют ссылки на предыдущие версии коллекции, не увидят изменения.

Умные реализации не обязательно должны копировать (клонировать) всю коллекцию, чтобы обеспечить эту неизменность. Простейшим примером является стек, реализованный в виде односвязного списка, и операции push / pop. Вы можете повторно использовать все узлы из предыдущей коллекции в новой коллекции, добавив только один узел для push и не клонировав ни одного узла для pop. Операция push_tail в односвязном списке, с другой стороны, не так проста и эффективна.

Неизменяемые и изменчивые переменные / ссылки

Некоторые функциональные языки применяют концепцию неизменности к самим ссылкам на объекты, допуская только одно присвоение ссылок.

  • В Erlang это верно для всех «переменных». Я могу назначить объекты только один раз. Если бы я работал с коллекцией, я бы не смог переназначить новую коллекцию на старую ссылку (имя переменной).
  • Scala также встраивает это в язык, при этом все ссылки объявляются с помощью var или val , причем vals является только одним присваиванием и продвигает функциональный стиль, но 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 . Это объясняет, почему неизменяемые объекты лучше, чем изменяемые. Коротко:

  • неизменяемые объекты проще создавать, тестировать и использовать
  • действительно неизменяемые объекты всегда поточно-ориентированы
  • они помогают избежать временной связи
  • их использование не имеет побочных эффектов (без защитных копий)
  • проблема изменчивости идентичности избегается
  • у них всегда есть отказ атомности
  • их гораздо проще кешировать
yegor256
источник
10

Неизменяемые объекты - очень мощная концепция. Они снимают с себя много бремени, пытаясь поддерживать согласованность объектов / переменных для всех клиентов.

Вы можете использовать их для низкоуровневых, неполиморфных объектов, таких как класс CPoint, которые в основном используются с семантикой значений.

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

Самое большое преимущество: неизменность + семантика объекта + умные указатели делают владение объектом несущественной, все клиенты объекта по умолчанию имеют свою собственную личную копию. Неявно это также означает детерминированное поведение при наличии параллелизма.

Недостаток: при использовании с объектами, содержащими много данных, потребление памяти может стать проблемой. Решением этой проблемы может быть сохранение операций над объектом символическими и выполнение ленивых вычислений. Однако это может привести к цепочке символьных вычислений, что может отрицательно повлиять на производительность, если интерфейс не предназначен для размещения символьных операций. В этом случае нужно определенно избегать возврата огромных кусков памяти из метода. В сочетании с цепными символьными операциями это может привести к значительному потреблению памяти и снижению производительности.

Таким образом, неизменяемые объекты, безусловно, являются моим основным способом мышления об объектно-ориентированном дизайне, но они не являются догмой. Они решают много проблем для клиентов объектов, но и создают много, особенно для разработчиков.

QBziZ
источник
Я думаю, что я неправильно понял Раздел 4 для наибольшего преимущества: неизменность + семантика объекта + умные указатели делают владение объектом «спорным» пунктом. Так что это спорно? Я думаю , что вы используете «спорный» неправильно ... Видя , как в следующем предложении является объектом подразумевал «детерминированное поведение» от его «тоо» (спорно) поведение.
Бенджамин
Вы правы, я неправильно использовал 'moot'. Считайте, что это изменилось :)
QBziZ
6

Вы должны указать, на каком языке вы говорите. Для языков низкого уровня, таких как C или C ++, я предпочитаю использовать изменяемые объекты для экономии места и уменьшения оттока памяти. В языках более высокого уровня неизменяемые объекты облегчают анализ поведения кода (особенно многопоточного кода), потому что нет «жуткого действия на расстоянии».

Джон Милликин
источник
Ты предполагаешь, что нити квантово запутаны? Это довольно натянуто :) На самом деле, если подумать, темы очень близки к тому, чтобы быть запутанными. Изменение, которое делает один поток, влияет на другой. +1
ndrewxie
4

Изменяемый объект - это просто объект, который можно изменить после того, как он создан / создан, в отличие от неизменяемого объекта, который нельзя изменить (см. Страницу Википедии по теме). Примером этого в языке программирования являются списки и кортежи Pythons. Списки могут быть изменены (например, новые элементы могут быть добавлены после его создания), тогда как кортежи не могут.

Я не думаю, что есть четкий ответ, какой из них лучше для всех ситуаций. У них обоих есть свои места.

willurd
источник
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) обычно имеет только одно значение. Большая часть «силы» неизменных объектов проистекает из этого факта.

Supercat
источник
1

Изменчивые экземпляры передаются по ссылке.

Неизменные экземпляры передаются по значению.

Абстрактный пример. Предположим, что на моем жестком диске существует файл с именем txtfile . Теперь, когда вы спрашиваете у меня txtfile , я могу вернуть его в двух режимах:

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

В первом режиме возвращаемый txtfile является изменяемым файлом, потому что когда вы вносите изменения в файл ярлыка, вы вносите изменения и в исходный файл. Преимущество этого режима заключается в том, что для каждого возвращаемого ярлыка требуется меньше памяти (в оперативной памяти или на жестком диске), а недостатком является то, что каждый (не только я, владелец) имеет права на изменение содержимого файла.

Во втором режиме возвращаемый txtfile является неизменяемым файлом, поскольку все изменения в полученном файле не относятся к исходному файлу. Преимущество этого режима заключается в том, что только я (владелец) может изменять исходный файл, а недостатком является то, что для каждой возвращаемой копии требуется память (в ОЗУ или на жестком диске).

Teodor
источник
Это не совсем верно. Неизменные случаи действительно могут быть переданы по ссылке, и часто это так. Копирование в основном выполняется, когда вам нужно обновить объект или структуру данных.
Хамон Холмгрен
0

Если вы возвращаете ссылки на массив или строку, то внешний мир может изменить содержимое этого объекта и, следовательно, сделать его изменяемым (модифицируемым) объектом.

user1923551
источник
0

Неизменяемые означает, что нельзя изменить, а изменяемые означает, что вы можете изменить.

Объекты отличаются от примитивов в Java. Примитивы встроены в типы (логические, int и т. Д.), А объекты (классы) - это созданные пользователем типы.

Примитивы и объекты могут быть изменяемыми или неизменными, если они определены как переменные-члены в реализации класса.

Многие люди думают, что примитивы и переменные объекта, имеющие перед ними конечный модификатор, неизменны, однако это не совсем так. Так что final почти не означает неизменяемость для переменных. Смотрите пример здесь
http://www.siteconsortium.com/h/D0000F.php .

JTHouseCat
источник
0

Immutable object- это состояние объекта, которое нельзя изменить после создания. Объект неизменен, когда все его поля неизменны

Поток безопасно

Основным преимуществом неизменяемого объекта является то, что он является естественным для параллельной среды. Самая большая проблема в параллелизме - это то, shared resourceчто может быть изменено любым потоком. Но если объект является неизменным, то read-onlyэто работа с потоками. Любая модификация исходного неизменяемого объекта возвращает копию

побочные эффекты бесплатно

Как разработчик, вы абсолютно уверены, что состояние неизменяемого объекта не может быть изменено из любого места (намеренно или нет)

оптимизация компиляции

Повысить производительность

Недостаток:

Копирование объекта - более сложная операция, чем изменение изменяемого объекта, поэтому он имеет некоторое влияние на производительность

Для создания immutableобъекта вы должны использовать:

  1. Уровень языка. Каждый язык содержит инструменты, которые помогут вам в этом. Например, Java имеет finalи primitivesSwift имеет letи struct[О] . Язык определяет тип переменной. Например, Java имеет primitiveи referenceтип, Swift имеет valueи referenceтип [О] . Для неизменяемых объектов более удобным является primitivesи valueтип, который делает копию по умолчанию. Что касается referenceтипа, это более сложно (потому что вы можете изменить состояние объекта из него), но возможно. Например, вы можете использовать cloneшаблон на уровне разработчика

  2. Уровень разработчика. Как разработчик, вы не должны предоставлять интерфейс для изменения состояния

yoAlex5
источник