警告:
应非常谨慎地使用此代码。使用风险自负。此示例按原样提供,不提供任何形式的保证。
还有另一种方法可以在对象图上执行深度克隆。在考虑使用此示例时,请务必注意以下事项:
缺点:
- 除非将这些引用提供给 Clone(object, ...) 方法,否则对外部类的任何引用也将被克隆。
- 不会在克隆对象上执行构造函数,它们会按原样复制。
- 不会执行任何 ISerializable 或序列化构造函数。
- 无法更改此方法在特定类型上的行为。
- 它会克隆所有内容,Stream、AppDomain、Form 等等,而这些可能会以可怕的方式破坏您的应用程序。
- 它可能会中断,而使用序列化方法更有可能继续工作。
- 下面的实现使用递归,如果您的对象图太深,很容易导致堆栈溢出。
那么你为什么要使用它呢?
优点:
- 它对所有实例数据进行完整的深度复制,而无需在对象中进行编码。
- 它保留重构对象中的所有对象图引用(甚至是循环的)。
- 它的执行速度比二进制格式化程序高 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 hash 和 reference 相等性的比较器......现在是艰苦的工作:
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;
}
}
您会立即注意到数组和委托副本的特殊情况。每个都有自己的原因,首先 Array 没有可以克隆的“成员”,因此您必须处理此问题并依赖浅层 Clone() 成员,然后克隆每个元素。至于委托,它可以在没有特殊情况的情况下工作;但是,这会更安全,因为它不会复制 RuntimeMethodHandle 之类的东西。如果您打算在核心运行时的层次结构中包含其他内容(例如 System.Type),我建议您以类似的方式显式处理它们。
最后一种情况,也是最常见的,就是使用与 BinaryFormatter大致相同的例程。这些允许我们从原始对象中弹出所有实例字段(公共或私有),克隆它们,并将它们粘贴到一个空对象中。这里的好处是 GetUninitializedObject 返回一个尚未在其上运行 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());
}
最后注:
老实说,这在当时是一个有趣的练习。在数据模型上进行深度克隆通常是一件好事。今天的现实是,大多数数据模型都是生成的,它们通过生成的深度克隆例程淘汰了上述黑客的有用性。我强烈建议生成您的数据模型,它能够执行深度克隆,而不是使用上面的代码。