Язык C# позволяет клонировать простой объект или граф объектов немалым количеством способов, каждый из которых обладает своими достоинствами и недостатками.
- Интерфейс ICloneable
- Собственный интерфейс
- Бинарная сериализация
- XML сериализация
- Конструктор копирования
Все последующие примеры будем разбирать на двух простых классах, которые выглядят так:
public class Person { public string Name { get; set; } public int Age { get; set; } public Address Address { get; set; } } public class Address { public string City { get; set; } public string Street { get; set; } }
Напомню, что в .NET существует 2 вида клонирования объекта: поверхностное (shallow) и глубокое (deep). При поверхностном клонированный объект получает свою собственную копию только значимых типов, но не ссылочных. Для создания поверхностной копии предназначен метод MemberwiseClone класса Object. Разницу между поверхностным и глубоким клонированием схематически можно представить так:
Интерфейс ICloneable
Начнем с интерфейса ICloneable, который предоставляется платформой .NET. Реализуем его на нашем графе объектов, используя метод MemberwiseClone внутри, который создает поверхностную копию объекта.
public class Person : ICloneable { public string Name { get; set; } public int Age { get; set; } public Address Address { get; set; } public object Clone() { var person = (Person)MemberwiseClone(); person.Address = (Address)Address.Clone(); return person; } } public class Address : ICloneable { public string City { get; set; } public string Street { get; set; } public object Clone() { return MemberwiseClone(); } }
Плюсы интерфейса ICloneable:
- Предоставляется платформой .NET. Нет необходимости определять свой собственный интерфейс.
- Дает полный контроль над клонированием объектов графа. Иногда может потребоваться не клонировать все объекты подряд по цепочке. Представим, что наш класс Person включает в себя еще одно свойство с типом AccountHistory. Логично предположить, что если мы клонируем объект Person, то новый объект не нуждается в копии истории предыдущего. Такое требование выражается в коде следующим образом:
public class Person : ICloneable { public string Name { get; set; } public int Age { get; set; } public Address Address { get; set; } public AccountHistory AccountHistory { get; set; } public object Clone() { var person = (Person)MemberwiseClone(); person.Address = (Address)Address.Clone(); person.AccountHistory = new AccountHistory(); //просто наделяем клон "чистым" объектом return person; } }
Минусы интерфейса ICloneable:
- Возвращает тип Object. Вызывающий код должен постоянно выполнять приведение к конкретному типу.
- Имя интерфейса или метода не дают информации о том, какая копия объекта создается: глубокая или поверхностная. Приходится изучать имплементацию.
- Необходимо реализовать интерфейс на каждом классе, который берет участие в процессе клонирования.
Собственный интерфейс
Альтернативой интерфейсу ICloneable может быть собственный обобщенный интерфейс ICopier, который устранит сразу несколько недостатков предшественника.
public interface ICopier<T> { T DeepClone(); } public class Person : ICopier<Person> { public string Name { get; set; } public int Age { get; set; } public Address Address { get; set; } public Person DeepClone() { var person = (Person)MemberwiseClone(); person.Address = Address.DeepClone(); return person; } } public class Address : ICopier<Address> { public string City { get; set; } public string Street { get; set; } public Address DeepClone() { return (Address)MemberwiseClone(); } }
Плюсы:
- Имя метода интерфейса несет в себе информацию о типе выполняемого клонирования.
- Клиент класса Person не должен заниматься приведением типов, так как интерфейс является обобщенным.
- Дает полный контроль над клонированием каждого объекта в графе.
Минусы:
- Необходимо реализовавыть интерфейс на каждом классе, который берет участие в процессе клонирования.
Бинарная сериализация
Другим способом клонирования объектов без необходимости реализации различных интерфейсов является сериализация. Идея в том, что мы можем сериализовать граф объектов в память, а потом десериализовать ее обратно. Начнем с бинарной сериализации.
public static class Helper { public static T DeepClone<T>(T obj) { using (var ms = new MemoryStream()) { IFormatter formatter = new BinaryFormatter(); formatter.Serialize(ms, obj); ms.Seek(0, SeekOrigin.Begin); return (T)formatter.Deserialize(ms); } } }
Плюсы:
- Нет необходимости реализовывать интерфейс на каждом классе. Вся логика клонирования графа любого размера находится в одном месте.
Минусы:
- Класс Person и все влюженные классы должны быть отмечены атрибутом [Serializable], иначе метод Serialize будет генерировать исключение.
- Отсутствует гибкость как в случае с интерфейсами. Можем клонировать только все объекты без исключения.
XML сериализация
Один из недостатков бинарной сериализации, а именно атрибут [Serializable], можно попробовать устранить сериализацией в XML.
public static class Helper { public static T DeepClone<T>(T obj) { using (var ms = new MemoryStream()) { XmlSerializer serializer = new XmlSerializer(obj.GetType()); serializer.Serialize(ms, obj); ms.Seek(0, SeekOrigin.Begin); return (T)serializer.Deserialize(ms); } } }
XML сериализация в отличие от бинарной уже не требует атрибута [Serializable] над каждым классом, но требует другого — класс Person и все вложенные должны иметь конструктор без параметров. На первый взгляд такое требование может показаться безобидным, но конструктор без параметром для типа Person означает, что человек может быть создан без имени и без всего остального. Да, свойства объекта можно проинициализировать и после создания (если не забыть), но в таком случае в промежутке между созданием объекта и инициализацией его свойств, объект будет находиться в невалидном состоянии.
Конструктор копирования
Конструктор копирования (copy constructor) — конструкция для клонирования объектов, характерна для С++ и малоиспользуема в С#. Представляет собой обычный конструктор, который принимает на вход тип класса, в котором он объявлен и выполняет инициализацию всех его членов.
public class Person { public Person(Person person) { Name = person.Name; Age = person.Age; Address = new Address(person.Address); } public string Name { get; set; } public int Age { get; set; } public Address Address { get; set; } } public class Address { public Address(Address address) { City = address.City; Street = address.Street; } public string City { get; set; } public string Street { get; set; } }
Плюсы:
- Полный контроль над клонированием всего графа объектов.
Минусы:
- Необходимо реализовать конструктор копирования в каждом классах графа.
- Программист, впервые столкнувшись с подобным подходом, поначалу может только строить предположения о предназначении конструктора.
Заключение
Реализация интерфейсов и конструктора копирования предоставляет разработчику полный контроль над процессом клонирования, в то время как клонирование при помощи сериализации/десериализации избавляет от необходимости вносить изменения в логику клонирования всякий раз при добавлении/удалении/изменении новых свойств в клонируемые классы. Единственного и идеального решения не существует. Проанализировав плюсы и минусы каждого подходы, можно выбрать один, который больше всего подходит в своем конкретном случае.