Копіювання об'єктів

Матеріал з Вікіпедії — вільної енциклопедії.
Перейти до навігації Перейти до пошуку

В об'єктно-орієнтованому програмуванні копіювання об'єкта — це створення копії існуючого об'єкта, одиниці даних в об'єктно-орієнтованому програмуванні. Результуючий об'єкт називається копією об'єкта або просто копією. Копіювання є базовою операцією, але має тонкощі та може мати значні накладні витрати. Є кілька способів скопіювати об'єкт, найчастіше за допомогою конструктора копіювання або клонування. Копіювання здебільшого виконується для того, щоб копію можна було змінити або перемістити, або зберегти поточне значення. Якщо жодна з цих дій непотрібна, то посилання на оригінальні дані є достатнім та більш ефективним, оскільки копіювання не відбувається.

Об'єкти зазвичай зберігають складені дані. У простих випадках копіювання можна виконати шляхом виділення нового неініціалізованого об'єкта та копіювання всіх полів (атрибутів) з оригінального об'єкта, у складніших випадках це не призводить до бажаної поведінки.

Способи копіювання[ред. | ред. код]

Мета проектування більшості об'єктів полягає в тому, щоб створити схожість із монолітним блоком (хоча при цьому більшість з них не є такими). Оскільки об'єкти складаються з кількох різних частин, копіювання стає нетривіальним. Існує кілька стратегій вирішення цієї проблеми.

Розглянемо об'єкт A, який містить поля xi (точніше, розглянемо, що A є рядком, а xi — масивом його символів). Існують різні стратегії створення копії A, які називаються поверхневою (простою) копією та глибокою копією. Багато мов програмування дозволяють загальне копіювання за допомогою тільки однієї або однієї з декількох стратегії, визначаючи або одну операцію копіювання, або окремі операції поверхневого копіювання та глибокого копіювання. [1] Зауважте, що використання посилання на вже існуючий об'єкт A ще більш поверхнево, адже в цьому випадку взагалі немає ніякого нового об'єкта, а є лише нове посилання.

Термінологія поверхневої копії та глибокої копії відноситься до Smalltalk-80.[2] Та ж сама відмінність має місце для порівняння об'єктів на предмет рівності: існує різниця між ідентичністю (той самий об'єкт) і рівністю (ті ж самі однакові значення), що відповідає поверхневій рівності та глибокій рівності (першого рівня) двох посилань на об'єкти. Але потім постає питання, чи означає рівність 1) порівняння лише полів відповідного об'єкта чи 2) розіменування деяких або всіх полів і порівняння їхніх значень по черзі (наприклад, чи є два зв'язані списки рівними, якщо вони мають однакові вузли чи однакові значення?). 

Поверхнева (проста) копія[ред. | ред. код]

Одним із методів копіювання об'єкта є поверхневе копіювання. У цьому випадку створюється новий об'єкт B, а значення полів A копіюються в B.[3][4][5] Це також відомо як копія по полях, або пополева копія.[6] Якщо значення поля є посиланням на об'єкт (наприклад, адресу пам'яті), воно копіює посилання, отже посилаючись на той самий об'єкт, що й A, а якщо значення поля є примітивним типом, воно копіює значення примітивного типу. У мовах без примітивних типів (де все є об'єктом), усі поля копії B є посиланнями на ті самі об'єкти, що й поля оригінального A. Таким чином, об'єкти, на які посилаються, є спільними, тому якщо один із цих об'єктів змінено (A або B), зміни видно в іншому. Поверхневі копії прості та зазвичай дешеві, оскільки зазвичай їх можна реалізувати, просто точно копіюючи біти.

Глибока копія[ред. | ред. код]

Глибока копія в процесі.
Глибока копія в процесі.
Глибока копія в результаті.
Глибока копія в результаті.

