Задачи по копированию отдельных объектов и связных графов часто встречаются в программировании. Методов их решения существует несколько в зависимости от исходных условий и требований. Цель статьи — рассмотреть ключевые разновидности решений, обозначить область применения, выделить преимущества и недостатки.
Рутинные подходы подразумевают имплементацию своей логики копирования для каждого конкретного класса, то есть создание ряда специальных служебных методов, отвечающих за копирование тех или иных сущностей. Методы эти зачастую содержат скучный и однообразный код в больших объёмах. Ручное написание таких методов утомительно и чревато ошибками. Однако автоматические генераторы кода облегчают эту задачу, хотя зачастую и накладывают свои ограничения на объекты и структуру графов. Преимущество данных методик в их высокой производительности при малом потреблении памяти. Обычно они применяются в protobuf-сериализаторах.
Обобщённые же подходы избавлены от необходимости написания дополнительной однотипной логики за счёт некоторого снижения производительности, а также применимы к объектам различных типов, отвечающим определённым требованиям.
Сериализация предполагает возможность сохранения информации о состояния графа объектов в строку либо массив байт, а десериализация — восстановления по данной информации графа с исходным состоянием, что, в свою очередь, позволяет использовать эти механизмы для глубинного копирования. Существует немало альтернативных реализаций сериализаторов для различных форматов, однако, даже будучи близкими по назначению, они очень различаются в нюансах. Но так или иначе их можно разделить на два класса: точные, что в определённых случаях вносят свои искажения в копию, и сверхточные, которые позволяют восстанавливать сложные графы без изменений.
Искажения чаще всего бывают следующего характера:
— нарушение ссылочной структуры графа
[причины: несколько ссылок на один объект, замкнутые циклические ссылки]
— утрата информации о типах объектов
[причины: применяется ссылка на объект с типом базового класса]
— искажение родственных примитивных типов
[причины: ограничения форматов сериализации]
— потеря свойств при сериализации классов-коллекций
[причины: ограничения сериализаторов]
— индивилуальные ограничения сериализаторов
[причины: например, многомерные массивы (object[,,,])]
* перечисленные недостатки присущи даже стандартному DataContractJsonSerializer
Поверхностное и глубинное копирование принципиально различны. Пускай даны объекты А и Б, причём А содержит ссылку на Б (граф А=>Б). При поверхностном копировании объекта А будет создан объект А', который также будет ссылаться на Б, то есть в итоге получится два графа А=>Б и А'=>Б. У них будет общая часть Б, поэтому при изменении объекта Б в первом графе, автоматически его состояние будет мутировать и во втором. Объекты же А и А' останутся независимы. Но наибольший интерес представляют графы с замкнутыми (циклическими) ссылками. Пускай А ссылается на Б и Б ссылается на А (А<=>Б), при поверхностном копировании объекта А в А' получим весьма необычный граф А'=>Б<=>А, то есть в итоговый граф попал изначальный объект, который подвергался клонированию. Глубинное же копирование предполагает клонирования всех объектов, входящих в граф. Для нашего случая А<=>Б преобразуется в А'<=>Б', в итоге оба графа совершенно изолированы друг от друга. В некоторых случаях достаточно поверхностного копирования, но далеко не всегда.
Что касается состояния, то при копировании его можно воспроизводить абсолютно полностью, то есть получать совершенно идентичный клон, либо частично, ограничиваясь лишь существенными для решения задачи данными, например, копировать только публичные члены или же те, что отмечены специальными атрибутами.
Для осуществления поверхностного копирования [shallow copy] объекта в платформе .NET предусмотрен специальный защищённый [protected] метод MemberwiseClone у класса object, который создаёт полную копию объекта путём копирования всех его полей. Используя данный метод в комбинации с рефлексией можно реализовать рекурсивный алгоритм глубинного копирования [deep copy].
Плюсы:
— портабельный
— быстро работает
— не нуждается в публичных и дефолтных конструкторах для создания объекта
Минусы:
— нельзя сериализовать и десериализовать объекты
— копирует все поля подряд без возможности их фильтрации
* альтернативные, но немного неоптимальные реализации данной методики один и два
В плане функциональности хорошие надежды подаёт совсем новая библиотека Replication Framework, о которой не очень давно вышла обзорная публикация на Хабре. Хотя она предназначена для решения более широкого круга задач, одними из ключевых требований при её разработке были реализация глубинного копирования и сверхточной [де]сериализации сколь угодно сложных графов.
Лицензионная версия Replication Framework бесплатна для некоммерческого и учебного использования и предоставляется по запросу. Пробная триал-версия на nuget функциональна до сентября 2017 года.
Всё!
Классификация подходов к копированию
1) по обобщённости: рутинные и обобщённые
Рутинные подходы подразумевают имплементацию своей логики копирования для каждого конкретного класса, то есть создание ряда специальных служебных методов, отвечающих за копирование тех или иных сущностей. Методы эти зачастую содержат скучный и однообразный код в больших объёмах. Ручное написание таких методов утомительно и чревато ошибками. Однако автоматические генераторы кода облегчают эту задачу, хотя зачастую и накладывают свои ограничения на объекты и структуру графов. Преимущество данных методик в их высокой производительности при малом потреблении памяти. Обычно они применяются в protobuf-сериализаторах.
Обобщённые же подходы избавлены от необходимости написания дополнительной однотипной логики за счёт некоторого снижения производительности, а также применимы к объектам различных типов, отвечающим определённым требованиям.
2) по возможностям сериализации и десериализации: без поддержки, с точной и сверхточные поддержкой
Сериализация предполагает возможность сохранения информации о состояния графа объектов в строку либо массив байт, а десериализация — восстановления по данной информации графа с исходным состоянием, что, в свою очередь, позволяет использовать эти механизмы для глубинного копирования. Существует немало альтернативных реализаций сериализаторов для различных форматов, однако, даже будучи близкими по назначению, они очень различаются в нюансах. Но так или иначе их можно разделить на два класса: точные, что в определённых случаях вносят свои искажения в копию, и сверхточные, которые позволяют восстанавливать сложные графы без изменений.
Искажения чаще всего бывают следующего характера:
— нарушение ссылочной структуры графа
[причины: несколько ссылок на один объект, замкнутые циклические ссылки]
var person = new Person();
var role = new Role();
person.MainRole = role; // use the one 'role' instance before serialization
person.Roles.Add(role); // but possible two separated instances after graph deserialization
var person = new Person();
var role = new Role {Person = person};
person.Roles.Add(role); // may cause stack overflow exception
— утрата информации о типах объектов
[причины: применяется ссылка на объект с типом базового класса]
// may cause exception on deserialization
[DataMember]public object SingleObject = new Person();
[DataMember]public object[] Array = new [] { new Person() };
— искажение родственных примитивных типов
[причины: ограничения форматов сериализации]
[DataMember]public object SingleObject = 12345L;
// long may be deserialized like int, Guid like string
[DataMember]public object[] Array = new [] { 123, 123L, Guid.New(), Guid.New().ToString() };
— потеря свойств при сериализации классов-коллекций
[причины: ограничения сериализаторов]
[CollectionDataContract]
public class CustomCollection: List<object>
{
// property may be lost
[DataMember]public string Name { get; set; }
}
— индивилуальные ограничения сериализаторов
[причины: например, многомерные массивы (object[,,,])]
// may cause exception on serialization
[DataMember]public int[,,] Multiarray = new {{{1,2,3}, {7,8,9}}};
* перечисленные недостатки присущи даже стандартному DataContractJsonSerializer
Классификация обобщённых способов копирования
1) по охвату структуры графа: поверхностное и глубинное
Поверхностное и глубинное копирование принципиально различны. Пускай даны объекты А и Б, причём А содержит ссылку на Б (граф А=>Б). При поверхностном копировании объекта А будет создан объект А', который также будет ссылаться на Б, то есть в итоге получится два графа А=>Б и А'=>Б. У них будет общая часть Б, поэтому при изменении объекта Б в первом графе, автоматически его состояние будет мутировать и во втором. Объекты же А и А' останутся независимы. Но наибольший интерес представляют графы с замкнутыми (циклическими) ссылками. Пускай А ссылается на Б и Б ссылается на А (А<=>Б), при поверхностном копировании объекта А в А' получим весьма необычный граф А'=>Б<=>А, то есть в итоговый граф попал изначальный объект, который подвергался клонированию. Глубинное же копирование предполагает клонирования всех объектов, входящих в граф. Для нашего случая А<=>Б преобразуется в А'<=>Б', в итоге оба графа совершенно изолированы друг от друга. В некоторых случаях достаточно поверхностного копирования, но далеко не всегда.
2) по охвату состояния графа: полное и частичное
Что касается состояния, то при копировании его можно воспроизводить абсолютно полностью, то есть получать совершенно идентичный клон, либо частично, ограничиваясь лишь существенными для решения задачи данными, например, копировать только публичные члены или же те, что отмечены специальными атрибутами.
Обзор основных методик копирования
1) MemberwiseClone вкупе с рефлексией
Для осуществления поверхностного копирования [shallow copy] объекта в платформе .NET предусмотрен специальный защищённый [protected] метод MemberwiseClone у класса object, который создаёт полную копию объекта путём копирования всех его полей. Используя данный метод в комбинации с рефлексией можно реализовать рекурсивный алгоритм глубинного копирования [deep copy].
Плюсы:
— портабельный
— быстро работает
— не нуждается в публичных и дефолтных конструкторах для создания объекта
Минусы:
— нельзя сериализовать и десериализовать объекты
— копирует все поля подряд без возможности их фильтрации
Реализация Deep Memberwise Cloning в библиотеке Replication Framework
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Art.Comparers
{
public class ReferenceComparer<T> : IEqualityComparer<T>
{
public static readonly ReferenceComparer<T> Default = new ReferenceComparer<T>();
public int GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj);
public bool Equals(T x, T y) => ReferenceEquals(x, y);
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Art.Comparers;
namespace Art
{
public static class Cloning
{
public static List<Type> LikeImmutableTypes = new List<Type> {typeof(string), typeof(Regex)};
public static T MemberwiseClone<T>(this T origin, bool deepMode,
IEqualityComparer<object> comparer = null) => deepMode
? (T) origin.GetDeepClone(new Dictionary<object, object>(comparer ?? ReferenceComparer<object>.Default))
: (T) MemberwiseCloneMethod.Invoke(origin, null);
private static readonly MethodInfo MemberwiseCloneMethod =
typeof(object).GetMethod("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.Instance);
private static IEnumerable<FieldInfo> EnumerateFields(this Type type, BindingFlags bindingFlags) =>
type.BaseType?.EnumerateFields(bindingFlags)
.Concat(type.GetFields(bindingFlags | BindingFlags.DeclaredOnly)) ??
type.GetFields(bindingFlags);
private static bool IsLikeImmutable(this Type type) => type.IsValueType || LikeImmutableTypes.Contains(type);
private static object GetDeepClone(this object origin, IDictionary<object, object> originToClone)
{
if (origin == null) return null;
var type = origin.GetType();
if (type.IsLikeImmutable()) return origin;
if (originToClone.TryGetValue(origin, out var clone)) return clone;
clone = MemberwiseCloneMethod.Invoke(origin, null);
originToClone.Add(origin, clone);
if (type.IsArray && !type.GetElementType().IsLikeImmutable())
{
var array = (Array) clone;
var indices = new int[array.Rank];
var dimensions = new int[array.Rank];
for (var i = 0; i < array.Rank; i++) dimensions[i] = array.GetLength(i);
for (var i = 0; i < array.Length; i++)
{
var t = i;
for (var j = indices.Length - 1; j >= 0; j--)
{
indices[j] = t % dimensions[j];
t /= dimensions[j];
}
var deepClone = array.GetValue(indices).GetDeepClone(originToClone);
array.SetValue(deepClone, indices);
}
}
var fields = type.EnumerateFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
foreach (var field in fields.Where(f => !f.FieldType.IsLikeImmutable()))
{
var deepClone = field.GetValue(origin).GetDeepClone(originToClone);
field.SetValue(origin, deepClone);
}
return clone;
}
}
}
Применение данного расширения
var person = new Person();
var role = new Role();
person.Roles.Add(role);
var deepClone = person.MemberwiseClone(true);
* альтернативные, но немного неоптимальные реализации данной методики один и два
2) Сравнение функциональности некоторых современных библиотек сериализации
В плане функциональности хорошие надежды подаёт совсем новая библиотека Replication Framework, о которой не очень давно вышла обзорная публикация на Хабре. Хотя она предназначена для решения более широкого круга задач, одними из ключевых требований при её разработке были реализация глубинного копирования и сверхточной [де]сериализации сколь угодно сложных графов.
Лицензионная версия Replication Framework бесплатна для некоммерческого и учебного использования и предоставляется по запросу. Пробная триал-версия на nuget функциональна до сентября 2017 года.
Примечание о производительности Replication Framework
Может возникнуть вопрос, почему Replication Framework позволяет копировать объекты быстрее других сериализаторов, но проигрывает по скорости в самих процессах сериализации и десериализации. Дело в том, что для копирования библиотека использует мгновенные снимки объектов [Snapshots], а не массивы байт или строки, то есть не происходит конвертации примитивных значений, за счёт чего достигается ускорение. При сериализации же сначала происходит создание снимка графа, а уже после он преобразуется в json-строку. Именно из-за этого промежуточного шага снижается скорость сериализации и последующей десериализации (чтение снимка и последующее воссоздание графа по нему).
Реализации методов глубинного копирования на C#
BinaryFormatter
public static T GetDeepClone<T>(this T obj)
{
using (var ms = new MemoryStream())
{
var formatter = new BinaryFormatter();
formatter.Serialize(ms, obj);
ms.Position = 0;
return (T) formatter.Deserialize(ms);
}
}
DataContractSerializer
public static T GetDeepClone<T>(this T obj)
{
using (var ms = new MemoryStream())
{ // preserveObjectReferences==true to save valid reference structure of graph
var serializer = new DataContractSerializer(typeof(T), null, int.MaxValue, false, true, null);
serializer.WriteObject(ms, obj);
ms.Position = 0;
return (T) serializer.ReadObject(ms);
}
}
DataContractJsonSerializer
public static T GetDeepClone<T>(this T obj)
{
using (var ms = new MemoryStream())
{
var serializer = new DataContractJsonSerializer(typeof(T));
serializer.WriteObject(ms, obj);
ms.Position = 0;
return (T) serializer.ReadObject(ms);
}
}
Newtonsoft.Json
public static T GetDeepClone<T>(this T obj)
{
var json = JsonConvert.SerializeObject(obj);
return JsonConvert.DeserializeObject<T>(json);
}
Replication Framework via Memberwise Clone
public static T GetShallowClone<T>(this T obj) => obj.MemberwiseClone(false);
public static T GetDeepClone<T>(this T obj) => obj.MemberwiseClone(true);
Replication Framework via Snapshot
public static T GetDeepClone<T>(this T obj)
{
var snapshot = obj.CreateSnapshot();
return snapshot.ReplicateGraph<T>();
}
Всё!
Поделиться с друзьями