Avatar ·

Как правильно сделать глубокое копирование объекта в .NET (C#)?

Всем привет! Я начинающий C# разработчик и столкнулся с проблемой копирования объектов. У меня есть класс Person с несколькими вложенными объектами, например, Address. Когда я пытаюсь скопировать объект через простое присваивание, то получаю ссылку на тот же самый объект, а не его копию. При изменении свойств в "копии" меняется и исходный объект. Вот пример кода, который иллюстрирует проблему: ```csharp 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 Address { get; set; } } // Использование var original = new Person { Name = "Иван", Age = 30, Address = new Address { City = "Москва", Street = "Тверская" } }; // Проблема: это поверхностная копия var copy = original; copy.Name = "Петр"; copy.Address.City = "Санкт-Петербург"; Console.WriteLine(original.Name); // Иван (хорошо) Console.WriteLine(original.Address.City); // Санкт-Петербург (плохо!) ``` Как видите, при изменении City в copy меняется и original. Я понимаю, что это из-за того, что Address — ссылочный тип. Как сделать так, чтобы при копировании создавалась полностью независимая копия объекта со всеми вложенными объектами? Какие есть лучшие практики в .NET для глубокого копирования? Заранее спасибо! <<>>

Проблема поверхностного 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.
  • Сложность настройки для очень специфичных сценариев.
  • Избыточен, если нужно просто скопировать объект без трансформаций.

Этот метод особенно полезен, когда у вас есть несколько типов объектов, которые нужно копировать с разными правилами. В простых случаях лучше использовать сериализацию.