Введение
Методы копирования объекта в объектно-ориентированном программировании
В объектно-ориентированном программировании копирование объекта — это создание и инициализация нового объекта на основе состояния существующего объекта. Различные способы реализации копирования имеют последствия, которые программисту необходимо понимать для написания корректной и производительной компьютерной программы. Копирование позволяет использовать и даже изменять текущее состояние исходного объекта, представленное его внутренним состоянием, не затрагивая исходный объект.
Стратегии
Как правило, объект представляется монолитным понятием, хотя имеет внутреннюю структуру, состоящую из составных данных – дерева состояний. Для копирования этого внутреннего состояния было разработано несколько стратегий, основанных на потребностях программы и затратах времени выполнения. Первыми рассматривались поверхностное и глубокое копирование, терминология для которых восходит к Smalltalk 80. Аналогичное различие применимо и к сравнению объектов на равенство. Чтобы два объекта считались равными, их состояние должно быть одинаковым в значимом смысле. Два объекта могут считаться равными, если равны их поля, без углубленного сравнения вложенных объектов (поверхностное). Или же они считаются равными только в том случае, если состояние одинаково во всем дереве объектов (глубокое). Если две переменные содержат одно и то же значение ссылки, то они, очевидно, ссылаются на один и тот же объект, что является более строгим условием, чем просто равенство.
Справочный экземпляр
Копирование ссылки – это даже более поверхностный вид копирования, чем поверхностное копирование, и является формой копирования объекта. Эта стратегия часто используется при передаче объекта в метод. Ссылка передается по значению – копируется значение ссылки (вероятно, адрес).
Глубокая копия
Глубокое копирование подразумевает копирование состояния всех подчиненных объектов, рекурсивное разрешение ссылок на объекты на каждом уровне дерева, представляющего состояние исходного объекта, и создание новых объектов с копированием полей. Изменение либо исходного, либо скопированного объекта, включая их внутренние объекты, не влияет на другой, поскольку они не разделяют общие данные.
Гибридный
В более сложных случаях некоторые поля в копии должны иметь общие значения с исходным объектом (как при поверхностном копировании), что соответствует отношению ассоциации, а другие поля должны иметь собственные копии (как при глубоком копировании), что соответствует отношению агрегации. В таких случаях обычно требуется собственная реализация копирования; эта проблема и её решение восходят к Smalltalk 80. Альтернативно, поля могут быть помечены как требующие поверхностного или глубокого копирования, и операции копирования генерируются автоматически (аналогично для операций сравнения). Однако это не реализовано в большинстве объектно-ориентированных языков, хотя в Eiffel имеется частичная поддержка.
Ленивая копия
Ленивое копирование, связанное с копированием при записи, является реализацией глубокого копирования. При первоначальном копировании объекта выполняется относительно быстрая поверхностная копия. Также используется счетчик для отслеживания количества объектов, совместно использующих данные. Когда программа хочет изменить объект, она может определить, совместно ли используются данные (проверяя счетчик) и выполнить глубокое копирование, если это необходимо. Ленивое копирование обеспечивает семантику глубокого копирования, но использует скорость поверхностного копирования, когда это возможно. Недостатком являются относительно высокие, но постоянные накладные расходы, связанные со счетчиком. Циклические ссылки могут вызывать проблемы.
Примеры
Обычно объектно-ориентированный язык программирования предоставляет способ копирования объекта. Программист должен определить, как копировать пользовательский объект, так же как он должен определить, равны ли два объекта, сравнимы ли они и так далее. Некоторые языки поддерживают одну из стратегий – поверхностного или глубокого копирования, определяя либо одну операцию копирования, либо отдельные операции поверхностного и глубокого копирования. Многие языки предоставляют некоторое поведение по умолчанию.
Ява
В Java к объекту всегда осуществляется доступ косвенно через ссылку. Объект никогда не создается неявно, а вместо этого всегда передается или присваивается ссылочной переменной. Параметры передаются по значению, однако передается значение ссылки. Виртуальная машина Java управляет сборкой мусора, чтобы объекты очищались после того, как они становятся недостижимыми. Язык не предоставляет автоматического способа копирования объекта. Копирование обычно выполняется методом `clone`. Этот метод обычно вызывает метод `clone` своего родительского класса для получения копии, а затем выполняет любые пользовательские процедуры копирования. В конечном итоге, это приводит к методу `clone` базового объекта (Object), который создает новый экземпляр того же класса, что и исходный объект, и копирует все поля в новый экземпляр (поверхностная копия). Если этот метод используется, класс должен реализовать интерфейс `Cloneable`, иначе будет выброшено исключение `CloneNotSupportedException`. После получения копии от родительского класса, собственный метод `clone` класса может обеспечить возможность пользовательского клонирования, например, глубокого копирования (т.е. дублирования некоторых структур, на которые ссылается объект) или предоставления новому экземпляру нового уникального идентификатора. Тип возвращаемого значения метода `clone` – `Object`, но реализаторы метода `clone` могут указать тип клонируемого объекта благодаря поддержке ковариантных типов возвращаемых значений в Java. Одним из преимуществ использования `clone` является то, что, поскольку это переопределяемый метод, его можно вызвать для любого объекта, и будет использован метод `clone` его класса, без необходимости, чтобы вызывающий код знал, что это за класс (что потребовалось бы при использовании копирующего конструктора). Недостатком является то, что часто нельзя получить доступ к методу `clone` для абстрактного типа. Большинство интерфейсов и абстрактных классов в Java не определяют публичный метод `clone`. Таким образом, часто единственный способ использовать метод `clone` – если известен класс объекта, что противоречит принципу абстракции, заключающемуся в использовании максимально общего типа. Например, если у вас есть ссылка `List` в Java, вы не можете вызвать `clone` для этой ссылки, потому что интерфейс `List` не определяет публичный метод `clone`. Реализации `List`, такие как `ArrayList` и `LinkedList`, обычно имеют методы `clone`, но неудобно и нарушает принципы абстракции хранить информацию о типе класса объекта. Другой способ копировать объекты в Java – сериализовать их через интерфейс `Serializable`. Это обычно используется для сохранения данных и сетевого взаимодействия, но это создает копии объектов и, в отличие от `clone`, глубокая копия, которая корректно обрабатывает циклические ссылки между объектами, легко доступна с минимальными усилиями со стороны программиста. Оба этих метода имеют одну существенную проблему: конструктор не используется для объектов, скопированных с помощью `clone` или сериализации. Это может привести к ошибкам из-за неправильно инициализированных данных, препятствует использованию финальных полей и затрудняет поддержку кода. Некоторые утилиты пытаются решить эти проблемы, используя рефлексию для глубокого копирования объектов, например, библиотека глубокого клонирования.
Эйфель
Объекты времени выполнения в Eiffel доступны либо косвенно через ссылки, либо как расширенные объекты, поля которых встроены в объекты, использующие их. Таким образом, поля объекта хранятся либо во внешней памяти, либо непосредственно в самом объекте. Класс Eiffel ANY содержит средства для поверхностного и глубокого копирования и клонирования объектов. Все классы Eiffel наследуются от ANY, поэтому эти средства доступны во всех классах и применимы как к объектам, доступным по ссылке, так и к расширенным объектам. Средство копирования выполняет поверхностное копирование, поле за полем, из одного объекта в другой. В этом случае новый объект не создается. Если y скопировать в x, то объекты, на которые ссылалась y до применения копирования, будут также доступны через x после завершения операции копирования. Для создания нового объекта, являющегося поверхностной копией y, используется средство twin (двойник). В этом случае создается один новый объект, поля которого идентичны полям исходного объекта. Средство twin использует средство copy (копирование), которое может быть переопределено в классах-потомках ANY при необходимости. Результат twin имеет анкорный тип, как Current. Глубокое копирование и создание глубоких двойников можно выполнить с помощью средств deep copy (глубокое копирование) и deep twin (глубокий двойник), также унаследованных от класса ANY. Эти средства потенциально могут создавать множество новых объектов, поскольку они дублируют все объекты в целой объектной структуре. Поскольку создаются новые дубликаты объектов, а не просто копируются ссылки на существующие объекты, глубокие операции могут стать причиной проблем с производительностью легче, чем поверхностные.
Язык
В C#, вместо использования интерфейса ICloneable, можно использовать универсальный метод расширения для создания глубокой копии с помощью рефлексии. Это дает два преимущества: во-первых, появляется возможность копировать любой объект, не указывая вручную каждое свойство и поле для копирования. Во-вторых, благодаря тому, что тип является универсальным, компилятор гарантирует, что целевой объект и исходный объект имеют одинаковый тип.
Цель-С
В Objective C методы `copy` и `mutableCopy` наследуются всеми объектами и предназначены для создания копий; последний используется для создания изменяемой копии исходного объекта. Эти методы, в свою очередь, вызывают методы `copyWithZone` и `mutableCopyWithZone` соответственно, для выполнения операции копирования. Объект должен реализовать метод `copyWithZone`, чтобы его можно было копировать.
ОКамм
В OCaml библиотечная функция Oo.copy выполняет поверхностное копирование объекта.
Python (англ.)
В Python модуль `copy` библиотеки предоставляет возможность создания поверхностных и глубоких копий объектов с помощью функций `copy` и `deepcopy` соответственно. Программисты могут определить специальные методы `__copy__` и `__deepcopy__` в объекте для реализации пользовательской логики копирования.
Рубины
В Ruby все объекты наследуют два метода для выполнения поверхностных копий: `clone` и `dup`. Эти два метода различаются тем, что `clone` копирует "запятнанное" состояние объекта, замороженное состояние и любые одиночные методы, которые у него могут быть, в то время как `dup` копирует только его "запятнанное" состояние. Глубокие копии можно получить, сбрасывая и загружая байтовый поток объекта или используя сериализацию YAML. В качестве альтернативы, можно использовать gem `deep_dive` для контролируемого глубокого копирования графов объектов.
Перл
В Perl вложенные структуры хранятся посредством использования ссылок, поэтому разработчик может либо последовательно перебрать всю структуру и переназначить ссылки на данные, либо воспользоваться функцией dclone из модуля Storable.
ВВА
В VBA присваивание переменных типа Object является поверхностной копией, а присваивание для всех остальных типов (числовые типы, String, пользовательские типы, массивы) – глубокой копией. Следовательно, ключевое слово Set при присваивании указывает на поверхностную копию, а (необязательное) ключевое слово Let – на глубокую копию. Встроенного метода для создания глубоких копий объектов в VBA нет.