Неизменяемый объект - Immutable object

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

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

Концепции

Неизменяемые переменные

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

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

Слабая и сильная неизменность

Иногда говорят о неизменности определенных полей объекта. Это означает, что нет возможности изменить эти части состояния объекта, даже если другие части объекта могут быть изменяемыми ( слабо неизменяемыми ). Если все поля неизменяемы, объект неизменен. Если весь объект не может быть расширен другим классом, объект называется строго неизменяемым . Это может, например, помочь явным образом обеспечить соблюдение определенных инвариантов относительно определенных данных в объекте, которые остаются неизменными в течение всего времени существования объекта. В некоторых языках это делается с помощью ключевого слова (например, constв C ++ , finalв Java ), которое обозначает поле как неизменяемое. В некоторых языках это происходит наоборот: в OCaml поля объекта или записи по умолчанию неизменяемы и для этого должны быть явно отмечены значком mutable.

Ссылки на объекты

В большинстве объектно-ориентированных языков на объекты можно ссылаться . Некоторыми примерами таких языков являются Java , C ++ , C # , VB.NET и многие языки сценариев , такие как Perl , Python и Ruby . В этом случае имеет значение, может ли состояние объекта меняться при совместном использовании объектов через ссылки.

Ссылки и копирование объектов

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

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

Копирование при записи

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

Интернирование

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

Безопасность потоков

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

Нарушение неизменности

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

Детали для конкретного языка

В Python , Java и .NET Framework строки являются неизменяемыми объектами. И Java, и .NET Framework имеют изменяемые версии строки. В Java это StringBufferи StringBuilder(изменяемые версии Java String), а в .NET - это StringBuilder(изменяемая версия .Net String). Python 3 имеет изменяемый строковый (байтовый) вариант с именем bytearray.

Кроме того, все примитивные классы-оболочки в Java неизменяемы.

Аналогичные шаблоны - это неизменяемый интерфейс и неизменяемая оболочка .

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

Ада

В Ada любой объект объявляется либо переменной (т. Е. Изменяемым; обычно неявное значение по умолчанию), либо constant(т. Е. Неизменяемым) с помощью constantключевого слова.

  type Some_type is new Integer; -- could be anything more complicated
  x: constant Some_type:= 1; -- immutable
  y: Some_type; -- mutable

Параметры подпрограмм являются неизменными в в режиме, и изменяемых в в из и из режимов.

  procedure Do_it(a: in Integer; b: in out Integer; c: out Integer) is
  begin
    -- a is immutable
    b:= b + a;
    c:= a;
  end Do_it;

C #

В C # вы можете обеспечить неизменность полей класса с помощью readonlyоператора. Установив все поля как неизменяемые, вы получите неизменяемый тип.

class AnImmutableType
{
    public readonly double _value;
    public AnImmutableType(double x) 
    { 
        _value = x; 
    }
    public AnImmutableType Square() 
    { 
        return new AnImmutableType(_value * _value); 
    }
}

C ++

В C ++ константно-правильная реализация Cartпозволяет пользователю создавать экземпляры класса, а затем использовать их как const(неизменяемые) или изменяемые, по желанию, путем предоставления двух разных версий items()метода. (Обратите внимание, что в C ++ нет необходимости - и фактически невозможно - предоставлять специализированный конструктор для constэкземпляров.)

class Cart {
 public:
  Cart(std::vector<Item> items): items_(items) {}

  std::vector<Item>& items() { return items_; }
  const std::vector<Item>& items() const { return items_; }

  int ComputeTotalCost() const { /* return sum of the prices */ }

 private:
  std::vector<Item> items_;
};

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

C ++ также обеспечивает абстрактную (в отличие от побитовой) неизменяемость с помощью mutableключевого слова, которое позволяет изменять переменную-член внутри constметода.

class Cart {
 public:
  Cart(std::vector<Item> items): items_(items) {}