Глибока копія означає, що поля розіменовуються: замість посилань на об'єкти, що копіюються, створюються нові об'єкти копії для будь-яких об'єктів, на які посилаються, а посилання на них розміщуються в B. Результат відрізняється від результату, який дає поверхнева копія тим, що об'єкти, на які посилається копія B, відрізняються від тих, на які посилається A, і незалежні. Глибокі копії дорожчі через необхідність створення додаткових об'єктів і можуть бути значно складнішими через посилання, які можуть утворювати складний граф.

Глибоке копіювання — це процес, у якому процес копіювання відбувається рекурсивно. Це означає спочатку створення нового об'єкта колекції, а потім рекурсивне заповнення його копіями дочірніх об'єктів, знайдених в оригіналі. У разі глибокого копіювання копія об'єкта копіюється в інший об'єкт. Це означає, що будь-які зміни, внесені до копії об'єкта, не відображаються на оригінальному об'єкті. У Python це реалізовано за допомогою функції «deep copy()».

Комбінація[ред. | ред. код]

У більш складних випадках деякі поля в копії повинні мати спільні значення з оригінальним об'єктом (як у поверхневій копії), що відповідає зв'язку «асоціації»; і деякі поля повинні мати копії (як у глибокій копії), що відповідає зв'язку «агрегація». У цих випадках зазвичай потрібна спеціальна реалізація копіювання; ця проблема та рішення датуються Smalltalk-80. [7] Крім того, поля можна позначити як такі, що вимагають поверхневої або глибокої копії, а операції копіювання генерувати автоматично (аналогічно і операції порівняння). [1] Однак це не реалізовано в більшості об'єктно-орієнтованих мов, хоча в Eiffel є часткова підтримка. [1]

Реалізація[ред. | ред. код]

Майже всі об'єктно-орієнтовані мови програмування надають певний спосіб копіювання об'єктів. Оскільки більшість мов не забезпечують більшості об'єктів для програм, програміст повинен самостійно визначити, як саме об'єкт слід копіювати, а також визначити, чи є два об'єкти ідентичними або чи можна їх навіть порівнювати. Багато мов програмування забезпечують певну поведінку за замовчуванням.

Спосіб розв'язання копіювання залежить від конкретної мови, а також від концепція об'єкта в неї.

Лінива копія[ред. | ред. код]

Лінива копія — це специфічна реалізація глибокої копії. Під час початкового копіювання об'єкта використовується (швидка) поверхнева копія. При цьому також додатково використовується спеціальний лічильник для відстеження кількості об'єктів, які спільно використовують дані. Коли програма хоче змінити об'єкт, вона може визначати, чи є дані спільними (перевіривши лічильник), і за потреби може зробити глибоку копію.

Лінива копія виглядає зовні так само, як глибока копія, але використовує (за можливості) швидкість поверхневої копії. Недоліком є досить високі, але постійні базові накладні витрати через лічильник. Крім того, у певних ситуаціях можуть спричинити проблеми так звані циклічні посилання.

Відкладене копіювання пов'язане з копіюванням при записуванні.

В Java[ред. | ред. код]

Далі наведено приклади для однієї з найпоширеніших об'єктно-орієнтованих мов — Java, які мають охоплювати майже всі способи вирішення цієї проблеми.

На відміну від C++, доступ до об'єктів у Java завжди здійснюється непрямо — опосередковано через посилання. Об'єкти ніколи не створюються неявно, натомість вони завжди передаються або призначаються змінною-посиланням.Віртуальна машина Java керує збиранням сміття, щоб об'єкти очищалися після того, як вони стали недоступними. У Java немає автоматичного способу копіювання будь-якого заданого об'єкта.

Копіювання зазвичай виконується методом clone() класу. Цей метод, у свою чергу, викликає метод clone() свого батьківського класу, щоб отримати копію, а потім виконує будь-які індивідуальні процедури копіювання. Зрештою це приводить до методу clone() найвищого класу Object — методу, який створює новий екземпляр того самого класу, що й об'єкт, а також копіює всі поля до нового екземпляра («поверхнева копія»). Якщо використовується цей метод копіювання, клас повинен реалізовувати інтерфейс Cloneable, інакше він викличе виключення CloneNotSupportedException. Після отримання копії від батьківського класу власний метод класу clone() може надати можливість спеціального клонування (наприклад, глибоке копіювання для дублювання деяких структур, на які посилається об'єкт) або надати новому екземпляру новий унікальний ідентифікатор.

