Как правильно сделать глубокое копирование объекта в .NET (C#)?
Проблема поверхностного vs глубокого копирования
Вы абсолютно правы: в C# по умолчанию при присваивании создаётся поверхностная копия (shallow copy) для ссылочных типов. Это означает, что копируется только ссылка на объект, а не сам объект. Для создания независимой копии всех вложенных объектов нужно использовать глубокое копирование (deep copy). Рассмотрим несколько надёжных подходов.
Способ 1: Ручная реализация через интерфейс ICloneable (не рекомендуется)
Хотя это классический подход, он имеет недостатки: неясно, поверхностное или глубокое копирование реализует метод Clone(). Microsoft рекомендует избегать его в новых проектах.
public class Person : ICloneable
{
public string Name { get; set; }
public int Age { get; set; }
public Address Address { get; set; }
public object Clone() => new Person
{
Name = this.Name,
Age = this.Age,
Address = new Address { City = this.Address.City, Street = this.Address.Street }
};
}
Способ 2: Сериализация (рекомендуемый универсальный метод)
Самый популярный и надёжный способ — использовать бинарную или JSON-сериализацию. Он автоматически обрабатывает все вложенные объекты и циклические ссылки (если настроить правильно).
Вариант A: BinaryFormatter (устарел, но всё ещё работает)
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
public static T DeepCopy<T>(T obj)
{
using var ms = new MemoryStream();
var formatter = new BinaryFormatter();
formatter.Serialize(ms, obj);
ms.Position = 0;
return (T)formatter.Deserialize(ms);
}
// Использование:
var copy = DeepCopy(original);
Важно: Классы должны быть помечены атрибутом [Serializable]. BinaryFormatter теперь считается небезопасным, используйте его только для легаси-кода.
Вариант B: JSON-сериализация (современный подход)
using System.Text.Json;
public static T DeepCopy<T>(T obj)
{
var json = JsonSerializer.Serialize(obj);
return JsonSerializer.Deserialize<T>(json);
}
// Для сложных графов объектов используйте Newtonsoft.Json:
// using Newtonsoft.Json;
// public static T DeepCopy<T>(T obj) => JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(obj));
Способ 3: Рекурсивное копирование через рефлексию (для продвинутых)
Если нужно избежать сериализации (например, для больших объектов), можно написать generic-метод с рефлексией:
public static T DeepCopyReflection<T>(T obj)
{
if (obj == null) return default;
var type = obj.GetType();
// Для простых типов и строк возвращаем сам объект
if (type.IsValueType || type == typeof(string)) return obj;
// Создаём новый экземпляр
var result = Activator.CreateInstance(type);
// Копируем все свойства
foreach (var prop in type.GetProperties())
{
if (prop.CanWrite)
{
var value = prop.GetValue(obj);
prop.SetValue(result, DeepCopyReflection(value));
}
}
return (T)result;
}
Рекомендации по выбору
- Для большинства случаев — используйте JSON-сериализацию (System.Text.Json). Это быстро, безопасно и не требует атрибутов.
- Если нужна максимальная производительность — напишите ручные методы копирования для конкретных классов.
- Избегайте ICloneable — он двусмыслен.
- Для циклических ссылок — используйте Newtonsoft.Json с настройкой ReferenceLoopHandling.Ignore.
В вашем примере с Person и Address достаточно добавить атрибут [Serializable] и использовать бинарную сериализацию или просто JSON. После глубокого копирования изменения в copy не затронут original.
<<Альтернативный подход: Использование библиотеки AutoMapper для маппинга
Если вы работаете с большими и сложными графами объектов, ручное копирование или сериализация могут быть неудобны. В таких случаях отлично подходит библиотека AutoMapper, которая обычно используется для маппинга DTO, но может служить и для глубокого копирования.
Пример настройки:
using AutoMapper;
// Настройка конфигурации
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<Person, Person>();
cfg.CreateMap<Address, Address>();
});
var mapper = config.CreateMapper();
var copy = mapper.Map<Person>(original);
Плюсы:
- Гибкая настройка: можно игнорировать определённые свойства, использовать кастомные преобразователи.
- Хорошая производительность после первоначальной компиляции конфигурации.
- Поддержка коллекций и вложенных объектов "из коробки".
Минусы:
- Требуется установка NuGet-пакета AutoMapper.
- Сложность настройки для очень специфичных сценариев.
- Избыточен, если нужно просто скопировать объект без трансформаций.
Этот метод особенно полезен, когда у вас есть несколько типов объектов, которые нужно копировать с разными правилами. В простых случаях лучше использовать сериализацию.
Ответов пока нет.
Войдите чтобы оставить ответ