В объектно-ориентированном (ОО) и функциональном программировании неизменяемый объект (неизменяемый [1] объект) — это объект , состояние которого не может быть изменено после его создания. [2] В этом отличие от изменяемого объекта (изменяемого объекта), который можно изменить после его создания. [3] В некоторых случаях объект считается неизменным, даже если изменяются некоторые внутренние атрибуты, но состояние объекта кажется неизменным с внешней точки зрения. Например, объект, который использует мемоизацию для кэширования результатов дорогостоящих вычислений, по-прежнему может считаться неизменяемым объектом.
Строки и другие конкретные объекты обычно выражаются как неизменяемые объекты для улучшения читаемости и эффективности выполнения объектно-ориентированного программирования. Неизменяемые объекты также полезны, поскольку они по своей сути потокобезопасны . [2] Другие преимущества заключаются в том, что их проще понять и обосновать, а также они обеспечивают более высокий уровень безопасности, чем изменяемые объекты. [2]
В императивном программировании значения, хранящиеся в переменных программы , содержимое которых никогда не меняется, называются константами , чтобы отличать их от переменных, которые могут быть изменены во время выполнения. Примеры включают коэффициенты перевода метров в футы или значение числа Пи с точностью до нескольких десятичных знаков.
Поля, доступные только для чтения, могут рассчитываться во время работы программы (в отличие от констант, которые известны заранее), но никогда не изменяются после их инициализации.
Иногда говорят о неизменяемости определенных полей объекта. Это означает, что невозможно изменить эти части состояния объекта, даже если другие части объекта могут быть изменяемыми ( слабо неизменяемыми ). Если все поля неизменяемы, то объект является неизменяемым. Если весь объект не может быть расширен другим классом, объект называется строго неизменяемым . [4] Это может, например, помочь явно обеспечить соблюдение определенных инвариантов в отношении определенных данных в объекте, которые останутся неизменными на протяжении всего срока службы объекта. В некоторых языках это делается с помощью ключевого слова (например, const
в C++ , final
в Java ), которое обозначает поле как неизменяемое. В некоторых языках все наоборот: в OCaml поля объекта или записи по умолчанию являются неизменяемыми и mutable
для этого должны быть явно помечены значком.
В большинстве объектно-ориентированных языков на объекты можно ссылаться с помощью ссылок . Некоторыми примерами таких языков являются Java , C++ , C# , VB.NET и многие языки сценариев , такие как Perl , Python и Ruby . В этом случае имеет значение, может ли состояние объекта меняться при совместном использовании объектов через ссылки.
Если известно, что объект является неизменяемым, предпочтительно создать ссылку на него, а не копировать весь объект. Это сделано для экономии памяти за счет предотвращения дублирования данных и избежания вызовов конструкторов и деструкторов; это также приводит к потенциальному увеличению скорости выполнения.
Технику копирования ссылок гораздо сложнее использовать для изменяемых объектов, поскольку если какой-либо пользователь ссылки на изменяемый объект меняет ее, все остальные пользователи этой ссылки увидят это изменение. Если это не ожидаемый эффект, может быть сложно уведомить других пользователей о том, чтобы они отреагировали правильно. В таких ситуациях защитное копирование всего объекта, а не ссылки, обычно является простым, но дорогостоящим решением. Шаблон наблюдателя — это альтернативный метод обработки изменений изменяемых объектов.
Метод копирования при записи (COW) сочетает в себе преимущества изменяемых и неизменяемых объектов и напрямую поддерживается практически всем современным оборудованием . Используя этот метод, когда пользователь просит систему скопировать объект, она вместо этого просто создает новую ссылку, которая по-прежнему указывает на тот же объект. Как только пользователь пытается изменить объект с помощью определенной ссылки, система создает реальную копию, применяет к ней изменения и устанавливает ссылку для ссылки на новую копию. На других пользователей это не влияет, поскольку они по-прежнему ссылаются на исходный объект. Таким образом, в COW все пользователи имеют изменяемую версию своих объектов, хотя в случае, если пользователи не изменяют свои объекты, преимущества неизменяемых объектов в экономии места и скорости сохраняются. Копирование при записи популярно в системах виртуальной памяти, поскольку позволяет им экономить пространство памяти, сохраняя при этом правильную обработку всего, что может делать прикладная программа.
Практика постоянного использования ссылок вместо копий одинаковых объектов называется интернированием . Если используется интернирование, два объекта считаются равными тогда и только тогда, когда их ссылки, обычно представленные в виде указателей или целых чисел, равны. Некоторые языки делают это автоматически: например, Python автоматически интернирует короткие строки . Если алгоритм, реализующий интернирование, гарантированно делает это во всех возможных случаях, то сравнение объектов на равенство сводится к сравнению их указателей — существенный выигрыш в скорости в большинстве приложений. (Даже если не гарантируется полнота алгоритма, все равно существует возможность улучшения быстрого пути , когда объекты равны и используют одну и ту же ссылку.) Интернирование обычно полезно только для неизменяемых объектов.
Неизменяемые объекты могут быть полезны в многопоточных приложениях. Несколько потоков могут работать с данными, представленными неизменяемыми объектами, не беспокоясь об изменении данных другими потоками. Поэтому неизменяемые объекты считаются более потокобезопасными , чем изменяемые объекты.
Неизменяемость не означает, что объект, хранящийся в памяти компьютера, не подлежит записи. Скорее, неизменяемость — это конструкция времени компиляции , которая указывает, что программист может сделать через обычный интерфейс объекта, а не обязательно то, что он может сделать абсолютно (например, обходя систему типов или нарушая корректность const в C или C++ ).
В Python , Java [5] :80 и .NET Framework строки являются неизменяемыми объектами. И в Java, и в .NET Framework есть изменяемые версии строк. В Java [5] : 84 это StringBuffer
и StringBuilder
(изменяемые версии Java String
), а в .NET это StringBuilder
(изменяемая версия .Net String
). В Python 3 есть вариант изменяемой строки (байтов) с именем bytearray
. [6]
Кроме того, все примитивные классы-оболочки в Java являются неизменяемыми.
Аналогичными шаблонами являются неизменяемый интерфейс и неизменяемая оболочка.
В чисто функциональных языках программирования невозможно создавать изменяемые объекты без расширения языка (например, с помощью библиотеки изменяемых ссылок или интерфейса внешних функций ), поэтому все объекты являются неизменяемыми.
В Ada любой объект объявляется либо переменным (т. е. изменяемым; обычно это неявное значение по умолчанию), либо constant
(т. е. неизменяемым) с помощью constant
ключевого слова.
введите Some_type — новое целое число ; -- может быть что-нибудь более сложное x : constant Some_type := 1 ; -- неизменяемый y : Some_type ; -- изменчивый
Параметры подпрограммы неизменяемы в режиме in и изменяемы в режимах in out и out .
процедура Do_it ( a : in Integer ; b : in out Integer ; c : out Integer ) is Begin -- a неизменяема b := b + a ; с := а ; закончить Do_it ;
В C# вы можете обеспечить неизменность полей класса с помощью этого readonly
оператора. [7] : 239
Сделав все поля неизменяемыми, вы получите неизменяемый тип.
класс AnImmutableType { public readonly double _value ; общественный AnImmutableType ( двойной х ) { _value = х ; } Общественный AnImmutableType Square () { вернуть новый AnImmutableType ( _value * _value ); } }
В C++ константно-корректная реализация Cart
позволит пользователю создавать экземпляры класса, а затем использовать их либо как const
(неизменяемые), либо как изменяемые, по желанию, предоставляя две разные версии метода items()
. (Обратите внимание, что в C++ нет необходимости (и фактически невозможно) предоставлять специализированный конструктор для const
экземпляров.)
class Cart { public : Cart ( std :: вектор < Item > items ) : items_ ( items ) {} std :: vector < Item >& items () { return items_ ; } const std :: vector < Item >& items () const { return items_ ; } int ComputeTotalCost () const { /* возвращаем сумму цен */ } частный : std :: vector <Item> items_ ; _ _ };
Обратите внимание: если существует элемент данных, который является указателем или ссылкой на другой объект, то изменить объект, на который указывает или на который ссылается ссылка, можно только внутри неконстантного метода.
C++ также обеспечивает абстрактную (в отличие от побитовой) неизменяемость посредством mutable
ключевого слова, которое позволяет изменять переменную-член изнутри const
метода.
class Cart { public : Cart ( std :: вектор < Item > items ) : items_ ( items ) {} const std :: вектор < Item >& items () const { return items_ ; } int ComputeTotalCost () const { if ( tal_cost_ ) { return * total_cost_ ; } int total_cost = 0 ; for ( const auto & item : items_ ) { tal_cost += item . Расходы (); } Total_cost_ = total_cost ; вернуть общую_стоимость ; } частный : std :: vector <Item> items_ ; _ _ изменяемый std :: необязательный <int> total_cost_ ; _ _ };
В D существуют два квалификатора типа и для переменных const
, immutable
которые нельзя изменить. [8] В отличие от C++ const
, Java final
и C# readonly
, они транзитивны и рекурсивно применяются ко всему, что достижимо через ссылки на такую переменную. Разница между const
и immutable
заключается в том, к чему они применяются: const
это свойство переменной: на законном основании могут существовать изменяемые ссылки на указанное значение, т. е. значение может фактически измениться. Напротив, immutable
является свойством указанного значения: значение и все, что транзитивно достижимо из него, не может измениться (без нарушения системы типов, что приводит к неопределенному поведению ). Любая ссылка на это значение должна быть помечена const
или immutable
. T
По сути , для любого неквалифицированного типа const(T)
это непересекающееся объединение T
(изменяемого) и immutable(T)
.
класс C { /*mutable*/ Object mField ; константный объект cField ; неизменяемый объект iField ; }
Для изменяемого C
объекта в него mField
можно записать. Объект не может быть изменен , он наследует const(C)
; по-прежнему неизменен, поскольку это более сильная гарантия. Для a все поля неизменяемы.mField
const
iField
immutable(C)
В такой функции:
void func ( C m , const C c , неизменяемый C i ) { /* внутри фигурных скобок */ }
Внутри фигурных скобок c
может ссылаться на тот же объект, что и m
, поэтому мутации на также m
могут косвенно меняться . c
Кроме того, c
может ссылаться на тот же объект, что и i
, но, поскольку значение тогда является неизменным, изменений не происходит. Однако m
и i
не может юридически относиться к одному и тому же объекту.
На языке гарантий mutable не имеет никаких гарантий (функция может изменить объект), const
является только внешней гарантией того, что функция ничего не изменит, и immutable
является двунаправленной гарантией (функция не изменит значение, и вызывающая сторона должна не менять).
Значения, которые инициализируются const
или immutable
должны быть инициализированы путем прямого присвоения в точке объявления или с помощью конструктора .
Поскольку const
параметры забывают, было ли значение изменяемым или нет, подобная конструкция inout
действует в некотором смысле как переменная для информации об изменяемости. Функция типа const(S) function(const(T))
возвращает const(S)
типизированные значения для изменяемых, константных и неизменяемых аргументов. Напротив, функция типа inout(S) function(inout(T))
возвращает значение S
для изменяемых T
аргументов, const(S)
значений const(T)
и immutable(S)
значений immutable(T)
.
Приведение неизменяемых значений к изменяемым приводит к неопределенному поведению при изменении, даже если исходное значение происходит из изменяемого источника. Приведение изменяемых значений к неизменяемым может быть законным, если после этого не останется изменяемых ссылок. «Выражение может быть преобразовано из изменяемого (...) в неизменяемое, если выражение уникально и все выражения, на которые оно транзитивно ссылается, либо уникальны, либо неизменяемы». [8] Если компилятор не может доказать уникальность, приведение может быть выполнено явно, и программист должен гарантировать отсутствие изменяемых ссылок.
Тип string
является псевдонимом для immutable(char)[]
, то есть типизированным фрагментом памяти, состоящим из неизменяемых символов. [9] Создание подстрок обходится дешево, поскольку просто копирует и изменяет указатель и поле длины, и безопасно, поскольку базовые данные не могут быть изменены. Объекты типа const(char)[]
могут ссылаться на строки, а также на изменяемые буферы.
Создание поверхностной копии константного или неизменяемого значения удаляет внешний слой неизменяемости: копирование неизменяемой строки ( immutable(char[])
) возвращает строку ( immutable(char)[]
). Неизменяемый указатель и длина копируются, а копии изменяемы. Указанные данные не были скопированы и сохраняют свой квалификатор, как в примере immutable
. Его можно удалить, создав глубокую копию, например, с помощью dup
функции.
Классическим примером неизменяемого объекта является экземпляр класса Java String
.
Строка s = "ABC" ; с . в нижний регистр (); // Это ничего не дает!
Метод toLowerCase()
не изменяет содержащиеся в нем данные «ABC» s
. Вместо этого создается экземпляр нового объекта String, которому во время создания присваиваются данные «abc». Ссылка на этот объект String возвращается методом toLowerCase()
. Чтобы строка s
содержала данные «abc», необходим другой подход:
с = с . в нижний регистр ();
Теперь String s
ссылается на новый объект String, содержащий «abc». В синтаксисе объявления класса String нет ничего , что делало бы его неизменяемым; скорее, ни один из методов класса String никогда не влияет на данные, содержащиеся в объекте String, что делает его неизменяемым.
Ключевое слово final
( подробная статья ) используется при реализации неизменяемых примитивных типов и ссылок на объекты, [10] но само по себе оно не может сделать сами объекты неизменяемыми. См. примеры ниже:
Переменные примитивного типа ( int
, long
, short
и т. д.) можно переназначать после определения. Этого можно избежать, используя final
.
интервал я = 42 ; //int — примитивный тип i = 43 ; // ХОРОШО окончательный int j = 42 ; j = 43 ; // не компилируется. j является окончательным, поэтому его нельзя переназначить
Ссылочные типы нельзя сделать неизменяемыми, просто используя final
ключевое слово. final
только предотвращает переназначение.
окончательный MyObject м = новый MyObject (); //m имеет ссылочный тип m . данные = 100 ; // ХОРОШО. Мы можем изменить состояние объекта m (m изменчив, и Final не меняет этого факта) m = new MyObject (); // не компилируется. m является окончательным, поэтому его нельзя переназначить
Примитивные оболочки ( Integer
, Long
, Short
, Double
, Float
, Character
, Byte
, Boolean
) также являются неизменяемыми. Неизменяемые классы можно реализовать, следуя нескольким простым рекомендациям. [11]
В JavaScript все примитивные типы (Undefine, Null, Boolean, Number, BigInt, String, Symbol) являются неизменяемыми, но пользовательские объекты обычно изменяемы.
function doSomething ( x ) { /* изменение x здесь меняет оригинал? */ }; вар стр = 'строка' ; вар объект = { ан : 'объект' }; сделать что-нибудь ( ул ); // строки, числа и типы bool неизменяемы, функция получает копию doSomething ( obj ); // объекты передаются по ссылке и изменяются внутри функции doAnotherThing ( str , obj ); // `str` не изменился, но `obj` может измениться.
Чтобы имитировать неизменность объекта, можно определить свойства как доступные только для чтения (доступные для записи: false).
вар объект = {}; Объект . defineProperty ( obj , 'foo' , { значение : 'bar' , доступно для записи : false }); объект . Фу = 'бар2' ; // молча игнорируется
Однако описанный выше подход по-прежнему позволяет добавлять новые свойства. В качестве альтернативы можно использовать Object.freeze, чтобы сделать существующие объекты неизменяемыми.
вар obj = { foo : 'bar' }; Объект . заморозить ( объект ); объект . Фу = 'бары' ; // не могу редактировать свойство, молча игнорирую obj . foo2 = 'бар2' ; // невозможно добавить свойство, молча игнорируется
Благодаря реализации ECMA262 JavaScript получил возможность создавать неизменяемые ссылки, которые нельзя переназначить. Однако использование const
объявления не означает, что значение ссылки только для чтения является неизменным, просто имя не может быть присвоено новому значению.
const ALWAYS_IMMUTABLE = правда ; попробуйте { ALWAYS_IMMUTABLE = ложь ; } поймать ( ошибка ) { console . log ( "Невозможно переназначить неизменяемую ссылку." ); } const arr = [ 1 , 2 , 3 ]; обр . нажать ( 4 ); консоль . журнал ( обр ); // [1, 2, 3, 4]
Использование неизменяемого состояния стало растущей тенденцией в JavaScript с момента появления React , который отдает предпочтение шаблонам управления состоянием, подобным Flux, таким как Redux . [12]
В Perl можно создать неизменяемый класс с помощью библиотеки Moo, просто объявив все атрибуты только для чтения:
пакет Неизменяемый ; используйте Му ; имеет значение => ( is => 'ro' , # только для чтения по умолчанию => 'data' , # можно переопределить, предоставив конструктору # значение: Immutable->new(value => 'something else'); ) ; 1 ;
Раньше создание неизменяемого класса требовало двух шагов: во-первых, создание средств доступа (автоматически или вручную), которые предотвращают изменение атрибутов объекта, и, во-вторых, предотвращение прямого изменения данных экземпляра экземпляров этого класса (обычно это хранилось в хэше). ссылку и может быть заблокирован с помощью функции lock_hash Hash::Util):
пакет Неизменяемый ; используйте строгий ; использовать предупреждения ; используйте базу qw(Class::Accessor) ; # создаем средства доступа только для чтения __PACKAGE__ -> mk_ro_accessors ( qw(value) ); используйте Hash::Util 'lock_hash' ; суб новый { мой $класс = сдвиг ; вернуть $class , если ref ( $class ); die "Аргументами new должны быть пары ключ => значение\n" if ( @_ % 2 == 0 ); мои %defaults = ( значение => 'данные' , ); мой $obj = { %defaults , @_ , }; благослови $obj , $class ; # предотвращаем изменение данных объекта lock_hash %$obj ; } 1 ;
Или с помощью написанного вручную аксессора:
пакет Неизменяемый ; используйте строгий ; использовать предупреждения ; используйте Hash::Util 'lock_hash' ; суб новый { мой $класс = сдвиг ; вернуть $class , если ref ( $class ); die "Аргументами new должны быть пары ключ => значение\n" if ( @_ % 2 == 0 ); мои %defaults = ( значение => 'данные' , ); мой $obj = { %defaults , @_ , }; благослови $obj , $class ; # предотвращаем изменение данных объекта lock_hash %$obj ; } # подзначение средства доступа только для чтения { my $self = сдвиг ; if ( my $new_value = shift ) { # попытка установить новое значение die "Этот объект не может быть изменен\n" ; } else { return $self -> { значение } } } 1 ;
В Python некоторые встроенные типы (числа, логические значения, строки, кортежи, замороженные наборы) являются неизменяемыми, но пользовательские классы, как правило, изменяемы. Чтобы имитировать неизменяемость в классе, можно переопределить установку и удаление атрибута, чтобы вызвать исключения:
class ImmutablePoint : """Неизменяемый класс с двумя атрибутами 'x' и 'y'.""" __slots__ = [ 'x' , 'y' ] def __setattr__ ( self , * args ): поднять TypeError ( «Невозможно изменить неизменяемый экземпляр». ) __delattr__ = __setattr__ def __init__ ( self , x , y ): # Мы больше не можем использовать self.value = value для хранения данных экземпляра # поэтому мы должны явно вызвать суперкласс super () . __setattr__ ( 'x' , x ) супер () . __setattr__ ( 'y' , y )
Помощники стандартной библиотеки Collections.namedtuple и typing.NamedTuple, доступные начиная с Python 3.6, создают простые неизменяемые классы. Следующий пример примерно эквивалентен приведенному выше, плюс некоторые функции, подобные кортежу:
от ввода импорта коллекций импорта NamedTuple Точка = коллекции . именованныйтупле ( 'Точка' , [ 'x' , 'y' ])# следующее создает аналогичный именованный кортеж для вышеуказанного класса Point ( NamedTuple ): x : int y : int
Классы данных, представленные в Python 3.7, позволяют разработчикам эмулировать неизменность с помощью замороженных экземпляров. Если создан замороженный класс данных, dataclasses
он будет переопределяться __setattr__()
и __delattr__()
подниматься FrozenInstanceError
при вызове.
из классов данных импортировать класс данных@dataclass ( Frozen = True ) класс Точка : x : int y : int
Racket существенно отличается от других реализаций Scheme , делая тип базовой пары («минус-ячейки») неизменяемым. Вместо этого он предоставляет параллельный изменяемый парный тип через mcons
, mcar
и set-mcar!
т. д. Кроме того, поддерживаются многие неизменяемые типы, например неизменяемые строки и векторы, и они широко используются. Новые структуры по умолчанию являются неизменяемыми, если только поле специально не объявлено изменяемым или вся структура:
( структура foo1 ( x y )) ; все поля неизменяемы ( struct foo2 ( x [ y #:mutable ])) ; одно изменяемое поле ( struct foo3 ( x y ) #:mutable ) ; все поля изменяемы
Язык также поддерживает функционально реализованные неизменяемые хэш-таблицы и неизменяемые словари.
Система владения Rust позволяет разработчикам объявлять неизменяемые переменные и передавать неизменяемые ссылки. По умолчанию все переменные и ссылки являются неизменяемыми. Изменяемые переменные и ссылки создаются явно с помощью mut
ключевого слова.
Постоянные элементы в Rust всегда неизменяемы.
// константные элементы всегда неизменяемы const ALWAYS_IMMUTABLE : bool = true ; struct Object { x : использовать размер , y : использовать размер , } fn main () { // явно объявляем изменяемую переменную let mut mutable_obj = Object { x : 1 , y : 2 }; mutable_obj . х = 3 ; // хорошо let mutable_ref = & mut mutable_obj ; mutable_ref . х = 1 ; // хорошо пусть immutable_ref = & mutable_obj ; immutable_ref . х = 3 ; // ошибка E0594 // по умолчанию переменные неизменяемы let immutable_obj = Object { x : 4 , y : 5 }; immutable_obj . х = 6 ; // ошибка E0596 let mutable_ref2 = & mut immutable_obj ; // ошибка E0596 пусть immutable_ref2 = & immutable_obj ; immutable_ref2 . х = 6 ; // ошибка E0594 }
В Scala любая сущность (в узком смысле — привязка) может быть определена как изменяемая или неизменяемая: в объявлении можно использовать val
(value) для неизменяемых сущностей и var
(variable) для изменяемых. Обратите внимание: даже если неизменяемую привязку нельзя переназначить, она все равно может ссылаться на изменяемый объект, и для этого объекта по-прежнему можно вызывать изменяющие методы: привязка является неизменяемой, но базовый объект может быть изменяемым.
Например, следующий фрагмент кода:
val maxValue = 100 вар currentValue = 1
определяет неизменяемую сущность maxValue
(целочисленный тип определяется во время компиляции) и изменяемую сущность с именем currentValue
.
По умолчанию классы коллекций, такие как List
и Map
, являются неизменяемыми, поэтому методы обновления возвращают новый экземпляр, а не изменяют существующий. Хотя это может показаться неэффективным, реализация этих классов и их гарантии неизменяемости означают, что новый экземпляр может повторно использовать существующие узлы, что, особенно в случае создания копий, очень эффективно. [13] [ нужен лучший источник ]
Эта статья содержит некоторые материалы из Книги шаблонов проектирования Perl.
Предпочтительный способ — сделать класс финальным.
Иногда это называют «сильной неизменностью».
Это не позволяет кому-либо расширить ваш класс и случайно или намеренно сделать его изменяемым.