I need to implement C# deep copy constructors with inheritance. What patterns are there to choose from?

前端 未结 8 1535
隐瞒了意图╮
隐瞒了意图╮ 2020-12-29 08:47

I wish to implement a deepcopy of my classes hierarchy in C#

public Class ParentObj : ICloneable
{
    protected int   myA;
    public virtual Object Clone          


        
相关标签:
8条回答
  • 2020-12-29 09:21

    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:

    1. Any references to external classes will also be cloned unless those references are provided to the Clone(object, ...) method.
    2. No constructors will be executed on cloned objects they are reproduced EXACTLY as they are.
    3. No ISerializable or serialization constructors will be executed.
    4. There is no way to alter the behavior of this method on a specific type.
    5. It WILL clone everything, Stream, AppDomain, Form, whatever, and those will likely break your application in horrific ways.
    6. It could break whereas using the serialization method is much more likely to continue working.
    7. 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:

    1. It does a complete deep-copy of all instance data with no coding required in the object.
    2. It preserves all object graph references (even circular) in the reconstituted object.
    3. It's executes more than 20 times fatser than the binary formatter with less memory consumption.
    4. 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.

    0 讨论(0)
  • I don't think you are implementing ICloneable correctly here; It requires a Clone() method with no parameters. What I would recommend is something like:

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

    Note that CopyObject() is basically Object.MemberwiseClone(), presumeably you would be doing more than just copying values, you would also be cloning any members that are classes.

    0 讨论(0)
  • 2020-12-29 09:25

    The best way is by serializing your object, then returning the deserialized copy. It will pick up everything about your object, except those marked as non-serializable, and makes inheriting serialization easy.

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

    It is not the most performant thing, but neither is the alternative: relection. The benefit of this option is that it seamlessly inherits.

    0 讨论(0)
  • 2020-12-29 09:29

    The typical approach is to use "copy constructor" pattern a la C++:

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

    The other approach is to use Object.MemberwiseClone in the implementation of Clone - this will ensure that result is always of the correct type, and will allow overrides to extend:

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

    Both approaches require that all classes in the hierarchy follow the pattern. Which one to use is a matter of preference.

    If you just have any random class implementing ICloneable with no guarantees on implementation (aside from following the documented semantics of ICloneable), there's no way to extend it.

    0 讨论(0)
  • 2020-12-29 09:31

    You should use the MemberwiseClone method instead:

    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;
            }
    }
    
    0 讨论(0)
  • 2020-12-29 09:35

    try the serialization trick:

    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);
    }
    
    0 讨论(0)
提交回复
热议问题