Изменчивые против неизменных объектов
Я пытаюсь разобраться с изменчивыми и неизменными объектами. Использование изменяемых объектов вызывает много проблем (например, возвращает массив строк из метода), но у меня возникают проблемы с пониманием того, как это негативно влияет. Каковы лучшие практики использования изменяемых объектов? Вы должны избегать их, когда это возможно?
10 ответов
Ну, есть несколько аспектов этого. Во-первых, изменяемые объекты без ссылочной идентичности могут вызывать ошибки в нечетное время. Например, рассмотрим Person
боб с ценностью на основе equals
метод:
Map<Person, String> map = ...
Person p = new Person();
map.put(p, "Hey, there!");
p.setName("Daniel");
map.get(p); // => null
Person
Экземпляр "теряется" на карте при использовании в качестве ключа, потому что это hashCode
и равенство основывалось на изменчивых ценностях. Эти значения изменились за пределами карты, и все хеширование устарело. Теоретики любят говорить об этом, но на практике я не нашел, чтобы это было слишком большой проблемой.
Другим аспектом является логическая "разумность" вашего кода. Это сложный термин для определения, охватывающий все, от читаемости до потока. В общем, вы должны быть в состоянии посмотреть на кусок кода и легко понять, что он делает. Но что более важно, вы должны быть в состоянии убедить себя, что он делает то, что делает правильно. Когда объекты могут независимо изменяться в разных "доменах" кода, иногда становится трудно отследить, что и где ("пугающее действие на расстоянии"). Это более сложная концепция, но она часто встречается в больших и более сложных архитектурах.
Наконец, изменяемые объекты являются убийцами в параллельных ситуациях. Всякий раз, когда вы обращаетесь к изменчивому объекту из отдельных потоков, вам приходится иметь дело с блокировкой. Это снижает пропускную способность и значительно усложняет поддержку вашего кода. Достаточно сложная система делает эту проблему настолько непропорциональной, что ее практически невозможно поддерживать (даже для экспертов по параллелизму).
Неизменяемые объекты (в частности, неизменяемые коллекции) позволяют избежать всех этих проблем. Как только вы разберетесь с тем, как они работают, ваш код превратится в нечто, что будет легче читать, легче поддерживать и с меньшей вероятностью приведет к сбою странным и непредсказуемым образом. Неизменяемые объекты еще проще тестировать не только из-за их легкой насмешки, но и из-за шаблонов кода, которые они стремятся применять. Короче, они хорошая практика повсюду!
С учетом сказанного, я вряд ли фанат в этом вопросе. Некоторые проблемы просто не моделируются, когда все неизменно. Но я думаю, что вы должны попытаться подтолкнуть как можно больше своего кода в этом направлении, при условии, конечно, что вы используете язык, который делает это разумным мнением (C/C++ делает это очень трудным, как и Java), Вкратце: преимущества в некоторой степени зависят от вашей проблемы, но я бы предпочел неизменность.
Неизменные предметы против неизменных коллекций
Один из лучших моментов в дискуссии о изменчивых и неизменных объектах - это возможность распространения концепции неизменности на коллекции. Неизменяемый объект - это объект, который часто представляет единую логическую структуру данных (например, неизменяемая строка). Если у вас есть ссылка на неизменный объект, содержимое объекта не изменится.
Неизменная коллекция - это коллекция, которая никогда не меняется.
Когда я выполняю операцию с изменяемой коллекцией, я изменяю коллекцию на месте, и все сущности, которые имеют ссылки на коллекцию, увидят это изменение.
Когда я выполняю операцию с неизменяемой коллекцией, в новую коллекцию возвращается ссылка, отражающая это изменение. Все объекты, которые имеют ссылки на предыдущие версии коллекции, не увидят изменения.
Умные реализации не обязательно должны копировать (клонировать) всю коллекцию, чтобы обеспечить эту неизменность. Простейшим примером является стек, реализованный в виде односвязного списка, и операции push / pop. Вы можете повторно использовать все узлы из предыдущей коллекции в новой коллекции, добавив только один узел для push и не клонировав ни одного узла для pop. Операция push_tail в односвязном списке, с другой стороны, не так проста и эффективна.
Неизменяемые и изменчивые переменные / ссылки
Некоторые функциональные языки используют концепцию неизменности для самих ссылок на объекты, допуская только одно присвоение ссылок.
- В Эрланге это верно для всех "переменных". Я могу назначить объекты только один раз. Если бы я работал с коллекцией, я бы не смог переназначить новую коллекцию на старую ссылку (имя переменной).
- Scala также встраивает это в язык, при этом все ссылки объявляются с помощью var или val, причем vals является только одним присваиванием и продвигает функциональный стиль, но vars допускает более с-подобную или java-подобную структуру программы.
- Требуется объявление var / val, в то время как многие традиционные языки используют необязательные модификаторы, такие как final в java и const в c.
Простота разработки и производительность
Почти всегда причина использования неизменяемого объекта заключается в содействии программированию без побочных эффектов и простым рассуждениям о коде (особенно в среде с высокой степенью параллелизма / параллельности). Вам не нужно беспокоиться об изменении базовых данных другим объектом, если объект неизменен.
Основным недостатком является производительность. Вот описание простого теста, который я провел на Java, сравнивая некоторые неизменяемые и изменяемые объекты в игрушечной задаче.
Проблемы с производительностью спорны во многих приложениях, но не во всех, поэтому многие большие числовые пакеты, такие как класс Numpy Array в Python, допускают обновления больших массивов на месте. Это было бы важно для областей применения, которые используют большие матричные и векторные операции. Эти большие проблемы с параллельной передачей данных и вычислительной нагрузкой достигают большого ускорения при работе на месте.
Неизменяемые объекты - очень мощная концепция. Они снимают большую часть бремени с того, чтобы поддерживать согласованность объектов / переменных для всех клиентов.
Вы можете использовать их для низкоуровневых, неполиморфных объектов, таких как класс CPoint, которые в основном используются с семантикой значений.
Или вы можете использовать их для полиморфных интерфейсов высокого уровня - как IFunction, представляющая математическую функцию - которая используется исключительно с семантикой объекта.
Самое большое преимущество: неизменность + семантика объекта + умные указатели делают владение объектом несущественной, все клиенты объекта по умолчанию имеют свою личную копию. Неявно это также означает детерминированное поведение при наличии параллелизма.
Недостаток: при использовании с объектами, содержащими много данных, потребление памяти может стать проблемой. Решением этой проблемы может быть сохранение операций над объектом символическими и выполнение ленивых вычислений. Однако это может привести к цепочке символьных вычислений, которые могут отрицательно повлиять на производительность, если интерфейс не предназначен для размещения символьных операций. В этом случае нужно определенно избегать возврата огромных кусков памяти из метода. В сочетании с цепочечными символическими операциями это может привести к значительному потреблению памяти и снижению производительности.
Таким образом, неизменяемые объекты, безусловно, являются моим основным способом мышления об объектно-ориентированном дизайне, но они не являются догмой. Они решают много проблем для клиентов объектов, но и создают много, особенно для разработчиков.
Проверьте это сообщение в блоге: http://www.yegor256.com/2014/06/09/objects-should-be-immutable.html. Это объясняет, почему неизменяемые объекты лучше, чем изменяемые. Короче:
- неизменяемые объекты проще создавать, тестировать и использовать
- действительно неизменяемые объекты всегда поточно-ориентированы
- они помогают избежать временной связи
- их использование не имеет побочных эффектов (без защитных копий)
- проблема изменчивости идентичности избегается
- у них всегда есть отказ атомности
- их гораздо проще кешировать
Вы должны указать, на каком языке вы говорите. Для языков низкого уровня, таких как C или C++, я предпочитаю использовать изменяемые объекты для экономии места и уменьшения оттока памяти. В языках более высокого уровня неизменяемые объекты облегчают анализ поведения кода (особенно многопоточного кода), потому что нет "жуткого действия на расстоянии".
Изменяемый объект - это просто объект, который можно изменить после того, как он создан / создан, в отличие от неизменяемого объекта, который нельзя изменить (см. Страницу Википедии по теме). Примером этого в языке программирования являются списки и кортежи Pythons. Списки могут быть изменены (например, новые элементы могут быть добавлены после его создания), тогда как кортежи не могут.
Я не думаю, что есть четкий ответ, какой из них лучше для всех ситуаций. У них обоих есть свои места.
Изменчивые экземпляры передаются по ссылке.
Неизменные экземпляры передаются по значению.
Абстрактный пример. Предположим, что на моем жестком диске существует файл с именем txtfile. Теперь, когда вы спрашиваете у меня txtfile, я могу вернуть его в двух режимах:
- Создать ярлык для txtfile и pas ярлык для вас, или
- Возьмите копию для txtfile и сделайте копию для вас.
В первом режиме возвращаемый txtfile является изменяемым файлом, потому что когда вы вносите изменения в файл ярлыка, вы вносите изменения и в исходный файл. Преимущество этого режима заключается в том, что для каждого возвращаемого ярлыка требуется меньше памяти (в оперативной памяти или на жестком диске), а недостатком является то, что каждый (не только я, владелец) имеет права на изменение содержимого файла.
Во втором режиме возвращаемый txtfile является неизменяемым файлом, поскольку все изменения в полученном файле не относятся к исходному файлу. Преимущество этого режима заключается в том, что только я (владелец) может изменять исходный файл, а недостатком является то, что для каждой возвращаемой копии требуется память (в ОЗУ или на жестком диске).
Изменяемые коллекции, как правило, быстрее, чем их неизменяемые аналоги при использовании для операций на месте.
Однако за изменчивость приходится платить: вам нужно гораздо более осторожно разделять их между различными частями вашей программы.
Легко создавать ошибки, когда общая изменяемая коллекция неожиданно обновляется, что заставляет вас искать, какая строка в большой кодовой базе выполняет нежелательное обновление.
Обычный подход - использовать изменяемые коллекции локально внутри функции или закрытые для класса, где есть узкое место в производительности, но использовать неизменяемые коллекции в другом месте, где скорость менее важна.
Это дает вам высокую производительность изменяемых коллекций там, где это наиболее важно, не жертвуя при этом безопасностью, которую неизменяемые коллекции дают вам на протяжении большей части логики вашего приложения.
Если тип класса изменчив, переменная этого типа может иметь несколько различных значений. Например, предположим, что объект 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
) обычно имеет только одно значение. Большая часть "силы" неизменных объектов проистекает из этого факта.
Неизменяемые означает, что нельзя изменить, а изменяемые означает, что вы можете изменить.
Объекты отличаются от примитивов в Java. Примитивы встроены в типы (логические, int и т. Д.), А объекты (классы) - это созданные пользователем типы.
Примитивы и объекты могут быть изменяемыми или неизменными, если они определены как переменные-члены в реализации класса.
Многие люди думают, что примитивы и переменные объекта, имеющие перед ними финальный модификатор, неизменны, однако это не совсем так. Так что final почти не означает неизменяемость для переменных. Смотрите пример здесь
http://www.siteconsortium.com/h/D0000F.php.
Если вы возвращаете ссылки на массив или строку, то внешний мир может изменить содержимое этого объекта и, следовательно, сделать его изменяемым (модифицируемым) объектом.
Unmodifiable
- это изменяемая обертка. Это гарантирует, что его нельзя изменить напрямую (но, возможно, использует объект поддержки)
Immutable
- состояние которого нельзя изменить после создания. Объект неизменен, если все его поля неизменны. Это следующий шаг неизменяемого объекта
Потокобезопасный
Основное преимущество объекта Immutable заключается в том, что он естественен для параллельной среды. Самая большая проблема параллелизма - этоshared resource
который может быть изменен любым потоком. Но если объект неизменен, онread-only
что является потокобезопасной операцией. Любая модификация исходного неизменяемого объекта возвращает копию
источник правды, без побочных эффектов
Как разработчик вы полностью уверены, что неизменяемое состояние объекта нельзя изменить из любого места (намеренно или нет). Например, если потребитель использует неизменяемый объект, он может использовать исходный неизменяемый объект.
оптимизация компиляции
Улучшить производительность
Недостаток:
Копирование объекта - более тяжелая операция, чем изменение изменяемого объекта, поэтому оно имеет некоторую производительность.
Чтобы создать immutable
объект, который вы должны использовать:
1. Уровень языка
Каждый язык содержит инструменты, которые помогут вам в этом. Например:
- Java имеет
final
а такжеprimitives
- Свифт имеет
let
а такжеstruct
[О].
Язык определяет тип переменной. Например:
- Java имеет
primitive
а такжеreference
тип, - Свифт имеет
value
а такжеreference
введите [О программе].
За immutable
объект удобнее primitives
а также value
типа, который по умолчанию делает копию. Что касаетсяreference
типа это сложнее (потому что вы можете изменить состояние объекта вне его), но возможно. Например, вы можете использоватьclone
шаблон на уровне разработчика, чтобы сделать deep
(вместо того shallow
) копия.
2. Уровень разработчика
Как разработчику вы не должны предоставлять интерфейс для изменения состояния