深度克隆对象

我想做类似的事情:

MyObject myObj = GetMyObj(); // Create and fill a new object
MyObject newObj = myObj.Clone();

然后对未反映在原始对象中的新对象进行更改。

我通常不需要此功能,因此在必要时我会先创建一个新对象,然后分别复制每个属性,但是这总是让我感到有更好或更优雅的处理方式情况。

如何克隆或深度复制对象,以便可以修改克隆的对象而不会在原始对象中反映任何更改?

答案

虽然标准做法是实现ICloneable接口( 在此进行了介绍,所以我不会反驳),但我还是在一段时间前在The Code Project上发现了一个不错的深克隆对象复印机,并将其合并到我们的资料中。

如在其他地方提到的,它确实要求您的对象可序列化。

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

/// <summary>
/// Reference Article http://www.codeproject.com/KB/tips/SerializedObjectCloner.aspx
/// Provides a method for performing a deep copy of an object.
/// Binary Serialization is used to perform the copy.
/// </summary>
public static class ObjectCopier
{
    /// <summary>
    /// Perform a deep Copy of the object.
    /// </summary>
    /// <typeparam name="T">The type of object being copied.</typeparam>
    /// <param name="source">The object instance to copy.</param>
    /// <returns>The copied object.</returns>
    public static T Clone<T>(T source)
    {
        if (!typeof(T).IsSerializable)
        {
            throw new ArgumentException("The type must be serializable.", nameof(source));
        }

        // Don't serialize a null object, simply return the default for that object
        if (Object.ReferenceEquals(source, null))
        {
            return default(T);
        }

        IFormatter formatter = new BinaryFormatter();
        Stream stream = new MemoryStream();
        using (stream)
        {
            formatter.Serialize(stream, source);
            stream.Seek(0, SeekOrigin.Begin);
            return (T)formatter.Deserialize(stream);
        }
    }
}

这个想法是先序列化您的对象,然后反序列化为一个新的对象。这样做的好处是,当对象变得太复杂时,您不必担心克隆所有内容。

并使用扩展方法(也来自最初引用的源):

如果您更喜欢使用 C#3.0 的新扩展方法 ,请将方法更改为具有以下签名:

public static T Clone<T>(this T source)
{
   //...
}

现在,方法调用就变成了objectBeingCloned.Clone();

编辑 (2015 年 1 月 10 日)以为我会重新审视这一点,我最近提到开始使用(Newtonsoft)Json 来做到这一点,它应该更轻巧,并且避免了 [Serializable] 标签的开销。 ( NB @atconway 在注释中指出,不使用 JSON 方法克隆私有成员)

/// <summary>
/// Perform a deep Copy of the object, using Json as a serialisation method. NOTE: Private members are not cloned using this method.
/// </summary>
/// <typeparam name="T">The type of object being copied.</typeparam>
/// <param name="source">The object instance to copy.</param>
/// <returns>The copied object.</returns>
public static T CloneJson<T>(this T source)
{            
    // Don't serialize a null object, simply return the default for that object
    if (Object.ReferenceEquals(source, null))
    {
        return default(T);
    }

    // initialize inner objects individually
    // for example in default constructor some list property initialized with some values,
    // but in 'source' these items are cleaned -
    // without ObjectCreationHandling.Replace default constructor values will be added to result
    var deserializeSettings = new JsonSerializerSettings {ObjectCreationHandling = ObjectCreationHandling.Replace};

    return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(source), deserializeSettings);
}

我想要一个克隆器,用于主要是基元和列表的非常简单的对象。如果您的对象是开箱即用的 JSON 可序列化的,则此方法可以解决问题。这不需要修改或实现克隆类上的接口,只需要像 JSON.NET 这样的 JSON 序列化程序即可。

public static T Clone<T>(T source)
{
    var serialized = JsonConvert.SerializeObject(source);
    return JsonConvert.DeserializeObject<T>(serialized);
}

另外,您可以使用此扩展方法

public static class SystemExtension
{
    public static T Clone<T>(this T source)
    {
        var serialized = JsonConvert.SerializeObject(source);
        return JsonConvert.DeserializeObject<T>(serialized);
    }
}

