Что такое неизменность и почему я должен беспокоиться об этом? - PullRequest
56 голосов
/ 08 марта 2009

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

Недавно я создал тему, в которой упоминается неизменность, но поскольку это сама по себе тема, сейчас я создаю отдельную тему.

Я упоминал в прошлой ветке, что думал, что неизменность - это процесс создания объекта только для чтения и придания ему низкой видимости. Другой участник сказал, что это не имеет к этому никакого отношения. Эта страница (часть серии ) использует пример неизменяемого класса / структуры и использует только для чтения и другие концепции для его блокировки.

Что такое определение состояния в случае этого примера? Государство - это концепция, которую я не совсем понял.

С точки зрения руководства по проектированию неизменным должен быть класс, который не принимает вводимые пользователем данные и действительно будет просто возвращать значения?

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

Ответы [ 15 ]

57 голосов
/ 08 марта 2009

Что такое неизменность?

  • Неизменяемость применяется в основном к объектам (строки, массивы, пользовательский класс Animal)
  • Как правило, если существует неизменяемая версия класса, также доступна изменяемая версия. Например, Objective-C и Cocoa определяют как класс NSString (неизменяемый), так и класс NSMutableString.
  • Если объект неизменный, его нельзя изменить после создания (в основном только для чтения). Вы можете думать об этом как "только конструктор может изменить объект".

Это не имеет прямого отношения к пользовательскому вводу; даже ваш код не может изменить значение неизменяемого объекта. Однако вы всегда можете создать новый неизменный объект, чтобы заменить его. Вот пример псевдокода; обратите внимание, что во многих языках вы можете просто сделать myString = "hello"; вместо использования конструктора, как я делал ниже, но я включил его для ясности:

String myString = new ImmutableString("hello");
myString.appendString(" world"); // Can't do this
myString.setValue("hello world"); // Can't do this
myString = new ImmutableString("hello world"); // OK

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

Преимущества Immutabilty

  • Если вы передаете объект другой функции / методу, вам не нужно беспокоиться о том, будет ли этот объект иметь то же значение после возврата из функции. Например:

    String myString = "HeLLo WoRLd";
    String lowercasedString = lowercase(myString);
    print myString + " was converted to " + lowercasedString;
    

    Что, если реализация lowercase() изменила myString при создании строчной версии? Третья строка не даст вам желаемого результата. Конечно, хорошая lowercase() функция не сможет этого сделать, но вы гарантированно подтвердите этот факт, если myString неизменен. Таким образом, неизменяемые объекты могут помочь в применении хороших методов объектно-ориентированного программирования.

  • Проще сделать потокобезопасным неизменяемый объект

  • Это потенциально упрощает реализацию класса (хорошо, если вы пишете класс)

Государство

Если бы вы взяли все переменные экземпляра объекта и записали их значения на бумаге, это было бы состоянием этого объекта в данный момент. Состояние программы - это состояние всех ее объектов в данный момент. Состояние быстро меняется со временем; программа должна изменить состояние, чтобы продолжить работу.

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

20 голосов
/ 08 марта 2009

Неизменность

Проще говоря, память неизменна, если она не изменена после инициализации.

Программы, написанные на императивных языках, таких как C, Java и C #, могут манипулировать данными в памяти по желанию. Область физической памяти, однажды выделенная, может быть изменена целиком или частично потоком выполнения в любое время во время выполнения программы. Фактически, императивные языки поощряют такой способ программирования.

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

Когда существует только один поток выполнения, вы можете себе представить, что этот единственный поток «владеет» всеми данными в памяти и поэтому может манипулировать ими по своему желанию. Тем не менее, не существует неявной концепции владения, когда задействованы несколько выполняющихся потоков.

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

Использование неизменяемых структур данных устраняет необходимость введения сложной блокировки в коде. Если гарантируется, что часть памяти не изменится в течение срока действия программы, несколько читателей могут одновременно получить доступ к памяти. Они не могут наблюдать эти конкретные данные в несогласованном состоянии.

