Обзор способов клонирования графа объектов в .NET

Язык C# позволяет клонировать простой объект или граф объектов немалым количеством способов, каждый из которых обладает своими достоинствами и недостатками.

Все последующие примеры будем разбирать на двух простых классах, которые выглядят так:

public class Person
{
    public string Name { getset; }
    public int Age { getset; }
    public Address Address { getset; }
}

public class Address
{
    public string City { getset; }
    public string Street { getset; }
}

Напомню, что в .NET существует 2 вида клонирования объекта: поверхностное (shallow) и глубокое (deep). При поверхностном клонированный объект получает свою собственную копию только значимых типов, но не ссылочных. Для создания поверхностной копии предназначен метод MemberwiseClone класса Object. Разницу между поверхностным и глубоким клонированием схематически можно представить так:

clone

Интерфейс ICloneable

Начнем с интерфейса ICloneable, который предоставляется платформой .NET. Реализуем его на нашем графе объектов, используя метод MemberwiseClone внутри, который создает поверхностную копию объекта.

public class Person : ICloneable
{
    public string Name { getset; }
    public int Age { getset; }
    public Address Address { getset; }

    public object Clone()
    {
        var person = (Person)MemberwiseClone();
        person.Address = (Address)Address.Clone();
        return person;
    }
}

public class Address : ICloneable
{
    public string City { getset; }
    public string Street { getset; }

    public object Clone()
    {
        return MemberwiseClone();
    }
}

Плюсы интерфейса ICloneable:

  • Предоставляется платформой .NET. Нет необходимости определять свой собственный интерфейс.
  • Дает полный контроль над клонированием объектов графа. Иногда может потребоваться не клонировать все объекты подряд по цепочке. Представим, что наш класс Person включает в себя еще одно свойство с типом AccountHistory. Логично предположить, что если мы клонируем объект Person, то новый объект не нуждается в копии истории предыдущего. Такое требование выражается в коде следующим образом:
public class Person : ICloneable
{
    public string Name { getset; }
    public int Age { getset; }
    public Address Address { getset; }
    public AccountHistory AccountHistory { getset; }

    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 { getset; }
    public int Age { getset; }
    public Address Address { getset; }

    public Person DeepClone()
    {
        var person = (Person)MemberwiseClone();
        person.Address = Address.DeepClone();
        return person;
    }
}

public class Address : ICopier<Address>
{
    public string City { getset; }
    public string Street { getset; }

    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 { getset; }
    public int Age { getset; }
    public Address Address { getset; }
}

public class Address
{
    public Address(Address address)
    {
        City = address.City;
        Street = address.Street;
    }

    public string City { getset; }
    public string Street { getset; }
}

Плюсы:

  • Полный контроль над клонированием всего графа объектов.

Минусы:

  • Необходимо реализовать конструктор копирования в каждом классах графа.
  • Программист, впервые столкнувшись с подобным подходом, поначалу может только строить предположения о предназначении конструктора.

Заключение

Реализация интерфейсов и конструктора копирования предоставляет разработчику полный контроль над процессом клонирования, в то время как клонирование при помощи сериализации/десериализации избавляет от необходимости вносить изменения в логику клонирования всякий раз при добавлении/удалении/изменении новых свойств в клонируемые классы. Единственного и идеального решения не существует. Проанализировав плюсы и минусы каждого подходы, можно выбрать один, который больше всего подходит в своем конкретном случае.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google photo

Для комментария используется ваша учётная запись Google. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s