  const std::vector<Item>& 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

В D , существует два отборочных типа , constи immutableдля переменных , которые не могут быть изменены. В отличие от языков C ++ const, Java finalи C # readonly, они транзитивны и рекурсивно применяются ко всему, что доступно через ссылки на такую ​​переменную. Разница между constи immutableзаключается в том, к чему они применяются: constэто свойство переменной: юридически могут существовать изменяемые ссылки на указанное значение, т.е. значение может фактически измениться. Напротив, immutableэто свойство упомянутого значения: значение и все, что транзитивно достижимо из него, не может измениться (без нарушения системы типов, что приводит к неопределенному поведению ). Любая ссылка на это значение должна быть помечена constили immutable. В основном для любого неквалифицированного типа T, const(T)является объединением непересекающихся T(изменяемый) и immutable(T).

class C {
  /*mutable*/ Object mField;
    const     Object cField;
    immutable Object iField;
}

Для изменяемого Cобъекта его mFieldможно записать. Для const(C)объекта, mFieldне может быть изменен, он наследует const; iFieldостается неизменным, поскольку это более надежная гарантия. Для объекта immutable(C)все поля неизменяемы.

В такой функции:

void func(C m, const C c, immutable C i)
{ /* inside the braces */ }

Внутри фигурных скобок 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)значений.

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

Тип string- это псевдоним immutable(char)[], то есть типизированный фрагмент памяти неизменяемых символов. Создание подстрок дешево, поскольку оно просто копирует и изменяет указатель и поле длины, и безопасно, поскольку базовые данные не могут быть изменены. Объекты типа const(char)[]могут относиться к строкам, но также и к изменяемым буферам.

Создание неглубокой копии константного или неизменяемого значения удаляет внешний слой неизменяемости: копирование неизменяемой строки ( immutable(char[])) возвращает строку ( immutable(char)[]). Неизменяемый указатель и длина копируются, и копии изменяемы. Указанные данные не были скопированы и в этом примере сохраняют свой квалификатор immutable. Его можно удалить, сделав более глубокую копию, например, используя dupфункцию.

Джава

Классическим примером неизменяемого объекта является экземпляр Stringкласса Java.

String s = "ABC";
s.toLowerCase();

Метод toLowerCase()не изменяет данные, которые sсодержат "ABC" . Вместо этого создается новый объект String, которому во время его создания предоставляются данные «abc». Ссылка на этот объект String возвращается toLowerCase()методом. Чтобы строка sсодержала данные «abc», необходим другой подход:

s = s.toLowerCase();

Теперь String sссылается на новый объект String, содержащий «abc». В синтаксисе объявления класса String нет ничего, что считало бы его неизменным; скорее, ни один из методов класса String никогда не влияет на данные, содержащиеся в объекте String, что делает его неизменяемым.

Ключевое слово final( подробная статья ) используется для реализации неизменяемых примитивных типов и ссылок на объекты, но само по себе не может сделать сами объекты неизменяемыми. См. Примеры ниже:

Примитивный переменные типа ( int, long, shortи т.д.) могут быть переназначены после того , как определены. Этого можно избежать, используя final.

int i = 42; //int is a primitive type
i = 43; // OK

final int j = 42;
j = 43; // does not compile. j is final so can't be reassigned

Ссылочные типы нельзя сделать неизменяемыми с помощью finalключевого слова. finalтолько предотвращает переназначение.

final MyObject m = new MyObject(); //m is of reference type
m.data = 100; // OK. We can change state of object m (m is mutable and final doesn't change this fact)
m = new MyObject(); // does not compile. m is final so can't be reassigned

Примитивные упаковщики ( Integer, Long, Short, Double, Float, Character, Byte, Boolean) также все неизменны. Неизменяемые классы можно реализовать, следуя нескольким простым рекомендациям.

JavaScript

В JavaScript все примитивные типы (Undefined, Null, Boolean, Number, BigInt, String, Symbol) неизменяемы, но пользовательские объекты обычно изменяемы.

