В объектно-ориентированном программировании копирование объектов — это создание копии существующего объекта , единицы данных в объектно-ориентированном программировании. Полученный объект называется копией объекта или просто копией исходного объекта. Копирование является базовым, но имеет тонкости и может иметь значительные накладные расходы. Существует несколько способов скопировать объект, чаще всего с помощью конструктора копирования или клонирования . Копирование выполняется в основном для того, чтобы копию можно было изменить или переместить, или сохранить текущее значение. Если что-либо из этого не нужно, достаточно ссылки на исходные данные, которая более эффективна, так как копирование не происходит.
Объекты в общем хранят составные данные . В то время как в простых случаях копирование может быть выполнено путем выделения нового, неинициализированного объекта и копирования всех полей ( атрибутов ) из исходного объекта, в более сложных случаях это не приводит к желаемому поведению.
Цель дизайна большинства объектов — создать видимость того, что они сделаны из одного монолитного блока, хотя большинство из них таковыми не являются. Поскольку объекты состоят из нескольких различных частей, копирование становится нетривиальной задачей. Существует несколько стратегий для решения этой проблемы.
Рассмотрим объект A, содержащий поля x i (конкретнее, рассмотрим, является ли A строкой, а x i — массивом ее символов). Существуют различные стратегии создания копии A, называемые поверхностной копией и глубокой копией . Многие языки допускают обобщенное копирование с помощью одной или обеих стратегий, определяя либо одну операцию копирования , либо отдельные операции поверхностной копии и глубокой копии . [1] Обратите внимание, что еще более поверхностно использовать ссылку на существующий объект A, в этом случае нет нового объекта, только новая ссылка.
Терминология поверхностного копирования и глубокого копирования восходит к Smalltalk -80. [2] То же самое различие справедливо для сравнения объектов на равенство: в основном существует разница между идентичностью (один и тот же объект) и равенством (одинаковое значение), что соответствует поверхностному равенству и (1 уровень) глубокому равенству двух ссылок на объекты, но затем следует вопрос, означает ли равенство сравнение только полей рассматриваемого объекта или разыменование некоторых или всех полей и сравнение их значений по очереди (например, равны ли два связанных списка, если у них одинаковые узлы или если у них одинаковые значения?). [ необходимо разъяснение ]
Одним из методов копирования объекта является поверхностное копирование . В этом случае создается новый объект B , а значения полей A копируются в B. [3] [4] [5] Это также известно как копирование поля за полем , [6] [7] [8] копирование поля для поля или копирование поля . [9] Если значение поля является ссылкой на объект (например, адрес памяти), оно копирует ссылку, следовательно, ссылаясь на тот же объект, что и A, а если значение поля является примитивным типом, оно копирует значение примитивного типа. В языках без примитивных типов (где все является объектом) все поля копии B являются ссылками на те же объекты, что и поля исходного A. Таким образом, объекты, на которые ссылаются, являются общими , поэтому если один из этих объектов изменяется (из A или B), изменение становится видимым в другом. Неглубокие копии просты и, как правило, дешевы, поскольку их обычно можно реализовать простым точным копированием битов.
Альтернативой является глубокое копирование, означающее, что поля разыменовываются: вместо ссылок на копируемые объекты создаются новые копии объектов для любых ссылаемых объектов, а ссылки на них помещаются в B. Последующие изменения содержимого любого из них остаются уникальными для A или B, поскольку содержимое не используется совместно.
В более сложных случаях некоторые поля в копии должны иметь общие значения с исходным объектом (как в поверхностной копии), что соответствует отношению «ассоциации»; а некоторые поля должны иметь копии (как в глубокой копии), что соответствует отношению «агрегации». В этих случаях обычно требуется пользовательская реализация копирования; эта проблема и ее решение относятся к Smalltalk-80. [10] В качестве альтернативы поля могут быть помечены как требующие поверхностной копии или глубокой копии, и операции копирования будут автоматически сгенерированы (аналогично для операций сравнения). [1] Однако это не реализовано в большинстве объектно-ориентированных языков, хотя частичная поддержка имеется в Eiffel. [1]
Почти все объектно-ориентированные языки программирования предоставляют некоторые способы копирования объектов. Поскольку большинство языков не предоставляют большинство объектов для программ, программист должен определить, как объект должен быть скопирован, так же как он должен определить, являются ли два объекта идентичными или даже сравнимыми в первую очередь. Многие языки предоставляют некоторое поведение по умолчанию.
То, как решается проблема копирования, и какое понятие об объекте оно имеет, различается в разных языках.
Ленивое копирование — это реализация глубокого копирования. При первоначальном копировании объекта используется (быстрое) поверхностное копирование. Счетчик также используется для отслеживания того, сколько объектов совместно используют данные. Когда программа хочет изменить объект, она может определить, являются ли данные общими (проверив счетчик), и может выполнить глубокое копирование, если необходимо.
Ленивое копирование выглядит снаружи как глубокое копирование, но использует скорость поверхностного копирования, когда это возможно. Недостатком являются довольно высокие, но постоянные базовые затраты из-за счетчика. Кроме того, в определенных ситуациях циклические ссылки могут вызывать проблемы.
Ленивое копирование связано с копированием при записи .
Ниже приведены примеры для одного из наиболее широко используемых объектно-ориентированных языков, Java , которые должны охватывать практически все способы, которыми объектно-ориентированный язык может решать эту проблему.
В отличие от C++, к объектам в Java всегда осуществляется косвенный доступ через ссылки . Объекты никогда не создаются неявно, а вместо этого всегда передаются или назначаются ссылочной переменной. (Методы в Java всегда передаются по значению , однако передается значение ссылочной переменной.) [11] Виртуальная машина Java управляет сборкой мусора , так что объекты очищаются после того, как они становятся недоступными. В Java нет автоматического способа скопировать любой заданный объект.
Копирование обычно выполняется методом clone() класса. Этот метод обычно, в свою очередь, вызывает метод clone() своего родительского класса для получения копии, а затем выполняет любые пользовательские процедуры копирования. В конце концов это доходит до метода clone() Object
(самого верхнего класса), который создает новый экземпляр того же класса, что и объект, и копирует все поля в новый экземпляр («поверхностная копия»). Если используется этот метод, класс должен реализовать Cloneable
интерфейс маркера, иначе он выдаст «Clone Not Supported Exception». После получения копии из родительского класса собственный метод clone() класса может затем предоставить пользовательскую возможность клонирования, например глубокое копирование (т. е. дублирование некоторых структур, на которые ссылается объект) или присвоение новому экземпляру нового уникального идентификатора.
Возвращаемый тип clone() — Object
, но реализаторы метода clone могли бы вместо этого написать тип клонируемого объекта из-за поддержки Java ковариантных возвращаемых типов . Одним из преимуществ использования clone() является то, что, поскольку это переопределяемый метод , мы можем вызвать clone() для любого объекта, и он будет использовать метод clone() его класса, без необходимости вызывающему коду знать, какой это класс (что было бы необходимо при использовании конструктора копирования).
Недостатком является то, что часто невозможно получить доступ к методу clone() для абстрактного типа. Большинство интерфейсов и абстрактных классов в Java не определяют публичный метод clone(). Таким образом, часто единственный способ использовать метод clone() — это если класс объекта известен, что противоречит принципу абстракции использования максимально возможного общего типа. Например, если у вас есть ссылка List в Java, вы не можете вызвать clone() для этой ссылки, потому что List не определяет публичный метод clone(). Реализации List, такие как Array List и Linked List, как правило, имеют методы clone(), но это неудобно и плохая абстракция, чтобы переносить тип класса объекта.
Другой способ копирования объектов в Java — сериализация их через Serializable
интерфейс. Обычно это используется для целей сохранения и протокола проводов , но это создает копии объектов и, в отличие от клонирования, глубокая копия, которая изящно обрабатывает циклические графики объектов, легко доступна с минимальными усилиями со стороны программиста.
Оба эти метода страдают от заметной проблемы: конструктор не используется для объектов, скопированных с помощью клонирования или сериализации. Это может привести к ошибкам с неправильно инициализированными данными, препятствует использованию final
полей-членов и усложняет обслуживание. Некоторые утилиты пытаются преодолеть эти проблемы, используя отражение для глубокого копирования объектов, например, библиотека глубокого клонирования. [12]
Объекты времени выполнения в Eiffel доступны либо косвенно через ссылки , либо как расширенные объекты, поля которых встроены в объекты, которые их используют. То есть поля объекта хранятся либо внешне, либо внутренне .
Класс Eiffel ANY
содержит функции для поверхностного и глубокого копирования и клонирования объектов. Все классы Eiffel наследуются от ANY
, поэтому эти функции доступны во всех классах и применимы как к ссылочным, так и к расширенным объектам.
Функция copy
производит поверхностное копирование поля за полем из одного объекта в другой. В этом случае новый объект не создается. Если y
были скопированы в x
, то те же объекты, на которые ссылался y
до применения copy
, также будут ссылаться x
после copy
завершения функции.
Для создания нового объекта, который является поверхностным дубликатом y
, используется функция twin
. В этом случае создается один новый объект с полями, идентичными полям источника.
Функция twin
опирается на функцию copy
, которая может быть переопределена в потомках ANY
, если это необходимо. Результат twin
имеет закрепленный тип like Current
.
Глубокое копирование и создание глубоких близнецов можно выполнить с помощью функций deep_copy
и deep_twin
, снова унаследованных от класса ANY
. Эти функции имеют потенциал для создания множества новых объектов, поскольку они дублируют все объекты во всей структуре объектов. Поскольку создаются новые дублирующие объекты вместо простого копирования ссылок на существующие объекты, глубокие операции станут источником проблем с производительностью скорее, чем поверхностные операции.
В C# вместо использования интерфейса ICloneable
можно использовать универсальный метод расширения для создания глубокой копии с использованием отражения. Это имеет два преимущества: во-первых, это обеспечивает гибкость для копирования каждого объекта без необходимости указывать каждое свойство и переменную для копирования вручную. Во-вторых, поскольку тип является универсальным, компилятор гарантирует, что целевой объект и исходный объект имеют одинаковый тип.
В Objective-C методы copy
и mutableCopy
наследуются всеми объектами и предназначены для выполнения копий; последний предназначен для создания изменяемого типа исходного объекта. Эти методы в свою очередь вызывают методы copyWithZone
и mutableCopyWithZone
, соответственно, для выполнения копирования. Объект должен реализовать соответствующий copyWithZone
метод, чтобы быть копируемым.
В OCaml библиотечная функция Oo.copy выполняет поверхностное копирование объекта.
В Python модуль копирования библиотеки обеспечивает поверхностное и глубокое копирование объектов с помощью функций copy()
и deepcopy()
соответственно. [13] Программисты могут определять специальные методы __copy__()
и __deepcopy__()
в объекте для предоставления пользовательской реализации копирования.
В Ruby все объекты наследуют два метода для выполнения поверхностных копий, clone и dup. Два метода отличаются тем, что clone
копируют испорченное состояние объекта, замороженное состояние и любые методы singletondup
, которые он может иметь, тогда как копируют только испорченное состояние. Глубокие копии могут быть получены путем дампа и загрузки потока байтов объекта или сериализации YAML.[1] В качестве альтернативы вы можете использовать gem deep_dive для выполнения контролируемой глубокой копии ваших графов объектов.[2]
В Perl вложенные структуры хранятся с помощью ссылок, поэтому разработчик может либо обойти всю структуру и повторно ссылаться на данные, либо использовать dclone()
функцию из модуля Storable.
В VBA назначение переменных типа Object
является поверхностной копией, назначение для всех других типов (числовых типов, String, пользовательских типов, массивов) является глубокой копией. Таким образом, ключевое слово Set
для назначения сигнализирует о поверхностной копии, а (необязательное) ключевое слово Let
сигнализирует о глубокой копии. Встроенного метода для глубоких копий объектов в VBA нет.
shallowCopy
); если значения копируются, то они не являются общими ( deepCopy
).»copy
— shallowCopy
. В подклассах, в которых копирование должно приводить к особой комбинации общих и не общих переменных, метод, связанный с копией, обычно реализуется повторно, а не метод, связанный с shallowCopy
или deepCopy
».