Типом повернення clone() є Object, але реалізатори методу clone() натомість можуть записати тип об'єкта, який клонується, завдяки підтримці Java коваріантних типів повернення. Одна з переваг використання clone() полягає в тому, що, оскільки це метод, який можна заміщати (overridable method), ми можемо викликати clone() для будь-якого об'єкта, і він використовуватиме метод clone() свого класу.

Недоліком клонування є те, що часто неможливо отримати доступ до методу clone() абстрактного типу. Більшість інтерфейсів і абстрактних класів у Java не специфікують загальнодоступний метод clone(). Таким чином, часто єдиним способом використання методу clone() є відомий клас об'єкта, що суперечить принципу абстракції використання максимально узагальненого типу. Наприклад, якщо в Java є посилання на List, то не можна викликати clone() для цього посилання, оскільки List не специфікує загальнодоступний метод clone(). Реалізації List, наприклад, ArrayList або LinkedList, зазвичай мають методи clone(), але постійно обробляти тип класу об'єкта незручно і то є поганою абстракцією.

Ще один спосіб скопіювати об'єкти в Java — серіалізувати за допомогою інтерфейса Serializable. Це створює копії об'єктів, і, на відміну від клонування, така глибока копія, що витончено обробляє циклічні графи об'єктів, легко доступна з мінімальними зусиллями з боку програміста.

Обидва ці методи мають одну проблему: конструктор не використовується для об'єктів, що були скопійовані за допомогою клонування або серіалізації. Це може призвести до помилок із неправильно ініціалізованими даними, перешкоджає використанню final полів члена, а також ускладнює обслуговування.

В інших мовах[ред. | ред. код]

У мові C# замість використання інтерфейсу ICloneable можна використовувати узагальнений метод розширення для створення глибокої копії за допомогою відображення. Цей підхід має дві переваги. По-перше, це забезпечує гнучкість копіювання кожного об'єкта без необхідності специфікувати кожну властивість і змінну для копіювання вручну. По-друге, оскільки тип є узагальненим, компілятор гарантує, що цільовий об'єкт і оригінальний об'єкт мають однаковий тип.

У мові Objective-C спеціалізовані методи copy та mutableCopy успадковуються всіма об'єктами, при цьому mutableCopy призначений для створення змінного типу оригінального об'єкта. Ці методи, у свою чергу, викликають методи copyWithZone і mutableCopyWithZone, відповідно, власне для виконання копіювання. Об'єкт має реалізувати відповідний метод copyWithZone для того, щоб його можна було копіювати.

У мові Python модуль копіювання бібліотеки забезпечує поверхневе копіювання та глибоке копіювання об'єктів за допомогою функцій copy() та deepcopy() відповідно.[8] Програміст може визначати спеціальні методи __copy__() і __deepcopy__() в об'єкті для того, щоб забезпечити свою власну реалізацію копіювання.

Див. також[ред. | ред. код]

Примітки[ред. | ред. код]

  1. а б в Grogono та Sakkinen, 2000.
  2. Goldberg та Robson, 1983, с. 97–99.
  3. C++ Shallow vs Deep Copy Explanation. Архів оригіналу за 10 лютого 2014. Процитовано 30 січня 2023.
  4. .NET Shallow vs Deep Copy Explanation.
  5. Generic Shallow vs Deep Copy Explanation. Архів оригіналу за 4 березня 2016. Процитовано 10 квітня 2013.
  6. «Josh Bloch on Design: A Conversation with Effective Java Author, Josh Bloch», by Bill Venners, JavaWorld, January 4, 2002, p. 13
  7. Goldberg та Robson, 1983, с. 97.
  8. Python copy module