function doSomething(x) { /* does changing x here change the original? */ };
var str = 'a string';
var obj = { an: 'object' };
doSomething(str);         // strings, numbers and bool types are immutable, function gets a copy
doSomething(obj);         // objects are passed in by reference and are mutable inside function
doAnotherThing(str, obj); // `str` has not changed, but `obj` may have.

Чтобы имитировать неизменяемость объекта, можно определить свойства как доступные только для чтения (для записи: false).

var obj = {};
Object.defineProperty(obj, 'foo', { value: 'bar', writable: false });
obj.foo = 'bar2'; // silently ignored

Однако описанный выше подход по-прежнему позволяет добавлять новые свойства. В качестве альтернативы можно использовать Object.freeze, чтобы сделать существующие объекты неизменяемыми.

var obj = { foo: 'bar' };
Object.freeze(obj);
obj.foo = 'bars'; // cannot edit property, silently ignored
obj.foo2 = 'bar2'; // cannot add property, silently ignored

С реализацией ECMA262 JavaScript имеет возможность создавать неизменяемые ссылки, которые нельзя переназначить. Однако использование constобъявления не означает, что значение ссылки только для чтения является неизменным, просто имя не может быть присвоено новому значению.

const ALWAYS_IMMUTABLE = true;

try {
  ALWAYS_IMMUTABLE = false;
} catch (err) {
  console.log("Can't reassign an immutable reference.");
}

const arr = [1, 2, 3];
arr.push(4);
console.log(arr); // [1, 2, 3, 4]

Использование неизменяемого состояния стало растущей тенденцией в JavaScript с момента появления React , который отдает предпочтение шаблонам управления состоянием, подобным Flux, таким как Redux .

Perl

В Perl можно создать неизменяемый класс с библиотекой Moo, просто объявив все атрибуты только для чтения:

package Immutable;
use Moo;

has value => (
    is      => 'ro',   # read only
    default => 'data', # can be overridden by supplying the constructor with
                       # a value: Immutable->new(value => 'something else');
);

1;

Для создания неизменяемого класса требовалось два шага: во-первых, создание аксессоров (автоматически или вручную), которые предотвращают изменение атрибутов объекта, и, во-вторых, предотвращение прямого изменения данных экземпляра экземпляров этого класса (обычно это хранилось в хеш-коде). ссылку и может быть заблокирован с помощью функции Hash :: Util lock_hash):

package Immutable;
use strict;
use warnings;
use base qw(Class::Accessor);
# create read-only accessors
__PACKAGE__->mk_ro_accessors(qw(value));
use Hash::Util 'lock_hash';

sub new {
    my $class = shift;
    return $class if ref($class);
    die "Arguments to new must be key => value pairs\n"
        unless (@_ % 2 == 0);
    my %defaults = (
        value => 'data',
    );
    my $obj = {
        %defaults,
        @_,
    };
    bless $obj, $class;
    # prevent modification of the object data
    lock_hash %$obj;
}
1;

Или с помощью написанного вручную аксессуара:

package Immutable;
use strict;
use warnings;
use Hash::Util 'lock_hash';

sub new {
    my $class = shift;
    return $class if ref($class);
    die "Arguments to new must be key => value pairs\n"
        unless (@_ % 2 == 0);
    my %defaults = (
        value => 'data',
    );
    my $obj = {
        %defaults,
        @_,
    };
    bless $obj, $class;
    # prevent modification of the object data
    lock_hash %$obj;
}

# read-only accessor
sub value {
    my $self = shift;
    if (my $new_value = shift) {
        # trying to set a new value
        die "This object cannot be modified\n";
    } else {
        return $self->{value}
    }
}
1;

Python

В Python некоторые встроенные типы (числа, логические значения, строки, кортежи, замороженные наборы) неизменяемы, но пользовательские классы обычно изменяемы. Чтобы имитировать неизменяемость в классе, можно переопределить установку и удаление атрибута, чтобы вызвать исключения:

