C#Deep Copy Constructorsを継承して実装する必要があります。どのパターンから選択できますか?
-
21-09-2019 - |
質問
C#でクラスの階層のディープコピーを実装したい
public Class ParentObj : ICloneable
{
protected int myA;
public virtual Object Clone ()
{
ParentObj newObj = new ParentObj();
newObj.myA = theObj.MyA;
return newObj;
}
}
public Class ChildObj : ParentObj
{
protected int myB;
public override Object Clone ( )
{
Parent newObj = this.base.Clone();
newObj.myB = theObj.MyB;
return newObj;
}
}
これは、子供のみをクローニングするときに親のみが新しくても機能しません。私のコードでは、一部のクラスには大きな階層があります。
これを行うための推奨方法は何ですか?基本クラスを呼び出すことなく各レベルですべてをクローニングすることは間違っているようですか?この問題にはきちんとした解決策があるに違いありませんが、それらは何ですか?
皆さんの答えに感謝しますか。いくつかのアプローチを見るのは本当に面白かったです。誰かが完全性のために反射の答えの例を挙げたらいいと思います。 +1待っています!
解決
典型的なアプローチは、「コピーコンストラクター」パターンを使用することです。
class Base : ICloneable
{
int x;
protected Base(Base other)
{
x = other.x;
}
public virtual object Clone()
{
return new Base(this);
}
}
class Derived : Base
{
int y;
protected Derived(Derived other)
: Base(other)
{
y = other.y;
}
public override object Clone()
{
return new Derived(this);
}
}
他のアプローチは使用することです Object.MemberwiseClone
の実装で Clone
- これにより、結果が常に正しいタイプであることが保証され、オーバーライドが拡張できるようになります。
class Base : ICloneable
{
List<int> xs;
public virtual object Clone()
{
Base result = this.MemberwiseClone();
// xs points to same List object here, but we want
// a new List object with copy of data
result.xs = new List<int>(xs);
return result;
}
}
class Derived : Base
{
List<int> ys;
public override object Clone()
{
// Cast is legal, because MemberwiseClone() will use the
// actual type of the object to instantiate the copy.
Derived result = (Derived)base.Clone();
// ys points to same List object here, but we want
// a new List object with copy of data
result.ys = new List<int>(ys);
return result;
}
}
どちらのアプローチでも、階層内のすべてのクラスがパターンに従うことが必要です。どちらを使用するかは好みの問題です。
ランダムなクラスが実装されている場合 ICloneable
実装に関する保証はありません(文書化されたセマンティクスに従うことは別として ICloneable
)、それを拡張する方法はありません。
他のヒント
シリアル化のトリックをお試しください:
public object Clone(object toClone)
{
BinaryFormatter bf = new BinaryFormatter();
MemoryStream ms= new MemoryStream();
bf.Serialize(ms, toClone);
ms.Flush();
ms.Position = 0;
return bf.Deserialize(ms);
}
警告:
このコードは、多くの注意を払って使用する必要があります。自己責任。この例は、いかなる種類の保証なしで提供されます。
オブジェクトグラフで深いクローンを実行するもう1つの方法があります。このサンプルを使用することを検討する場合は、次のことを認識することが重要です。
短所:
- 外部クラスへの参照は、それらの参照がクローン(オブジェクト、...)メソッドに提供されない限り、クローン化されます。
- クローニングされたオブジェクトでは、そのまま再現されるクローンオブジェクトではコンストラクターが実行されません。
- iserializableまたはSerializationコンストラクターは実行されません。
- 特定のタイプでこの方法の動作を変更する方法はありません。
- それはすべて、ストリーミング、appdomain、form、whathing、そしてそれらをクローンします おそらくそうなるでしょう 恐ろしい方法でアプリケーションを破ります。
- シリアル化方法を使用することは、動作を続ける可能性がはるかに高いのに対し、破損する可能性があります。
- 以下の実装では、再帰を使用しており、オブジェクトグラフが深すぎると簡単にスタックオーバーフローを引き起こす可能性があります。
では、なぜあなたはそれを使いたいのですか?
長所:
- オブジェクトにコーディングが必要ない場合、すべてのインスタンスデータの完全なディープコピーを実行します。
- 再構成されたオブジェクトにすべてのオブジェクトグラフ参照(偶数円形)を保存します。
- メモリ消費量が少ないバイナリフォーマッタの20倍以上のファッサーを実行しています。
- 何も必要ありません。属性、実装されたインターフェイス、パブリックプロパティ、何も必要ありません。
コードの使用法:
オブジェクトでそれを呼ぶだけです:
Class1 copy = Clone(myClass1);
または、子供のオブジェクトがあり、イベントに登録されているとしましょう...今、あなたはその子オブジェクトをクローン化したいと思います。オブジェクトのリストを提供することにより いいえ クローン、オブジェクトグラフのポーションを保存できます。
Class1 copy = Clone(myClass1, this);
実装:
それでは、最初に簡単なものを邪魔にならないようにしましょう...これがエントリポイントです:
public static T Clone<T>(T input, params object[] stableReferences)
{
Dictionary<object, object> graph = new Dictionary<object, object>(new ReferenceComparer());
foreach (object o in stableReferences)
graph.Add(o, o);
return InternalClone(input, graph);
}
これで十分に単純で、クローン中にオブジェクトの辞書マップを構築し、クローン化されないオブジェクトを穴を開けるだけです。辞書に提供された比較は、参照担当者であることに注意してください。それが何をしているのかを見てみましょう。
class ReferenceComparer : IEqualityComparer<object>
{
bool IEqualityComparer<object>.Equals(object x, object y)
{ return Object.ReferenceEquals(x, y); }
int IEqualityComparer<object>.GetHashCode(object obj)
{ return RuntimeHelpers.GetHashCode(obj); }
}
それは十分に簡単で、System.Object's Get Hashと参照平等の使用を強制する比較だけです...今、ハードワークがあります:
private static T InternalClone<T>(T input, Dictionary<object, object> graph)
{
if (input == null || input is string || input.GetType().IsPrimitive)
return input;
Type inputType = input.GetType();
object exists;
if (graph.TryGetValue(input, out exists))
return (T)exists;
if (input is Array)
{
Array arItems = (Array)((Array)(object)input).Clone();
graph.Add(input, arItems);
for (long ix = 0; ix < arItems.LongLength; ix++)
arItems.SetValue(InternalClone(arItems.GetValue(ix), graph), ix);
return (T)(object)arItems;
}
else if (input is Delegate)
{
Delegate original = (Delegate)(object)input;
Delegate result = null;
foreach (Delegate fn in original.GetInvocationList())
{
Delegate fnNew;
if (graph.TryGetValue(fn, out exists))
fnNew = (Delegate)exists;
else
{
fnNew = Delegate.CreateDelegate(input.GetType(), InternalClone(original.Target, graph), original.Method, true);
graph.Add(fn, fnNew);
}
result = Delegate.Combine(result, fnNew);
}
graph.Add(input, result);
return (T)(object)result;
}
else
{
Object output = FormatterServices.GetUninitializedObject(inputType);
if (!inputType.IsValueType)
graph.Add(input, output);
MemberInfo[] fields = inputType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
object[] values = FormatterServices.GetObjectData(input, fields);
for (int i = 0; i < values.Length; i++)
values[i] = InternalClone(values[i], graph);
FormatterServices.PopulateObjectMembers(output, fields, values);
return (T)output;
}
}
配列と委任のコピーの特別なケースにすぐに気付くでしょう。それぞれに独自の理由があり、最初の配列にはクローン化できる「メンバー」がないため、これを処理し、浅いclone()メンバーに依存し、各要素をクローンする必要があります。代表団については 五月 特別ケースなしで作業します。ただし、Runtimemethodhandleなどのようなものを複製していないため、これははるかに安全です。 Core Runtime(System.Typeなど)の階層に他のものを含めるつもりなら、同様の方法でそれらを明示的に処理することをお勧めします。
最後のケース、そして最も一般的なものは単に使用することです だいたい BinaryFormatterで使用される同じルーチン。これらを使用すると、元のオブジェクトからすべてのインスタンスフィールド(パブリックまたはプライベート)をポップし、それらをクローン化し、空のオブジェクトに貼り付けることができます。ここでの良いことは、GetunInitializializedObjectが、問題を引き起こし、パフォーマンスを遅くする可能性のあるCTORを実行していない新しいインスタンスを返すことです。
上記が機能するかどうかは、特定のオブジェクトグラフとそのデータに大きく依存します。グラフ内のオブジェクトを制御し、スレッドのような愚かなものを参照していないことを知っている場合、上記のコードは非常にうまく機能するはずです。
テスト:
これが私がこれを最初にテストするために書いたものです:
class Test
{
public Test(string name, params Test[] children)
{
Print = (Action<StringBuilder>)Delegate.Combine(
new Action<StringBuilder>(delegate(StringBuilder sb) { sb.AppendLine(this.Name); }),
new Action<StringBuilder>(delegate(StringBuilder sb) { sb.AppendLine(this.Name); })
);
Name = name;
Children = children;
}
public string Name;
public Test[] Children;
public Action<StringBuilder> Print;
}
static void Main(string[] args)
{
Dictionary<string, Test> data2, data = new Dictionary<string, Test>(StringComparer.OrdinalIgnoreCase);
Test a, b, c;
data.Add("a", a = new Test("a", new Test("a.a")));
a.Children[0].Children = new Test[] { a };
data.Add("b", b = new Test("b", a));
data.Add("c", c = new Test("c"));
data2 = Clone(data);
Assert.IsFalse(Object.ReferenceEquals(data, data2));
//basic contents test & comparer
Assert.IsTrue(data2.ContainsKey("a"));
Assert.IsTrue(data2.ContainsKey("A"));
Assert.IsTrue(data2.ContainsKey("B"));
//nodes are different between data and data2
Assert.IsFalse(Object.ReferenceEquals(data["a"], data2["a"]));
Assert.IsFalse(Object.ReferenceEquals(data["a"].Children[0], data2["a"].Children[0]));
Assert.IsFalse(Object.ReferenceEquals(data["B"], data2["B"]));
Assert.IsFalse(Object.ReferenceEquals(data["B"].Children[0], data2["B"].Children[0]));
Assert.IsFalse(Object.ReferenceEquals(data["B"].Children[0], data2["A"]));
//graph intra-references still in tact?
Assert.IsTrue(Object.ReferenceEquals(data["B"].Children[0], data["A"]));
Assert.IsTrue(Object.ReferenceEquals(data2["B"].Children[0], data2["A"]));
Assert.IsTrue(Object.ReferenceEquals(data["A"].Children[0].Children[0], data["A"]));
Assert.IsTrue(Object.ReferenceEquals(data2["A"].Children[0].Children[0], data2["A"]));
data2["A"].Name = "anew";
StringBuilder sb = new StringBuilder();
data2["A"].Print(sb);
Assert.AreEqual("anew\r\nanew\r\n", sb.ToString());
}
最終メモ:
正直なところ、それは当時楽しい運動でした。一般的に、データモデルを深くクローン化することは素晴らしいことです。今日の現実は、ほとんどのデータモデルが生成され、上記のハッカーの有用性が生成された深いクローンルーチンで生成されます。データモデルを生成することを強くお勧めします。上記のコードを使用するのではなく、ディープクローンを実行する機能を強くお勧めします。
最良の方法は、オブジェクトをシリアル化してから、脱色コピーを返すことです。非セライアル化できないとマークされたものを除いて、オブジェクトに関するすべてを拾い、シリアル化を継承することを容易にします。
[Serializable]
public class ParentObj: ICloneable
{
private int myA;
[NonSerialized]
private object somethingInternal;
public virtual object Clone()
{
MemoryStream ms = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(ms, this);
object clone = formatter.Deserialize(ms);
return clone;
}
}
[Serializable]
public class ChildObj: ParentObj
{
private int myB;
// No need to override clone, as it will still serialize the current object, including the new myB field
}
それは最もパフォーマンスのあるものではありませんが、代替案でもありません。このオプションの利点は、シームレスに継承することです。
- 反射を使用してすべての変数をループしてコピーできます。(遅い)ソフトウェアを遅くする場合は、DynamicMethodを使用してILを生成できます。
- オブジェクトをシリアル化し、再度脱必要にします。
ここでiClonableを正しく実装しているとは思わない。パラメーターのないclone()メソッドが必要です。私がお勧めするのは、次のようなものです。
public class ParentObj : ICloneable
{
public virtual Object Clone()
{
var obj = new ParentObj();
CopyObject(this, obj);
}
protected virtual CopyObject(ParentObj source, ParentObj dest)
{
dest.myA = source.myA;
}
}
public class ChildObj : ParentObj
{
public override Object Clone()
{
var obj = new ChildObj();
CopyObject(this, obj);
}
public override CopyObject(ChildObj source, ParentObj dest)
{
base.CopyObject(source, dest)
dest.myB = source.myB;
}
}
CopyObject()は基本的にObject.memberwiseclone()であることに注意してください。おそらく、値をコピーするだけでなく、クラスであるメンバーをクローニングするだけでなく、おそらく実行することに注意してください。
次のものを使用してみてください[キーワード「new "を使用
public class Parent
{
private int _X;
public int X{ set{_X=value;} get{return _X;}}
public Parent copy()
{
return new Parent{X=this.X};
}
}
public class Child:Parent
{
private int _Y;
public int Y{ set{_Y=value;} get{return _Y;}}
public new Child copy()
{
return new Child{X=this.X,Y=this.Y};
}
}
を使用する必要があります MemberwiseClone
代わりにメソッド:
public class ParentObj : ICloneable
{
protected int myA;
public virtual Object Clone()
{
ParentObj newObj = this.MemberwiseClone() as ParentObj;
newObj.myA = this.MyA; // not required, as value type (int) is automatically already duplicated.
return newObj;
}
}
public class ChildObj : ParentObj
{
protected int myB;
public override Object Clone()
{
ChildObj newObj = base.Clone() as ChildObj;
newObj.myB = this.MyB; // not required, as value type (int) is automatically already duplicated
return newObj;
}
}