Многие функциональные языки программирования, такие как Lisp, Haskell, Erlang, F # и Clojure, поддерживают неизменяемые структуры данных по своей природе. Именно по этой причине они испытывают всплеск интереса, поскольку мы движемся к все более сложной разработке многопоточных приложений и многокомпьютерных компьютерных архитектур.

Государство

Состояние приложения можно просто представить как содержимое всей памяти и регистров ЦП в данный момент времени.

Логически состояние программы можно разделить на две части:

  1. Состояние кучи
  2. Состояние стека каждого исполняющего потока

В управляемых средах, таких как C # и Java, один поток не может получить доступ к памяти другого. Следовательно, каждый поток «владеет» состоянием своего стека. Стек можно рассматривать как содержащий локальные переменные и параметры типа значения (struct), а также ссылки на объекты. Эти значения изолированы от внешних потоков.

Однако данные в куче доступны всем потокам, поэтому необходимо следить за одновременным доступом. Все экземпляры объекта ссылочного типа (class) хранятся в куче.

В ООП состояние экземпляра класса определяется его полями. Эти поля хранятся в куче и поэтому доступны из всех потоков. Если класс определяет методы, позволяющие изменять поля после завершения работы конструктора, тогда этот класс является изменяемым (не неизменяемым). Если поля не могут быть изменены каким-либо образом, то тип является неизменным. Важно отметить, что класс со всеми полями C # readonly / Java final не обязательно является неизменным. Эти конструкции гарантируют, что ссылка не может измениться, но не объект ссылки. Например, поле может иметь неизменную ссылку на список объектов, но фактическое содержимое списка может быть изменено в любое время.

Определяя тип как действительно неизменяемый, его состояние можно считать замороженным, и поэтому тип безопасен для доступа несколькими потоками.

На практике может быть неудобно определять все ваши типы как неизменяемые. Для изменения значения в неизменяемом типе может потребоваться значительное копирование памяти. Некоторые языки делают этот процесс проще, чем другие, но в любом случае процессор будет выполнять дополнительную работу. Многие факторы влияют на то, перевешивает ли время, затрачиваемое на копирование памяти, влияние блокировок.

Много исследований было уделено разработке неизменных структур данных, таких как списки и деревья. При использовании таких структур, скажем, списка, операция «add» вернет ссылку на новый список с добавленным новым элементом. Ссылки на предыдущий список не видят никаких изменений и по-прежнему имеют согласованное представление данных.

8 голосов
/ 08 марта 2009

Проще говоря: после того, как вы создадите неизменный объект, вы не сможете изменить содержимое этого объекта. Примерами неизменяемых объектов .Net являются String и Uri.

Когда вы изменяете строку, вы просто получаете новую строку. Исходная строка не изменится. У Uri есть только свойства только для чтения и нет доступных методов для изменения содержимого Uri.

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

Надеюсь, это поможет.

6 голосов
/ 08 марта 2009

Вещи, которые неизменны, никогда не меняются. Изменчивые вещи могут измениться. Мутабельные вещи мутируют. Неизменные вещи, кажется, изменяются, но фактически создают новую изменчивую вещь.

Например, вот карта в Clojure

(def imap {1 "1" 2 "2"})
(conj imap [3 "3"])
(println imap)

Первая строка создает новую неизменяемую карту Clojure. Вторая строка соединяет 3 и «3» на карте. Это может выглядеть так, как будто она модифицирует старую карту, но в действительности она возвращает новую карту с добавленными 3 "3" Это яркий пример неизменности. Если бы это была изменяемая карта, она просто добавила бы 3 "3" напрямую к той же старой карте. Третья строка печатает карту

{3 "3", 1 "1", 2 "2"}

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

3 голосов
/ 08 марта 2009

Хороший вопрос.

Многопоточность. Если все типы являются неизменяемыми, условия гонки не существуют, и вы можете добавить столько потоков в код, сколько пожелаете.

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

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

Кроме того, неизменность уменьшает количество возможностей в программе, что уменьшает потенциальную сложность и вероятность ошибок.

2 голосов
/ 08 марта 2009

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

2 голосов
/ 08 марта 2009

Неизменяемый объект - это то, что вы можете смело предположить, что оно не изменится; у него есть важное свойство: каждый, кто его использует, может предположить, что видит одно и то же значение.

