В объектно-ориентированном (ОО) и функциональном программировании неизменяемый объект (неизменяемый [1] объект) — это объект , состояние которого не может быть изменено после его создания. [2] Это контрастирует с изменяемыми объектами (изменяемыми объектами), которые могут быть изменены после его создания. [3] В некоторых случаях объект считается неизменяемым, даже если некоторые внутренне используемые атрибуты изменяются, но состояние объекта выглядит неизменным с внешней точки зрения. Например, объект, который использует мемоизацию для кэширования результатов дорогостоящих вычислений, все еще может считаться неизменяемым объектом.
Строки и другие конкретные объекты обычно выражаются как неизменяемые объекты для улучшения читаемости и эффективности выполнения в OO-программировании. Неизменяемые объекты также полезны, поскольку они по своей сути потокобезопасны . [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 являются неизменяемыми.
Похожими шаблонами являются Immutable Interface и Immutable Wrapper.
В чистых функциональных языках программирования невозможно создавать изменяемые объекты без расширения языка (например, с помощью библиотеки изменяемых ссылок или интерфейса внешней функции ), поэтому все объекты являются неизменяемыми.
В языке Ada любой объект объявляется либо переменным (т. е. изменяемым; обычно это неявное значение по умолчанию), либо constant
(т. е. неизменяемым) с помощью constant
ключевого слова.
тип Some_type is new Integer ; -- может быть чем-то более сложным x : const Some_type := 1 ; -- неизменяемый y : Some_type ; -- изменяемый
Параметры подпрограммы неизменяемы в режиме in и изменяемы в режимах in out и out .
procedure Do_it ( a : in Integer ; b : in out Integer ; c : out Integer ) is begin -- a является неизменяемым b := b + a ; c := a ; end Do_it ;
В C# вы можете обеспечить неизменяемость полей класса с помощью оператора readonly
. [7] : 239
Обеспечивая неизменяемость всех полей, вы получаете неизменяемый тип.
class AnImmutableType { public readonly double _value ; public AnImmutableType ( double x ) { _value = x ; } public AnImmutableType Square () { return new AnImmutableType ( _value * _value ); } }
В C# есть записи, которые являются неизменяемыми. [8] [9]
запись Person ( string FirstName , string LastName );
В C++ реализация с корректным значением constCart
позволила бы пользователю создавать экземпляры класса, а затем использовать их как const
(неизменяемые) или как изменяемые, по желанию, предоставляя две различные версии метода items()
. (Обратите внимание, что в C++ не обязательно — и фактически невозможно — предоставлять специализированный конструктор для const
экземпляров.)
класс Корзина { public : Корзина ( std :: vector < Элемент > элементы ) : элементы_ ( элементы ) {} std :: vector < Элемент >& items () { return items_ ; } const std :: vector < Элемент >& items () const { return items_ ; } int ComputeTotalCost () const { /* возвращает сумму цен */ } private : std :: vector < Элемент > items_ ; };
Обратите внимание, что если есть член данных, являющийся указателем или ссылкой на другой объект, то изменить объект, на который он указывает или на который ссылается, можно только в неконстантном методе.
C++ также обеспечивает абстрактную (в отличие от побитовой) неизменяемость с помощью mutable
ключевого слова, которое позволяет изменять переменную-член изнутри const
метода.
класс Корзина { public : Корзина ( std :: vector < Элемент > элементы ) : элементы_ ( элементы ) {} const std :: vector < Элемент >& items () const { return items_ ; } int ComputeTotalCost () const { if ( total_cost_ ) { return * total_cost_ ; } int total_cost = 0 ; for ( const auto & item : items_ ) { total_cost += item.Cost ( ) ; } total_cost_ = total_cost ; return total_cost ; } private : std :: vector < Item > items_ ; mutable std :: Optional < int > total_cost_ ; };
В D существуют два квалификатора типа , const
и immutable
, для переменных, которые не могут быть изменены. [10] В отличие от C++ const
, Java final
и C# readonly
они транзитивны и рекурсивно применяются ко всему, что доступно через ссылки такой переменной. Разница между const
и immutable
заключается в том, к чему они применяются: const
является свойством переменной: могут законно существовать изменяемые ссылки на указанное значение, т. е. значение может фактически изменяться. Напротив, immutable
является свойством указанного значения: значение и все, к чему можно транзитивно добраться из него, не могут измениться (без нарушения системы типов, что приводит к неопределенному поведению ). Любая ссылка на это значение должна быть помечена const
или immutable
. По сути, для любого неквалифицированного типа T
, const(T)
является непересекающимся объединением T
(изменяемого) и immutable(T)
.
класс C { /*изменяемый*/ Объект mField ; константный Объект cField ; неизменяемый Объект iField ; }
Для изменяемого C
объекта mField
в него можно записывать. Для const(C)
объекта, mField
который нельзя изменить, он наследует const
; iField
по-прежнему неизменяем, поскольку это более сильная гарантия. Для immutable(C)
, все поля неизменяемы.
В такой функции:
void func ( C m , const C c , immutable 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)
значений.
Приведение неизменяемых значений к изменяемым приводит к неопределенному поведению при изменении, даже если исходное значение происходит из изменяемого источника. Приведение изменяемых значений к неизменяемым может быть допустимым, если после этого не остается изменяемых ссылок. «Выражение может быть преобразовано из изменяемого (...) в неизменяемое, если выражение уникально и все выражения, на которые оно транзитивно ссылается, являются либо уникальными, либо неизменяемыми». [10] Если компилятор не может доказать уникальность, приведение может быть выполнено явно, и программист должен убедиться, что не существует изменяемых ссылок.
Тип string
является псевдонимом для immutable(char)[]
, т. е. типизированного фрагмента памяти неизменяемых символов. [11] Создание подстрок дешево, так как оно просто копирует и изменяет указатель и поле длины, и безопасно, так как базовые данные не могут быть изменены. Объекты типа const(char)[]
могут ссылаться на строки, а также на изменяемые буферы.
Создание поверхностной копии константного или неизменяемого значения удаляет внешний слой неизменяемости: Копирование неизменяемой строки ( immutable(char[])
) возвращает строку ( immutable(char)[]
). Неизменяемый указатель и длина копируются, и копии изменяемы. Указанные данные не были скопированы и сохраняют свой квалификатор, в примере immutable
. Их можно удалить, сделав более глубокую копию, например, с помощью dup
функции .
Классическим примером неизменяемого объекта является экземпляр класса String
Java
String s = "ABC" ; s . toLowerCase (); // Это ничего не даст!
Метод toLowerCase()
не изменяет данные "ABC", которые s
содержатся. Вместо этого создается новый объект String и ему придаются данные "abc" во время его создания. Метод возвращает ссылку на этот объект String toLowerCase()
. Чтобы заставить String s
содержать данные "abc", необходим другой подход:
s = s .toLowerCase ( );
Теперь String ссылается на новый объект String, содержащий "abc". В синтаксисе объявленияs
класса String нет ничего , что делало бы его неизменяемым; скорее, ни один из методов класса String никогда не влияет на данные, которые содержит объект String, тем самым делая его неизменяемым.
Ключевое слово final
( подробная статья ) используется при реализации неизменяемых примитивных типов и ссылок на объекты, [12] но оно не может само по себе сделать сами объекты неизменяемыми. Смотрите примеры ниже:
Переменные примитивного типа ( int
, long
, short
, и т.д.) могут быть переназначены после определения. Этого можно избежать, используя final
.
int i = 42 ; //int — примитивный тип i = 43 ; // OK final int j = 42 ; j = 43 ; // не компилируется. j является final, поэтому не может быть переназначен
Ссылочные типы нельзя сделать неизменяемыми только с помощью final
ключевого слова. final
предотвращает только переназначение.
final MyObject m = new MyObject (); //m имеет ссылочный тип m . data = 100 ; // OK. Мы можем изменить состояние объекта m (m изменяем и final не меняет этот факт) m = new MyObject (); // не компилируется. m является final, поэтому не может быть переназначен
Примитивные оболочки ( Integer
, Long
, Short
, Double
, Float
, Character
, Byte
, Boolean
) также являются неизменяемыми. Неизменяемые классы могут быть реализованы с помощью нескольких простых рекомендаций. [13]
В JavaScript все примитивные типы (Undefined, Null, Boolean, Number, BigInt, String, Symbol) являются неизменяемыми, но пользовательские объекты, как правило, изменяемы.
function doSomething ( x ) { /* изменяет ли изменение x здесь оригинал? */ }; var str = 'a string' ; var obj = { an : 'object' }; doSomething ( str ); // строки, числа и типы bool неизменяемы, функция получает копию doSomething ( obj ); // объекты передаются по ссылке и могут быть изменены внутри function doAnotherThing ( str , obj ); // `str` не изменился, но `obj` мог измениться.
Чтобы имитировать неизменность объекта, можно определить свойства как доступные только для чтения (записываемые: false).
var obj = {}; Object.defineProperty ( obj , ' foo' , { value : ' bar ' , writable : false }); obj.foo = ' bar2' ; // молча игнорируется
Однако подход выше все еще позволяет добавлять новые свойства. В качестве альтернативы можно использовать Object.freeze, чтобы сделать существующие объекты неизменяемыми.
var obj = { foo : 'bar' }; Object . freeze ( obj ); obj . foo = 'bars' ; // невозможно редактировать свойство, игнорируется без уведомления obj . foo2 = 'bar2' ; // невозможно добавить свойство, игнорируется без уведомления
С реализацией ECMA262 JavaScript имеет возможность создавать неизменяемые ссылки, которые нельзя переназначить. Однако использование const
объявления не означает, что значение ссылки только для чтения является неизменяемым, а означает лишь, что имя не может быть назначено новому значению.
const ALWAYS_IMMUTABLE = true ; try { ALWAYS_IMMUTABLE = false ; } catch ( err ) { console . log ( "Невозможно переназначить неизменяемую ссылку." ); } const arr = [ 1 , 2 , 3 ]; arr . push ( 4 ); console . log ( arr ); // [1, 2, 3, 4]
Использование неизменяемого состояния стало растущей тенденцией в JavaScript с момента появления React , который отдает предпочтение Flux-подобным шаблонам управления состоянием, таким как Redux . [14]
В Perl можно создать неизменяемый класс с помощью библиотеки Moo, просто объявив все атрибуты доступными только для чтения:
пакет Immutable ; использовать Moo ; имеет значение => ( is => 'ro' , # только для чтения default => 'data' , # можно переопределить, указав в конструкторе # значение: Immutable->new(value => 'something else'); ); 1 ;
Раньше создание неизменяемого класса требовало двух шагов: во-первых, создание методов доступа (автоматически или вручную), которые предотвращают изменение атрибутов объекта, и, во-вторых, предотвращение прямого изменения данных экземпляра экземпляров этого класса (они обычно хранились в ссылке на хэш и могли быть заблокированы с помощью функции lock_hash класса Hash::Util):
package Immutable ; use strict ; use warnings ; use base qw(Class::Accessor) ; # создать методы доступа только для чтения __PACKAGE__ -> mk_ro_accessors ( qw(value) ); use Hash::Util 'lock_hash' ; sub new { my $class = shift ; return $class if ref ( $class ); die "Аргументы new должны быть парами ключ => значение\n" unless ( @_ % 2 == 0 ); my %defaults = ( value => 'data' , ); my $obj = { %defaults , @_ , }; bless $obj , $class ; # предотвратить изменение данных объекта lock_hash %$obj ; } 1 ;
Или с помощью вручную написанного метода доступа:
пакет Immutable ; использовать строгий ; использовать предупреждения ; использовать Hash::Util 'lock_hash' ; sub new { my $class = shift ; return $class if ref ( $class ); die "Аргументы для new должны быть парами ключ => значение\n" unless ( @_ % 2 == 0 ); my %defaults = ( value => 'data' , ); my $obj = { %defaults , @_ , }; bless $obj , $class ; # предотвратить изменение данных объекта lock_hash %$obj ; } # метод доступа только для чтения sub value { my $self = shift ; if ( my $new_value = shift ) { # попытка задать новое значение die "Этот объект не может быть изменен\n" ; } else { return $self -> { value } } } 1 ;
В Python некоторые встроенные типы (числа, булевы значения, строки, кортежи, frozensets) являются неизменяемыми, но пользовательские классы, как правило, изменяемы. Чтобы имитировать неизменяемость в классе, можно переопределить установку и удаление атрибутов, чтобы вызвать исключения:
class ImmutablePoint : """Неизменяемый класс с двумя атрибутами 'x' и 'y'.""" __слоты__ = [ 'x' , 'y' ] def __setattr__ ( self , * args ): raise TypeError ( "Невозможно изменить неизменяемый экземпляр." ) __делаттр__ = __сетаттр__ def __init__ ( self , x , y ): # Мы больше не можем использовать self.value = value для хранения данных экземпляра # поэтому мы должны явно вызвать суперкласс super () . __setattr__ ( 'x' , x ) super () . __setattr__ ( 'y' , y )
Стандартные вспомогательные библиотеки collections.namedtuple и typing.NamedTuple, доступные с Python 3.6 и выше, создают простые неизменяемые классы. Следующий пример примерно эквивалентен приведенному выше, плюс некоторые функции, похожие на кортежи:
от ввода import NamedTuple import collectionsТочка = коллекции . namedtuple ( 'Точка' , [ 'x' , 'y' ])# следующий код создает именованный кортеж, аналогичный указанному выше классу Point ( NamedTuple ): x : int y : int
Представленные в Python 3.7, классы данных позволяют разработчикам эмулировать неизменяемость с замороженными экземплярами. Если замороженный класс данных создан, он dataclasses
переопределит __setattr__()
и __delattr__()
вызовет FrozenInstanceError
при вызове.
из классов данных импортировать класс данных@dataclass ( frozen = True ) класс Точка : x : целое y : целое
Racket существенно отличается от других реализаций Scheme , делая свой основной тип пар («cons cells») неизменяемым. Вместо этого он предоставляет параллельный изменяемый тип пар через mcons
, mcar
и set-mcar!
т. д. Кроме того, поддерживаются многие неизменяемые типы, например, неизменяемые строки и векторы, и они широко используются. Новые структуры неизменяемы по умолчанию, если только поле или вся структура специально не объявлены изменяемыми:
( struct 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 . x = 3 ; // хорошо пусть mutable_ref = & mut mutable_obj ; mutable_ref . x = 1 ; // хорошо пусть immutable_ref = & mutable_obj ; immutable_ref . x = 3 ; // ошибка E0594 // по умолчанию переменные неизменяемы let immutable_obj = Object { x : 4 , y : 5 }; immutable_obj . x = 6 ; // ошибка E0596 пусть mutable_ref2 = & mut immutable_obj ; // ошибка E0596 пусть immutable_ref2 = & immutable_obj ; immutable_ref2 . x = 6 ; // ошибка E0594 }
В Scala любая сущность (узко, привязка) может быть определена как изменяемая или неизменяемая: в объявлении можно использовать val
(значение) для неизменяемых сущностей и var
(переменная) для изменяемых. Обратите внимание, что даже если неизменяемая привязка не может быть переназначена, она все равно может ссылаться на изменяемый объект, и все еще возможно вызывать изменяющие методы для этого объекта: привязка неизменяема , но базовый объект может быть изменяемым.
Например, следующий фрагмент кода:
val maxValue = 100 вар currentValue = 1
определяет неизменяемую сущность maxValue
(целочисленный тип выводится во время компиляции) и изменяемую сущность с именем currentValue
.
По умолчанию классы коллекций, такие как List
и Map
являются неизменяемыми, поэтому методы обновления возвращают новый экземпляр, а не мутируют существующий. Хотя это может показаться неэффективным, реализация этих классов и их гарантии неизменяемости означают, что новый экземпляр может повторно использовать существующие узлы, что, особенно в случае создания копий, очень эффективно. [15] [ требуется лучший источник ]
В статье содержится часть материала из книги «Perl Design Patterns Book».
Предпочтительный способ — сделать класс final. Иногда это называют "сильной неизменяемостью". Это не позволяет кому-либо расширить ваш класс и случайно или намеренно сделать его изменяемым.