我需要实现具有继承的C#深层复制构造函数.有哪些模式可供选择?

Ant*_*ert 13 .net c# clone class-hierarchy

我希望在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;
        }
}
Run Code Online (Sandbox Code Playgroud)

这不起作用,因为克隆孩子只是父母是新的.在我的代码中,一些类具有大的层次结构.

这样做的推荐方法是什么?克隆每个级别的所有内容而不调用基类似乎是错误的?这个问题必须有一些巧妙的解决方案,它们是什么?

能否感谢大家的回答.看到一些方法真的很有趣.我认为如果有人举一个完整性的反思答案的例子会很好.+1等待!

Pav*_*aev 30

典型的方法是使用"复制构造函数"模式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);
     }
 }
Run Code Online (Sandbox Code Playgroud)

另一种方法是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;
     }
 }
Run Code Online (Sandbox Code Playgroud)

这两种方法都要求层次结构中的所有类都遵循该模式.使用哪一个是优先考虑的问题.

如果你有任何随机类实现ICloneable而没有实现保证(除了遵循记录的语义ICloneable),就没有办法扩展它.

  • @taoufik,请提供该声明的参考(通过MemberwiseClone克隆的实现比复制构造函数更受欢迎 - 我不知道在这个特定点上任何公认的"最佳实践").我也很感激,如果投票的人会解释投票. (4认同)
  • 复制构造函数不是任何C#最佳实践的一部分.应该使用`ICloneable`和`MemberwiseClone`代替. (2认同)

Fer*_*ndo 7

尝试序列化技巧:

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);
}
Run Code Online (Sandbox Code Playgroud)

  • 更大的缺点是它需要图中的_all_对象可序列化,而不仅仅是你的类.这可能并不总是可行的,并且一些可克隆的现有类型不可序列化(例如``XPathNavigator`实现`ICloneable`,但它不可序列化). (5认同)
  • +1,唯一的缺点是你的对象需要用SerializableAttribute标记. (2认同)

csh*_*net 7

警告:

这段代码应该非常谨慎使用.使用风险由您自己承担.此示例按原样提供,没有任何形式的担保.


还有另一种方法可以在对象图上执行深度克隆.在考虑使用此示例时,请务必注意以下事项:

缺点:

  1. 除非将这些引用提供给Clone(object,...)方法,否则还将克隆对外部类的任何引用.
  2. 没有构造函数将在克隆对象上执行,它们被完全复制.
  3. 不会执行ISerializable或序列化构造函数.
  4. 无法在特定类型上更改此方法的行为.
  5. 它将克隆所有内容,Stream,AppDomain,Form等等,这些可能会以可怕的方式破坏您的应用程序.
  6. 它可能会破坏,而使用序列化方法更有可能继续工作.
  7. 下面的实现使用递归,如果对象图太深,很容易导致堆栈溢出.

那你为什么要用呢?

优点:

  1. 它对所有实例数据执行完整的深层复制,而无需对象中的编码.
  2. 它保留重构对象中的所有对象图引用(甚至是圆形).
  3. 它的执行速度是二进制格式化程序的20倍以上,内存消耗较少.
  4. 它不需要任何东西,没有属性,实现的接口,公共属性,没有.

代码用法:

你只需用一个对象来调用它:

Class1 copy = Clone(myClass1);
Run Code Online (Sandbox Code Playgroud)

或者假设您有一个子对象,并且您订阅了它的事件......现在您要克隆该子对象.通过提供克隆的对象列表,您可以保留对象图的一些部分:

Class1 copy = Clone(myClass1, this);
Run Code Online (Sandbox Code Playgroud)

执行:

现在让我们先简单介绍一下......这里是切入点:

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);
}
Run Code Online (Sandbox Code Playgroud)

现在这很简单,它只是在克隆过程中为对象构建一个字典映射,并用任何不应克隆的对象填充它.你会注意到提供给字典的比较器是一个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); }
}
Run Code Online (Sandbox Code Playgroud)

这很容易,只是一个强制使用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;
    }
}
Run Code Online (Sandbox Code Playgroud)

您将注意到数组和委托副本的特殊情况.每个都有它们自己的原因,首先Array没有可以克隆的'成员',所以你必须处理它并依赖浅的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());
}
Run Code Online (Sandbox Code Playgroud)

最后注意:

老实说,当时这是一项有趣的运动.在数据模型上进行深度克隆通常是一件好事.今天的现实是,生成的大多数数据模型都会使用生成的深度克隆例程来废弃上面的hackery的有用性.我强烈建议您生成数据模型,并且能够执行深度克隆,而不是使用上面的代码.