class ImmutablePoint:
    """An immutable class with two attributes 'x' and 'y'."""

    __slots__ = ['x', 'y']

    def __setattr__(self, *args):
        raise TypeError("Can not modify immutable instance.")

    __delattr__ = __setattr__

    def __init__(self, x, y):
        # We can no longer use self.value = value to store the instance data
        # so we must explicitly call the superclass
        super().__setattr__('x', x)
        super().__setattr__('y', y)

Стандартные помощники библиотеки collections.namedtupleи typing.NamedTuple, доступные начиная с Python 3.6, создают простые неизменяемые классы. Следующий пример примерно эквивалентен приведенному выше, плюс некоторые функции, похожие на кортежи:

from typing import NamedTuple
import collections

Point = collections.namedtuple('Point', ['x', 'y'])

# the following creates a similar namedtuple to the above
class Point(NamedTuple):
    x: int
    y: int

Представленный в Python 3.7, dataclassesпозволяет разработчикам эмулировать неизменяемость с помощью замороженных экземпляров . Если построен замороженный класс данных, dataclassesбудет переопределен __setattr__()и __delattr__()будет повышаться FrozenInstanceErrorпри вызове.

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: int
    y: int

Ракетка

Racket существенно отличается от других реализаций Схемы , делая свой тип базовой пары («cons-ячейки») неизменяемым. Вместо этого, он обеспечивает параллельный изменяемый тип пару, с помощью mcons, mcar, и set-mcar!т.д. Кроме того, многие неизменные типов поддерживаемых, например, неизменных строк и векторов, и они широко используются. Новые структуры неизменяемы по умолчанию, если только поле специально не объявлено изменяемым, или вся структура:

(struct foo1 (x y))             ; all fields immutable
(struct foo2 (x [y #:mutable])) ; one mutable field
(struct foo3 (x y) #:mutable)   ; all fields mutable

Язык также поддерживает неизменяемые хеш-таблицы, реализованные функционально, и неизменяемые словари.

Ржавчина

Система владения Rust позволяет разработчикам объявлять неизменяемые переменные и передавать неизменяемые ссылки. По умолчанию все переменные и ссылки неизменяемы. Изменяемые переменные и ссылки явно создаются с помощью mutключевого слова.

Постоянные элементы в Rust всегда неизменяемы.

// constant items are always immutable
const ALWAYS_IMMUTABLE: bool = true;

struct Object {
    x: usize,
    y: usize,
}

fn main() {
    // explicitly declare a mutable variable
    let mut mutable_obj = Object { x: 1, y: 2 };
    mutable_obj.x = 3; // okay

    let mutable_ref = &mut mutable_obj;
    mutable_ref.x = 1; // okay

    let immutable_ref = &mutable_obj;
    immutable_ref.x = 3; // error E0594

    // by default, variables are immutable
    let immutable_obj = Object { x: 4, y: 5 };
    immutable_obj.x = 6; // error E0596

    let mutable_ref2 = 
        &mut immutable_obj; // error E0596

    let immutable_ref2 = &immutable_obj;
    immutable_ref2.x = 6; // error E0594
    
}

Scala

В Scala любую сущность (узко, привязку) можно определить как изменяемую или неизменяемую: в объявлении можно использовать val(значение) для неизменяемых сущностей и var(переменную) для изменяемых. Обратите внимание, что даже несмотря на то, что неизменяемая привязка не может быть переназначена, она по-прежнему может относиться к изменяемому объекту, и по-прежнему можно вызывать изменяющие методы для этого объекта: привязка неизменна, но базовый объект может быть изменяемым.

Например, следующий фрагмент кода:

val maxValue = 100
var currentValue = 1

определяет неизменяемую сущность maxValue(целочисленный тип определяется во время компиляции) и изменяемую сущность с именем currentValue.

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

Смотрите также

использованная литература

Эта статья содержит некоторые материалы из книги Perl Design Patterns Book.

внешние ссылки