Незмінний об'єкт
Незмі́нний об'є́кт (англ. Immutable object) — в об'єктно-орієнтованому програмуванні, об'єкт, стан якого не може бути змінено після створення. На противагу незмінним, стан змінних об'єктів може змінюватись після створення. Об'єкт може бути як незмінним повністю, так і певні його атрибути можуть бути задекларовані незмінними, використовуючи, наприклад, декларацію const мови програмування C++. У деяких випадках, об'єкт вважається незмінним навіть тоді, коли змінюються деякі його внутрішні атрибути, за умови, що зовні його стан виглядає незмінним. Наприклад, об'єкт, який використовує запам'ятовування результатів проміжних обчислень для кешування результатів складних обчислень, може вважатись незмінним. Початковий стан незмінного об'єкта, як правило, визначається під час створення об'єкта, але, він може, також, визначатись безпосередньо перед використанням об'єкта.
Часто, незмінні об'єкти можуть бути корисними через те, що вони дозволяють уникнути деяких дорогих операцій копіювання та порівняння, полегшуючи, в такий спосіб, вихідний код програми, та прискорюючи її роботу. Однак, у деяких випадках, незмінність об'єкта може заважати, наприклад, якщо об'єкт містить велику кількість змінних даних. Через це, багато мов програмування мають можливості роботи як із змінними, так і з незмінними об'єктами.
Незмінні об'єкти часто бувають корисними завдяки тому, що вони по суті потоко-безпечні.[1] Інша перевага в тому, що вони є простішими для розуміння і пропонують більш високий рівень безпеки, ніж змінювані об'єкти.[1]
Реалізація
Незмінність об'єкту не означає, що фізична пам'ять в якій зберігається об'єкт не має доступу для запису. Скоріше, властивість бути незмінним йому задається під час компіляції у вигляді конструкції, яка вказує програмісту що можна робити з об'єктом через його інтерфейс, не зважаючи на те, що в абсолютній ситуації він може змінити його (наприклад, в обхід системи типів або порушуючи константність в C або C++).
Ada
В Ada будь-який об'єкт оголошується або variable
(тобто змінним), або constant
(тобто незмінним) через ключове слово constant
.
type Some_type is new Integer;
x: constant 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
-- а незмінний
b:= b + a;
c:= a;
end Do_it;
C#
У майбутніх версіях C# можливо з'явиться ключове слово immutable
, за допомогою якого можна буде визначати незмінний тип на основі його сигнатури. Поки ж нам доводиться користуватися тим, що є.
Візьмемо для прикладу клас RGBColor
, що описує деякий колір в колірній моделі RGB:
public class RGBColor
{
public int Red { get; set; }
public int Green { get; set; }
public int Blue { get; set; }
public RGBColor(int red = 0, int green = 0, int blue = 0)
{
Red = red;
Green = green;
Blue = blue;
}
}
Цей клас має автоматичні властивості як для читання, так і для запису і необов'язкові параметри, які дозволяють користувачу опускати значення для будь-яких компонентних кольорів.
Для перетворення даного класу в незмінний об’єкт перш за все зробимо його сетери приватними (private
)
public class RGBColor
{
public int Red { get; private set; }
public int Green { get; private set; }
public int Blue { get; private set; }
public RGBColor(int red = 0, int green = 0, int blue = 0)
{
Red = red;
Green = green;
Blue = blue;
}
}
Тепер об'єкт не може бути змінений ззовні, однак всередині він так само може змінюватись. Необхідно позбутись від сеттерів і впровадити поля тільки для читання (readonly). Також змінимо будову конструктора, перетворивши його необов’язкові поля на обов’язкові.
public class RGBColor
{
private readonly int red;
private readonly int green;
private readonly int blue;
public int Red => red;
public int Green => green
public int Blue => blue;
public RGBColor(int red, int green, int blue)
{
this.red = red;
this.green = green;
this.blue = blue;
}
}
Отже, можна виділити 3 прості кроки для створення незмінного класу в С#:
- Видалити сетери.
- Перетворити приватні поля на readonly (або створити їх якщо вони не існують).
- Змінити конструктор так, щоб він вимагав всі необхідні параметри під час створення нового об’єкту [2].
C++
В C++, реалізація константності класу Cart
дозволено створювати нові екземпляри класу незмінними, з використанням ключового слова const
(незмінний) або змінним, за бажанням, забезпечуючи дві різні версії методу getItems()
.
(Відмітимо, що в C++ це не обов'язково — і фактично не можливо — створити спеціалізований конструктор для незмінних екземплярів const
.)
template<typename T>
class Cart {
private:
std::vector<T> items;
public:
Cart(const std::vector<T>& v): items(v) { }
std::vector<T>& getItems() { return items; }
const std::vector<T>& getItems() const { return items; }
int total() const { /* повертає суму */ }
};
Якби в класі було поле, яке є вказівником чи посиланням на інший об'єкт, то все одно можливо змінювати об'єкт, на який посилається такий вказівник або посилання в константному методі, без помилки порушення константності. Можна стверджувати, що об'єкт в такому випадку не є цілком незмінним. C++ також забезпечує абстрактну незмінність (на відміну від незмінності даних) за допомогою ключового слова mutable
, що дозволяє змінювати змінну член класу в середині методу, що оголошений як const
.
template<typename T>
class Cart {
private:
std::vector<T> items;
mutable int costInCents;
mutable bool totaled;
public:
Cart(const std::vector<T>& v): items(v), totaled(false) { }
const std::vector<T>& getItems() const { return items; }
int total() const {
if (!totaled) {
costInCents = 0;
for (std::vector<T>::const_iterator itor = items.begin(); itor != items.end(); ++itor)
costInCents += itor->costInCents();
totaled = true;
}
return costInCents;
}
};
D
В мові D існують два класифікатори типу - const
та immutable
- для незмінних об'єктів [3]. На відміну класифікаторів const
(C++), final
(Java) і readonly
(C#), вони є транзитивними і повторно застосовуваними до будь-якого типу, досяжного через посилання такої змінної. Різниця між const
і immutable
полягає в тому, для чого вони використовуються. const
є властивістю змінної: таким чином цілком коректно можуть існувати змінні посилання на згадане значення (referred value), тобто значення може насправді змінитися. На противагу const
, immutable
- це властивість згаданого значення (referred value), тобто значення і все транзитивно досяжне від нього не може змінюватися (без порушення системи типів, що може призвести до невизначеної поведінки). Будь-яке посилання на таке значення має бути позначене const
або immutable
. В основному для будь-якого невизначеного типу T
, const(T)
є непересічним об'єднанням T
(змінний) і immutable(T)
.
class C {
/*змінний*/ Object mField;
const Object cField;
immutable Object iField;
}
Java
Класичним прикладом незмінного об'єкта є екземпляр класу Java String
:
String s = "ABC";
s.toLowerCase();
Метод toLowerCase()
не змінює дані "ABC", що містяться в s
. Замість цього створюється новий об'єкт String, якому присвоюється значення "abc". Посилання на цей об'єкт String повертається методом toLowerCase()
. Для того, щоб String s
містив дані "abc", необхідний інший підхід:
s = s.toLowerCase();
Тепер String s
посилається на новий об'єкт String, який містить "abc". В синтаксисі оголошення класу String немає нічого, що могло б забезпечити його незмінність. Скоріше жоден з методів класу String ніколи не впливав на дані, які містить об'єкт String, що робить його незмінним.
Ключове слово final
використовується для реалізації незмінних примітивних типів і об'єктних посилань [4], але саме по собі воно не може зробити об'єкти незмінними. Наведемо нижче деякі приклади.
Примітивні типи (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, тому не може бути перевизначеним
У загальному випадку незмінний об'єкт може бути створений шляхом визначення класу, який не має жодного з його членів, і не має будь-яких сеттерів. Наступний клас створить незмінний об'єкт:
class ImmutableInt {
private final int value;
public ImmutableInt(int i) {
value = i;
}
public int getValue() {
return value;
}
}
Як видно з наведеного вище прикладу, значення ImmutableInt
може бути встановлено тільки при створенні екземпляра об'єкта і при наявності тільки геттера (getValue
) стан об'єкта не може бути змінено після створення екземпляра.
Однак необхідно стежити за тим, щоб всі об'єкти, на які посилається даний об'єкт, також були незмінними. Наприклад, дозволяючи отримання посилання на масив або ArrayList
, які будуть отримані через геттер, ми можемо допустити зміну внутрішнього стану об’єкта шляхом зміни масиву або колекції:
class NotQuiteImmutableList<T> {
private final List<T> list;
public NotQuiteImmutableList(List<T> list) {
// створює новий ArrayList і зберігає посилання на нього.
this.list = new ArrayList(list);
}
public List<T> getList() {
return list;
}
}
Проблема з наведеним вище кодом полягає в тому, що ArrayList
можна отримати за допомогою getList
і маніпулювати, що призведе до зміни стану самого об'єкта, тому він не є незмінним.
// notQuiteImmutableList містить "a", "b", "c"
List<String> notQuiteImmutableList= new NotQuiteImmutableList(Arrays.asList("a", "b", "c"));
// тепер список містить "a", "b", "c", "d" -- цей список змінний
notQuiteImmutableList.getList().add("d");
Один із способів обійти цю проблему - повернути копію масиву або колекції при виклиці геттера [5]:
public List<T> getList() {
// повертає копію списку, тому внутрішній стан початкового списку не може бути змінено
return new ArrayList(list);
}
JavaScript
У JavaScript деякі вбудовані типи (числа, рядки) незмінні, але користувальницькі об'єкти, як правило, змінюються.
function doSomething(x) { /* чи поміняє х свій первинний стан? */ };
var str = 'a string';
var obj = { an: 'object' };
doSomething(str); // рядки, числа і логічні типи незмінні, функція отримує копію
doSomething(obj); // об’єкти проходять за посиланням і змінні всередині функції
doAnotherThing(str, obj); // `str` не поміняється, але `obj` може помінятись
Щоб імітувати незмінність в об'єкті, властивості можна визначити як read-only
(writable: 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'; // не можна додати властивість, ігнорується
З моменту впровадження React, незмінний стан все частіше використовується в JavaScript-і, що сприяє поширення потоко подібних моделей, таких як Redux [6].
Примітки
- Goetz et al. Java Concurrency in Practice. Addison Wesley Professional, 2006, Section 3.4. Immutability
- Carlos Schults (March 2018). C# Immutable Types: Understanding the Attraction. ndepend.com.
- D Language Specification § 18
- How to create Immutable Class and Object in Java – Tutorial Example. Javarevisited.blogspot.co.uk. 4 березня 2013. Процитовано 14 квітня 2014.
- Неизменяемый класс?. qaru.site.
- Immutability in JavaScript: A Contrarian View. Desalasworks.