WARNING:
This code should be used with a great deal of caution. Use at your own risk. This example is provided as-is and without a warranty of any kind.
There is one other way to perform a deep clone on an object graph. It is important to be aware of the following when considering using this sample:
Cons:
- Any references to external classes will also be cloned unless those references are provided to the Clone(object, ...) method.
- No constructors will be executed on cloned objects they are reproduced EXACTLY as they are.
- No ISerializable or serialization constructors will be executed.
- There is no way to alter the behavior of this method on a specific type.
- It WILL clone everything, Stream, AppDomain, Form, whatever, and those will likely break your application in horrific ways.
- It could break whereas using the serialization method is much more likely to continue working.
- The implementation below uses recursion and can easily cause a stack overflow if your object graph is too deep.
So why would you want to use it?
Pros:
- It does a complete deep-copy of all instance data with no coding required in the object.
- It preserves all object graph references (even circular) in the reconstituted object.
- It's executes more than 20 times fatser than the binary formatter with less memory consumption.
- It requires nothing, no attributes, implemented interfaces, public properties, nothing.
Code Usage:
You just call it with an object:
Class1 copy = Clone(myClass1);
Or let's say you have a child object and you are subscribed to it's events... Now you want to clone that child object. By providing a list of objects to not clone, you can preserve some potion of the object graph:
Class1 copy = Clone(myClass1, this);
Implementation:
Now let's get the easy stuff out of the way first... Here is the entry point:
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);
}
Now that is simple enough, it just builds a dictionary map for the objects during the clone and populates it with any object that should not be cloned. You will note the comparer provided to the dictionary is a ReferenceComparer, let's take a look at what it does:
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); }
}
That was easy enough, just a comparer that forces the use of the System.Object's get hash and reference equality... now comes the hard work:
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;
}
}
You will notice right-off the special case for array and delegate copy. Each have their own reasons, first Array does not have 'members' that can be cloned, so you have to handle this and depend on the shallow Clone() member and then clone each element. As for the delegate it may work without the special-case; however, this will be far safer since it's not duplicating things like RuntimeMethodHandle and the like. If you intend to include other things in your hierarchy from the core runtime (like System.Type) I suggest you handle them explicitly in similar fashion.
The last case, and most common, is simply to use roughly the same routines that are used by the BinaryFormatter. These allow us to pop all the instance fields (public or private) out of the original object, clone them, and stick them into an empty object. The nice thing here is that the GetUninitializedObject returns a new instance that has not had the ctor run on it which could cause issues and slow the performance.
Whether the above works or not will highly depend upon your specific object graph and the data therein. If you control the objects in the graph and know that they are not referencing silly things like a Thread then the above code should work very well.
Testing:
Here is what I wrote to originally test this:
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());
}
Final Note:
Honestly it was a fun exercise at the time. It is generally a great thing to have deep cloning on a data model. Today's reality is that most data models are generated which obsoletes the usefulness of the hackery above with a generated deep clone routine. I highly recommend generating your data model & it's ability to perform deep-clones rather than using the code above.