不使用的原因ICloneable不是因为它没有一个通用的接口。 不使用它的原因是因为它含糊不清 。不清楚是要浅拷贝还是要深拷贝。这取决于实施者。

是的, MemberwiseClone会进行浅表复制,但是MemberwiseClone的反面不是Clone ;可能是DeepClone ,它不存在。通过对象的 ICloneable 接口使用对象时,您不知道基础对象执行哪种克隆。 (而且 XML 注释也不清楚,因为您将获得接口注释,而不是对象的 Clone 方法中的接口注释。)

我通常所做的只是简单地执行完全符合我想要的Copy方法。

在对这里链接的许多选项以及该问题的可能解决方案进行了很多阅读之后,我相信所有选项在Ian P的链接上都进行了很好的总结 (所有其他选项都是这些选项的变体),并且最佳解决方案是Pedro77在问题评论上的链接

因此,我将仅在此处复制这两个参考的相关部分。这样我们就可以拥有:

在 C Sharp 中克隆对象最好的办法!

首先,这些都是我们的选择:

文章 “通过表达树进行快速深度复制”还对通过序列化,反射和表达树进行克隆的性能进行了比较。

为什么我选择ICloneable (即手动)

Venkat Subramaniam 先生(此处为冗余链接)详细说明了原因

他所有的文章都围绕着一个示例,该示例尝试使用 3 个对象: PersonBrainCity来适用于大多数情况。我们想要克隆一个人,这个人将拥有自己的大脑,但城市相同。您可以想象上面任何其他方法可以带来的所有问题或阅读本文。

这是我对他的结论的略微修改:

通过指定New后跟类名来复制对象通常会导致代码不可扩展。使用克隆(原型模式的应用)是一种更好的方法。但是,使用 C#(和 Java)提供的克隆也很成问题。最好提供一个受保护的(非公共)副本构造函数,然后从 clone 方法中调用它。这使我们能够将创建对象的任务委派给类本身的实例,从而提供可扩展性,并使用受保护的副本构造函数安全地创建对象。

希望此实现可以使事情变得清晰:

public class Person : ICloneable
{
    private final Brain brain; // brain is final since I do not want 
                // any transplant on it once created!
    private int age;
    public Person(Brain aBrain, int theAge)
    {
        brain = aBrain; 
        age = theAge;
    }
    protected Person(Person another)
    {
        Brain refBrain = null;
        try
        {
            refBrain = (Brain) another.brain.clone();
            // You can set the brain in the constructor
        }
        catch(CloneNotSupportedException e) {}
        brain = refBrain;
        age = another.age;
    }
    public String toString()
    {
        return "This is person with " + brain;
        // Not meant to sound rude as it reads!
    }
    public Object clone()
    {
        return new Person(this);
    }
    …
}

现在考虑从 Person 派生一个类。

public class SkilledPerson extends Person
{
    private String theSkills;
    public SkilledPerson(Brain aBrain, int theAge, String skills)
    {
        super(aBrain, theAge);
        theSkills = skills;
    }
    protected SkilledPerson(SkilledPerson another)
    {
        super(another);
        theSkills = another.theSkills;
    }

    public Object clone()
    {
        return new SkilledPerson(this);
    }
    public String toString()
    {
        return "SkilledPerson: " + super.toString();
    }
}

您可以尝试运行以下代码:

public class User
{
    public static void play(Person p)
    {
        Person another = (Person) p.clone();
        System.out.println(p);
        System.out.println(another);
    }
    public static void main(String[] args)
    {
        Person sam = new Person(new Brain(), 1);
        play(sam);
        SkilledPerson bob = new SkilledPerson(new SmarterBrain(), 1, "Writer");
        play(bob);
    }
}

产生的输出将是:

This is person with Brain@1fcc69
This is person with Brain@253498
SkilledPerson: This is person with SmarterBrain@1fef6f
SkilledPerson: This is person with SmarterBrain@209f4e

观察到,如果我们对对象数量进行计数,则此处实现的克隆将对对象数量进行正确计数。

