警告:
このコードは、細心の注意を払って使用する必要があります。自己責任。この例は現状のまま提供され、いかなる種類の保証もありません。
オブジェクト グラフでディープ クローンを実行する別の方法があります。このサンプルの使用を検討する際には、次の点に注意することが重要です。
短所:
- これらの参照が Clone(object, ...) メソッドに提供されない限り、外部クラスへの参照も複製されます。
- 複製されたオブジェクトに対してコンストラクターは実行されず、そのまま正確に再現されます。
- ISerializable またはシリアル化コンストラクターは実行されません。
- 特定の型でこのメソッドの動作を変更する方法はありません。
- ストリーム、AppDomain、フォームなど、すべてを複製します。これらは、アプリケーションを恐ろしい方法で破壊する可能性があります。
- シリアライゼーション方法を使用すると、動作し続ける可能性がはるかに高くなりますが、壊れる可能性があります。
- 以下の実装では再帰を使用しているため、オブジェクト グラフが深すぎると、スタック オーバーフローが簡単に発生する可能性があります。
では、なぜそれを使いたいのでしょうか?
長所:
- オブジェクトにコーディングを必要とせずに、すべてのインスタンス データの完全なディープ コピーを実行します。
- 再構成されたオブジェクト内のすべてのオブジェクト グラフ参照 (循環も含む) を保持します。
- メモリ消費量が少なく、バイナリ フォーマッタよりも 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);
}
これで十分簡単になりました。複製中にオブジェクトのディクショナリ マップを作成し、複製してはならないオブジェクトを入力するだけです。ディクショナリに提供される比較子は ReferenceComparer であることに注意してください。それが何をするかを見てみましょう。
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 の get ハッシュと参照の等価性を強制的に使用する比較子だけでした。次は大変な作業です。
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 などを複製しないため、はるかに安全です。コア ランタイム (System.Type など) から階層に他のものを含める場合は、同様の方法で明示的に処理することをお勧めします。
最後のケースで最も一般的なのは、BinaryFormatter で使用されるルーチンとほぼ同じルーチンを単純に使用することです。これらにより、元のオブジェクトからすべてのインスタンス フィールド (パブリックまたはプライベート) をポップし、それらを複製して、空のオブジェクトに貼り付けることができます。ここで良いことは、GetUninitializedObject が、ctor が実行されていない新しいインスタンスを返すことです。これにより、問題が発生し、パフォーマンスが低下する可能性があります。
上記が機能するかどうかは、特定のオブジェクト グラフとその中のデータに大きく依存します。グラフ内のオブジェクトを制御し、それらが Thread のような愚かなものを参照していないことがわかっている場合、上記のコードは非常にうまく機能するはずです。
テスト:
これを最初にテストするために書いたものは次のとおりです。
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());
}
最後の注意:
正直なところ、当時は楽しいエクササイズでした。一般に、データ モデルでディープ クローンを作成することは非常に便利です。今日の現実は、ほとんどのデータ モデルが生成され、生成されたディープ クローン ルーチンで上記のハッカーの有用性が失われていることです。上記のコードを使用するのではなく、データ モデルを生成することを強くお勧めします。これにより、ディープ クローンを実行できます。