Неизменность обычно также означает, что вы можете думать об объекте как о «ценности», и что нет эффективной разницы между идентичными копиями объекта и самого объекта.

1 голос
/ 24 декабря 2016

Почему неизменяемость?

  1. Они менее подвержены ошибкам и более безопасны.

  2. Неизменяемые классы легче проектировать, реализовывать и использовать, чем изменяемые классы.

  3. Неизменяемые объекты являются поточно-ориентированными, поэтому проблем с синхронизацией нет.

  4. Неизменяемые объекты - это хорошие ключи карты и элементы Set, поскольку они обычно не изменяются после создания.

  5. Неизменяемость облегчает написание, использование и анализ кода (инвариант класса устанавливается один раз, а затем остается неизменным).

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

  7. Внутреннее состояние программы будет согласованным, даже если у вас есть исключения.

  8. Ссылки на неизменяемые объекты можно кэшировать, поскольку они не собираются изменяться (т. Е. В хешировании это обеспечивает быстрые операции).

Смотрите мой блог для более подробного ответа:
http://javaexplorer03.blogspot.in/2015/07/minimize-mutability.html

1 голос
/ 08 марта 2009

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

Обратите внимание, что при этом мы теперь используем дополнительную память. Некоторые для старого значения, некоторые для нового значения. Также обратите внимание, что некоторые люди запутываются, потому что они смотрят на код, например:

string mystring = "inital value";
mystring = "new value";
System.Console.WriteLine(mystring); // Outputs "new value";

и подумайте про себя: «но я изменяю это, посмотрите прямо здесь, в черно-белом виде! Mystring выводит« новое значение »...... Я думал, вы сказали, что я не могу это изменить? !!"

Но на самом деле под капотом происходит выделение новой памяти, то есть mystring теперь указывает на другой адрес и пространство памяти. В этом смысле «неизменный» относится не к значению mystring, а к памяти, используемой переменной mystring для хранения ее значения.

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

Одно из мест, которое действительно разрушает re: использование памяти в сильно итеративных циклах, особенно со строками, как в посте Эша. Скажем, вы строили HTML-страницу в итеративном цикле, где вы постоянно добавляли следующий HTML-блок к последнему, и просто ради удовольствия вы делали это на большом сервере. Такое постоянное распределение «памяти новых значений» может быстро стать дорогостоящим и, в конечном итоге, фатальным, если «память старых значений» не будет очищена должным образом.

Другая проблема заключается в том, что некоторые люди предполагают, что такие вещи, как сборка мусора (GC), происходят немедленно. Но это не так. Существуют различные оптимизации, так что сборка мусора должна выполняться в течение более простаивающих периодов. Таким образом, может иметь место значительная задержка между тем, когда память помечается как удаленная, и когда она фактически освобождается сборщиком мусора .... так что вы можете испытать большие скачки использования памяти, если просто перенесете проблему на GC.

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

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

Как и в публикации Эша, в .Net и со строками, рекомендуется использовать изменяемый класс StringBuilder, а не неизменяемые строковые классы, когда речь идет о необходимости постоянного изменения значения строки.

Другие языки / типы также будут иметь свои обходные пути.

1 голос
/ 08 марта 2009

"... зачем мне об этом беспокоиться?"

Практическим примером является повторяющаяся конкатенация строк. Например, в .NET:

string SlowStringAppend(string [] files)
{
    // Declare an string
    string result="";

    for (int i=0;i<files.length;i++)
    {
        // result is a completely new string equal to itself plus the content of the new
        // file
        result = result + File.ReadAllText(files[i]);
    }

    return result;
}    

string EfficientStringAppend(string [] files)
{
    // Stringbuilder manages a internal data buffer that will only be expanded when absolutely necessary
    StringBuilder result=new SringBuilder();

    for (int i=0;i<files.length;i++)
    {
        // The pre-allocated buffer (result) is appended to with the new string 
        // and only expands when necessary.  It doubles in size each expansion
        // so need for allocations become less common as it grows in size. 
        result.Append(File.ReadAllText(files[i]));
    }

    return result.ToString();
}

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

...