与克隆相比,我更喜欢复制构造函数。意图更加明确。

复制所有公共属性的简单扩展方法。适用于任何物体,并且不需要类是[Serializable] 。可以扩展为其他访问级别。

public static void CopyTo( this object S, object T )
{
    foreach( var pS in S.GetType().GetProperties() )
    {
        foreach( var pT in T.GetType().GetProperties() )
        {
            if( pT.Name != pS.Name ) continue;
            ( pT.GetSetMethod() ).Invoke( T, new object[] 
            { pS.GetGetMethod().Invoke( S, null ) } );
        }
    };
}

我在 Silverlight 中使用 ICloneable 时遇到了问题,但是我喜欢序列化的想法,因为我可以序列化 XML,所以我这样做了:

static public class SerializeHelper
{
    //Michael White, Holly Springs Consulting, 2009
    //michael@hollyspringsconsulting.com
    public static T DeserializeXML<T>(string xmlData) where T:new()
    {
        if (string.IsNullOrEmpty(xmlData))
            return default(T);

        TextReader tr = new StringReader(xmlData);
        T DocItms = new T();
        XmlSerializer xms = new XmlSerializer(DocItms.GetType());
        DocItms = (T)xms.Deserialize(tr);

        return DocItms == null ? default(T) : DocItms;
    }

    public static string SeralizeObjectToXML<T>(T xmlObject)
    {
        StringBuilder sbTR = new StringBuilder();
        XmlSerializer xmsTR = new XmlSerializer(xmlObject.GetType());
        XmlWriterSettings xwsTR = new XmlWriterSettings();

        XmlWriter xmwTR = XmlWriter.Create(sbTR, xwsTR);
        xmsTR.Serialize(xmwTR,xmlObject);

        return sbTR.ToString();
    }

    public static T CloneObject<T>(T objClone) where T:new()
    {
        string GetString = SerializeHelper.SeralizeObjectToXML<T>(objClone);
        return SerializeHelper.DeserializeXML<T>(GetString);
    }
}

我刚刚创建了CloneExtensions项目。它使用 Expression Tree 运行时代码编译生成的简单赋值操作执行快速,深度的克隆。

如何使用它?

不必编写自己的CloneCopy方法,而是在字段和属性之间分配分配音调,而是让该程序使用 Expression Tree 自己完成。标记为扩展方法的GetClone<T>()方法使您可以在实例上简单地调用它:

var newInstance = source.GetClone();

您可以使用CloningFlags枚举选择应该从source复制到newInstance CloningFlags

var newInstance 
    = source.GetClone(CloningFlags.Properties | CloningFlags.CollectionItems);

可以克隆什么?

  • 基本类型(int,uint,byte,double,char 等),已知的不可变类型(DateTime,TimeSpan,String)和委托(包括 Action,Func 等)
  • 可空
  • T [] 数组
  • 自定义类和结构,包括通用类和结构。

以下类 / 结构成员是内部克隆的:

  • 公用而非只读字段的值
  • 带有 get 和 set 访问器的公共属性的值
  • 实现 ICollection 的类型的收集项

有多快?

解决方案要快于反射,因为在给定类型T首次使用GetClone<T>之前,成员信息仅需收集一次。

当您克隆更多然后耦合相同类型T实例时,它也比基于序列化的解决方案更快。

和更多...

文档中阅读有关生成的表达式的更多信息。

List<int>示例表达式调试列表:

