我需要用继承实现 C# 深拷贝构造函数。都有哪些图案可供选择?

Posted

技术标签:

【中文标题】我需要用继承实现 C# 深拷贝构造函数。都有哪些图案可供选择?【英文标题】:I need to implement C# deep copy constructors with inheritance. What patterns are there to choose from?我需要用继承实现 C# 深拷贝构造函数。有哪些图案可供选择? 【发布时间】:2010-12-07 02:30:18 【问题描述】:

我希望在 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;
        

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

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

我能感谢大家的回答吗?看到一些方法真的很有趣。我认为如果有人给出一个完整的反思答案的例子会很好。 +1 等待中!

【问题讨论】:

为什么你的 Clone 方法需要一个参数(他们不使用)?正确的签名是 object ICloneable.Clone() - 没有参数。 对不起我的错...你在飞行中输入代码是对的! 微软建议不要实现 ICloneable,因为合约没有指定克隆的类型(深/浅)。来自框架设计指南第 2 版。 【参考方案1】:

典型的方法是使用 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);
     
 

另一种方法是在 Clone 的实现中使用 Object.MemberwiseClone - 这将确保结果始终是正确的类型,并允许覆盖扩展:

 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;
     
 

这两种方法都要求层次结构中的所有类都遵循该模式。使用哪一个是一个偏好问题。

如果您只是有任何随机类实现 ICloneable,但不保证实现(除了遵循 ICloneable 的书面语义),则无法扩展它。

【讨论】:

重要提示:由于 MemberwiseClone 方法会产生浅拷贝,因此您必须创建所有引用类型成员的新实例。您还需要取消任何委托/事件。 不是所有引用类型成员——只有那些你真正需要它的成员。例如,String 是一个引用类型,但我认为即使在深度克隆中您也不会想要创建它的新实例。同样适用于任何其他“价值类别”,例如 UriXName 复制构造函数不是任何 C# 最佳实践的一部分。应该改用ICloneableMemberwiseClone @taoufik,请提供该声明的参考(通过 MemberwiseClone 实现 Clone 优于复制构造函数 - 我不知道在这一点上是否有任何公认的“最佳实践”)。如果投反对票的人能解释投反对票,我也将不胜感激。 +1 复制构造函数为 ICloneable 提供了一个非常干净的实现。【参考方案2】:

尝试序列化技巧:

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

【讨论】:

+1,唯一的缺点是你的对象需要用 SerializableAttribute 来标记。 一个更大的缺点是它要求图中的 all 对象是可序列化的,而不仅仅是你的类。这可能并不总是可行的,并且某些可克隆的现有类型不可序列化(例如,XPathNavigator 实现了 ICloneable,但它不可序列化)。 此属性失败:私有字符串 a;公共字符串 A 获取 返回一个; 设置 值 = a; 【参考方案3】:

警告:

使用此代码时应格外小心。使用风险自负。此示例按原样提供,不提供任何形式的保证。


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

缺点:

    任何对外部类的引用也将被克隆,除非这些引用提供给 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());

最后说明:

老实说,这在当时是一个有趣的练习。在数据模型上进行深度克隆通常是一件好事。今天的现实是,大多数数据模型都是生成的,它们通过生成的深度克隆例程淘汰了上述黑客的有用性。我强烈建议您生成数据模型,并且它能够执行深度克隆,而不是使用上面的代码。

【讨论】:

感谢您的广泛回答。 +1 只为努力!当我开始实施我的解决方案时,将需要详细查看这一点(为了做到公正)。谢谢。【参考方案4】:

最好的方法是序列化你的对象,然后返回反序列化的副本。它将获取有关您对象的所有内容,除了那些标记为不可序列化的对象,并使继承序列化变得容易。

[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

这不是最有效的事情,但也不是替代品:重新选举。此选项的好处是可以无缝继承。

【讨论】:

【参考方案5】:
    您可以使用反射来循环所有变量并复制它们。(慢)如果它对您的软件来说很慢,您可以使用 DynamicMethod 并生成 il。 序列化对象并再次反序列化。

【讨论】:

【参考方案6】:

我认为您在这里没有正确实施 ICloneable;它需要一个没有参数的 Clone() 方法。我推荐的是这样的:

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;
    

请注意,CopyObject() 基本上是 Object.MemberwiseClone(),据推测您所做的不仅仅是复制值,您还将克隆任何属于类的成员。

【讨论】:

【参考方案7】:

尝试使用以下[使用关键字“new”]

public class Parent

  private int _X;
  public int X set_X=value; getreturn _X;
  public Parent copy()
  
     return new ParentX=this.X;
  

public class Child:Parent

  private int _Y;
  public int Y set_Y=value; getreturn _Y;
  public new Child copy()
  
     return new ChildX=this.X,Y=this.Y;
  

【讨论】:

【参考方案8】:

您应该改用MemberwiseClone 方法:

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;
        

【讨论】:

是“newObj.myA = theObj.myA;”在这种情况下,memberwiseclone 不会复制它,因为它不是对象吗? 它可以正常工作。基本上,任何 ChildObj 对象都是 ParentObj 和 ChildObj。如果您有对 ChildObj 的引用,并且会在其上调用 MemberwiseClone(与您调用它的位置无关),您将获得一个既是 ParentObj 又是 ChildObj 的对象。因此,“newObj.myA = this.myA;”将完全有效。 1) this.base 是什么? 2) 当你知道演员总是成功的时候,为什么还要使用as? 3) 当MemberwiseClone 已经这样做了,为什么还要复制字段值? 4) 什么是theObj?你的意思是this @Pavel:这个想法是回答问题,而不是重写他的代码或回答任何未提出的问题。 this.base => 基础;无论您使用as 还是(ChildObj) 都是开发人员的偏好。我认为(ChildObj) 看起来很糟糕。是的,您不需要复制任何值类型字段。但再一次,这不是他的问题的一部分。

以上是关于我需要用继承实现 C# 深拷贝构造函数。都有哪些图案可供选择?的主要内容,如果未能解决你的问题,请参考以下文章

c/c++/c++11 浅拷贝和深拷贝

java深拷贝与c#深拷贝的实现

C++类拷贝控制 深拷贝 浅拷贝

拷贝构造函数(深拷贝vs浅拷贝)

JavaScript继承的几种实现

C++深拷贝和浅拷贝细节理解