C#のオブジェクトの接続されたグラフの一般化されたコピーとそれらのシリアル化のニュアンス

個々のオブジェクトと接続されたグラフをコピーするタスクは、プログラミングでよく見られます。 初期条件と要件に応じて、それらを解決する方法がいくつかあります。 この記事の目的は、主要なソリューションを検討し、範囲を特定し、利点と欠点を強調することです。



画像






コピー手法の分類



1)一般化による:ルーチンおよび一般化



ルーチンアプローチは、特定の各クラスのコピーロジックの実装、つまり、特定のエンティティのコピーを担当する多数の特別なユーティリティメソッドの作成を意味します。 これらのメソッドには、多くの場合、退屈で単調なコードが大量に含まれています。 このようなメソッドを手動で記述するのは退屈で、エラーがたくさんあります。 ただし、自動コードジェネレーターはこのタスクを容易にしますが、多くの場合、オブジェクトとグラフ構造に制限を課します。 これらの手法の利点は、メモリ消費が少なく高性能であることです。 これらは通常、protobufシリアライザーで使用されます。



ただし、一般的なアプローチは、パフォーマンスの低下により同じタイプの追加ロジックを記述する必要がなくなり、特定の要件を満たすさまざまなタイプのオブジェクトにも適用できます。



2)シリアル化と逆シリアル化の可能性に応じて:サポートなしで、正確かつ超精密なサポート



シリアル化とは、オブジェクトのグラフの状態に関する情報を文字列またはバイト配列に保存する可能性を意味します。逆シリアル化とは、この情報から初期状態でグラフを復元することを意味します。これにより、これらのメカニズムをディープコピーに使用できます。 さまざまな形式のシリアライザーには多くの代替実装がありますが、目的が近くても、微妙に違いがあります。 しかし、何らかの方法で2つのクラスに分けることができます。特定の場合に歪みをコピーに導入する正確なクラスと、複雑なグラフを変更せずに復元できる非常に正確なクラスです。



歪みは、ほとんどの場合、次の性質のものです。



-グラフのリンク構造の違反

[理由:1つのオブジェクトへの複数のリンク、閉じた円形リンク]



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; } }
      
      





-個々のシリアライザーの制約

[理由:たとえば、多次元配列(オブジェクト[,,,])]



 // may cause exception on serialization [DataMember]public int[,,] Multiarray = new {{{1,2,3}, {7,8,9}}};
      
      





*上記の欠点は、標準のDataContractJsonSerializerにも固有のものです。



一般的なコピー方法の分類



1)グラフ構造の範囲:表面および深層



表面と深さのコピーは根本的に異なります。 オブジェクトAとBが与えられ、AにはBへのリンクが含まれているとします(列A => B)。 オブジェクトAの表面コピーを行うと、オブジェクトAが作成され、これもBを参照します。つまり、2つの列A => BとA '=> Bが得られます。 それらには共通の部分Bがあるため、最初の列でオブジェクトBを変更すると、その状態は2番目の列で自動的に変化します。 オブジェクトAとAは独立したままです。 しかし、最も興味深いのは、閉じた(循環)リンクを持つグラフです。 AがBを参照し、BがAを参照するようにします(A <=> B)。オブジェクトAをAに表面的にコピーすると、非常に珍しいグラフA '=> B <=> A、つまり、クローンされました。 ディープコピーには、グラフに含まれるすべてのオブジェクトのクローンが含まれます。 この場合、A <=> BはA '<=> B'に変換されます。その結果、両方のグラフは互いに完全に分離されます。 場合によっては、表面的なコピーで十分ですが、常にではありません。



2)グラフの状態のカバレッジ:完全および部分的



状態に関しては、コピーする場合は完全に完全に再現することができます。つまり、完全に同一のクローンを取得するか、部分的に、問題を解決するために不可欠なデータのみに制限することができます。たとえば、パブリックメンバーのみ、または特別な属性でマークされたデータのみをコピーします。



基本的なコピー技術の概要



1)リフレクションと組み合わせたMemberwiseClone



.NETプラットフォームでオブジェクトの浅いコピー[浅いコピー]を実行するために、オブジェクトクラスの特別なprotected [protected] MemberwiseCloneメソッドがあります。これは、すべてのフィールドをコピーしてオブジェクトの完全なコピーを作成します。 このメソッドをリフレクションと組み合わせて使用​​すると、再帰的なディープコピーアルゴリズムを実装できます。



長所:

-ポータブル

-高速に動作します

-オブジェクトを作成するためにパブリックおよびデフォルトのコンストラクターを必要としません



短所:

-オブジェクトをシリアライズおよびデシリアライズできません

-フィルタリングすることなく、行内のすべてのフィールドをコピーします



レプリケーションフレームワークライブラリでのディープメンバーワイズクローニングの実装
 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);
      
      







* 1つ 、この手法の2つの代替ですが、わずかに最適ではない実装



2)最新のシリアル化ライブラリの機能の比較







機能の面では、まったく新しいレプリケーションフレームワークライブラリに大きな期待が寄せられています 。これについては、Habréに関するレビュー出版物がそれほど前にリリースされていません。 広範なタスクを解決するように設計されていますが、開発における重要な要件の1つは、任意の複雑なグラフのディープコピーと超正確な[de]シリアル化の実装でした。



レプリケーションフレームワークのライセンスバージョンは、非営利目的および教育目的での使用は無料で、リクエストに応じて入手できます nugetの試用版 2017年9月まで機能します。



レプリケーションフレームワークパフォーマンスノート
レプリケーションフレームワークを使用すると、他のシリアライザーよりも高速にオブジェクトをコピーできるのに、シリアル化および逆シリアル化のプロセス自体の速度が低下する理由が疑問になる場合があります。 実際には、ライブラリはバイトまたは文字列の配列ではなく、オブジェクトのスナップショット[スナップショット]をコピーに使用します。つまり、加速が達成されるため、プリミティブ値の変換は行われません。 シリアル化中に、グラフのスナップショットが最初に作成され、その後、json文字列に変換されます。 この中間ステップにより、シリアル化とそれに続く逆シリアル化の速度が低下します(画像の読み取りとそれに続くグラフの再作成)。





Cでのディープコピーメソッドの実装#



バイナリフォーマッター
  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); }
      
      





Memberwise Cloneを介したレプリケーションフレームワーク
  public static T GetShallowClone<T>(this T obj) => obj.MemberwiseClone(false); public static T GetDeepClone<T>(this T obj) => obj.MemberwiseClone(true);
      
      







スナップショット経由のレプリケーションフレームワーク
  public static T GetDeepClone<T>(this T obj) { var snapshot = obj.CreateSnapshot(); return snapshot.ReplicateGraph<T>(); }
      
      





それだけです!



All Articles