.Lambda #Lambda1<System.Func`4[System.Collections.Generic.List`1[System.Int32],CloneExtensions.CloningFlags,System.Collections.Generic.IDictionary`2[System.Type,System.Func`2[System.Object,System.Object]],System.Collections.Generic.List`1[System.Int32]]>(
    System.Collections.Generic.List`1[System.Int32] $source,
    CloneExtensions.CloningFlags $flags,
    System.Collections.Generic.IDictionary`2[System.Type,System.Func`2[System.Object,System.Object]] $initializers) {
    .Block(System.Collections.Generic.List`1[System.Int32] $target) {
        .If ($source == null) {
            .Return #Label1 { null }
        } .Else {
            .Default(System.Void)
        };
        .If (
            .Call $initializers.ContainsKey(.Constant<System.Type>(System.Collections.Generic.List`1[System.Int32]))
        ) {
            $target = (System.Collections.Generic.List`1[System.Int32]).Call ($initializers.Item[.Constant<System.Type>(System.Collections.Generic.List`1[System.Int32])]
            ).Invoke((System.Object)$source)
        } .Else {
            $target = .New System.Collections.Generic.List`1[System.Int32]()
        };
        .If (
            ((System.Byte)$flags & (System.Byte).Constant<CloneExtensions.CloningFlags>(Fields)) == (System.Byte).Constant<CloneExtensions.CloningFlags>(Fields)
        ) {
            .Default(System.Void)
        } .Else {
            .Default(System.Void)
        };
        .If (
            ((System.Byte)$flags & (System.Byte).Constant<CloneExtensions.CloningFlags>(Properties)) == (System.Byte).Constant<CloneExtensions.CloningFlags>(Properties)
        ) {
            .Block() {
                $target.Capacity = .Call CloneExtensions.CloneFactory.GetClone(
                    $source.Capacity,
                    $flags,
                    $initializers)
            }
        } .Else {
            .Default(System.Void)
        };
        .If (
            ((System.Byte)$flags & (System.Byte).Constant<CloneExtensions.CloningFlags>(CollectionItems)) == (System.Byte).Constant<CloneExtensions.CloningFlags>(CollectionItems)
        ) {
            .Block(
                System.Collections.Generic.IEnumerator`1[System.Int32] $var1,
                System.Collections.Generic.ICollection`1[System.Int32] $var2) {
                $var1 = (System.Collections.Generic.IEnumerator`1[System.Int32]).Call $source.GetEnumerator();
                $var2 = (System.Collections.Generic.ICollection`1[System.Int32])$target;
                .Loop  {
                    .If (.Call $var1.MoveNext() != False) {
                        .Call $var2.Add(.Call CloneExtensions.CloneFactory.GetClone(
                                $var1.Current,
                                $flags,


                         $initializers))
                } .Else {
                    .Break #Label2 { }
                }
            }
            .LabelTarget #Label2:
        }
    } .Else {
        .Default(System.Void)
    };
    .Label
        $target
    .LabelTarget #Label1:
}

}

与以下 C#代码具有相同的含义:

(source, flags, initializers) =>
{
    if(source == null)
        return null;

    if(initializers.ContainsKey(typeof(List<int>))
        target = (List<int>)initializers[typeof(List<int>)].Invoke((object)source);
    else
        target = new List<int>();

    if((flags & CloningFlags.Properties) == CloningFlags.Properties)
    {
        target.Capacity = target.Capacity.GetClone(flags, initializers);
    }

    if((flags & CloningFlags.CollectionItems) == CloningFlags.CollectionItems)
    {
        var targetCollection = (ICollection<int>)target;
        foreach(var item in (ICollection<int>)source)
        {
            targetCollection.Add(item.Clone(flags, initializers));
        }
    }

    return target;
}

这与您为List<int>编写自己的Clone方法的方式很不一样?

如果您已经使用像一个第三方应用程序ValueInjecterAutomapper ,你可以这样做:

MyObject oldObj; // The existing object to clone

MyObject newObj = new MyObject();
newObj.InjectFrom(oldObj); // Using ValueInjecter syntax

使用此方法,您不必在对象上实现 ISerializable 或 ICloneable。这在 MVC / MVVM 模式中很常见,因此已经创建了这样的简单工具。

请参阅CodePlex 上的 valueinjecter 深度克隆解决方案

最好是实现诸如扩展方法

public static T DeepClone<T>(this T originalObject)
{ /* the cloning code */ }

然后在解决方案中的任何地方使用它

var copy = anyObject.DeepClone();

我们可以有以下三种实现:

  1. 通过序列化 (最短的代码)
  2. 通过反射 - 快 5 倍
  3. 通过表达式树 - 快 20 倍

所有链接的方法都运行良好,并经过了深入的测试。