Deep cloning в C#: от рефлексии до сериализации

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Глубокое копирование в C#: почему это головная боль каждого разработчика?

Вы когда-нибудь копировали объект в C#, а потом с удивлением обнаруживали, что изменение в "копии" ломает оригинал? Знакомо? Это классическая проблема поверхностного копирования (shallow copy). Когда у объекта есть ссылочные поля (другие классы, списки, массивы), MemberwiseClone() просто копирует ссылки. В итоге и оригинал, и копия смотрят на один и тот же вложенный объект.

В этой статье мы разберем deep cloning в C# — процесс создания полной независимой копии объекта со всеми вложенными объектами. Мы сравним 5 популярных подходов, напишем реальный код, проведем мысленный бенчмарк и выясним, какой метод выбрать для вашего проекта.

Что такое Deep Clone и чем он отличается от Shallow Clone?

Представьте, что у вас есть класс Person, который содержит Address:

public class Address
{
    public string City { get; set; }
    public string Street { get; set; }
}

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Address HomeAddress { get; set; }
}

При shallow clone (обычный MemberwiseClone()) вы получите нового человека, но его HomeAddress будет указывать на тот же объект Address, что и у оригинала. Изменили улицу в копии — улица изменилась и в оригинале. Катастрофа.

Deep clone решает эту проблему: он рекурсивно копирует все вложенные объекты, создавая полностью независимую структуру данных.

Подход 1: Ручная реализация через ICloneable и конструкторы копирования

Самый очевидный, но и самый трудоемкий способ — реализовать интерфейс ICloneable или написать свой метод DeepClone().

Пример реализации:

public class Address : ICloneable
{
    public string City { get; set; }
    public string Street { get; set; }

    public object Clone()
    {
        return new Address { City = this.City, Street = this.Street };
    }
}

public class Person : ICloneable
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Address HomeAddress { get; set; }

    public object Clone()
    {
        return new Person
        {
            Name = this.Name,
            Age = this.Age,
            HomeAddress = (Address)this.HomeAddress?.Clone()
        };
    }
}

Плюсы:

  • Полный контроль над процессом копирования
  • Максимальная производительность
  • Понятный код (нет "магии")

Минусы:

  • Огромное количество boilerplate кода
  • Легко забыть обновить метод Clone() при добавлении нового поля
  • Не работает для сторонних классов (например, из библиотек)

Когда использовать: Только для простых DTO с 2-3 полями, где производительность критична (например, в игровых движках).

Подход 2: Рефлексия (Reflection) — универсальный солдат

Рефлексия позволяет написать универсальный метод deep cloning, который будет работать с любым объектом.

Пример кода:

public static T DeepCloneByReflection<T>(T source)
{
    if (source == null) return default;

    var type = source.GetType();
    
    // Для значимых типов и строк просто возвращаем значение
    if (type.IsValueType || type == typeof(string))
        return source;

    // Для массивов
    if (type.IsArray)
    {
        var arr = source as Array;
        var clonedArr = Array.CreateInstance(type.GetElementType(), arr.Length);
        for (int i = 0; i < arr.Length; i++)
            clonedArr.SetValue(DeepCloneByReflection(arr.GetValue(i)), i);
        return (T)(object)clonedArr;
    }

    // Для классов — создаем экземпляр и копируем поля
    var clone = Activator.CreateInstance(type);
    var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
    
    foreach (var field in fields)
    {
        var value = field.GetValue(source);
        field.SetValue(clone, DeepCloneByReflection(value));
    }

    return (T)clone;
}

Плюсы:

  • Универсальность — работает с любыми классами
  • Не требует модификации исходных классов
  • Обрабатывает циклические ссылки (если добавить проверку)

Минусы:

  • Медленная работа (рефлексия — тяжелая операция)
  • Сложность с readonly полями и свойствами только для чтения
  • Нет поддержки sealed классов с приватными конструкторами

Бенчмарк: Для объекта с 10 полями рефлексия работает в 50-100 раз медленнее ручного клонирования.

Подход 3: Сериализация — JSON, Binary, XML

Самый популярный и простой способ deep clone объекта в C# — сериализовать его в поток, а потом десериализовать обратно.

BinaryFormatter (устарел, не используйте!)

// Внимание: BinaryFormatter считается небезопасным!
// Используйте только если нет другого выхода
public static T DeepCloneBinary<T>(T source)
{
    using (var stream = new MemoryStream())
    {
        var formatter = new BinaryFormatter();
        formatter.Serialize(stream, source);
        stream.Seek(0, SeekOrigin.Begin);
        return (T)formatter.Deserialize(stream);
    }
}

JSON сериализация (Newtonsoft.Json или System.Text.Json)

// Вариант с Newtonsoft.Json
public static T DeepCloneJson<T>(T source)
{
    var settings = new JsonSerializerSettings
    {
        TypeNameHandling = TypeNameHandling.All, // для полиморфизма
        ReferenceLoopHandling = ReferenceLoopHandling.Ignore
    };
    var json = JsonConvert.SerializeObject(source, settings);
    return JsonConvert.DeserializeObject<T>(json, settings);
}

// Вариант с System.Text.Json (.NET Core 3.1+)
public static T DeepCloneSystemJson<T>(T source)
{
    var json = JsonSerializer.Serialize(source);
    return JsonSerializer.Deserialize<T>(json);
}

Плюсы:

  • Очень простой код (буквально 2-3 строки)
  • Работает с любыми классами (если они сериализуемы)
  • Поддерживает циклические ссылки (при правильных настройках)

Минусы:

  • Требует атрибутов [Serializable] или [DataContract]
  • Медленнее ручного клонирования в 5-10 раз
  • Проблемы с полиморфизмом (нужен TypeNameHandling)
  • Не работает с приватными полями без настройки

Блоги

Рекомендаций по книгам