Глубокое копирование в 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)
- Не работает с приватными полями без настройки