题 深度克隆对象


我想做的事情如下:

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

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

我不经常需要这个功能,所以当有必要的时候,我已经使用了创建一个新对象然后单独复制每个属性,但它总是让我觉得有更好或更优雅的处理方式情况。

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


1831
2017-09-17 00:06


起源


可能有用:“为什么要复制一个物体是件可怕的事情?” agiledeveloper.com/articles/cloning072002.htm - Pedro77
stackoverflow.com/questions/8025890/... 另一种方案...... - Felix K.
你应该看一下AutoMapper - Daniel Little
你的解决方案要复杂得多,我迷失了阅读它......呵呵。我正在使用DeepClone界面。公共接口IDeepCloneable <T> {T DeepClone(); } - Pedro77
@ Pedro77:我关心的问题 IDeepCloneable 并非所有可以深度克隆的东西的集合都应该是;克隆时的正确行为 List<T> 不仅取决于 T,还要根据清单的目的。如果列表中的任何项目都不会暴露给任何会使它们变异的项目,那么即使列表中的项目可以克隆,最好直接复制引用。 - supercat


答案:


虽然标准做法是实施 ICloneable 界面(描述 这里,所以我不会反刍),这是我发现的一个很好的深度克隆对象复印机 代码项目 前一段时间,并将其纳入我们的东西。

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

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.", "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);
}

1467
2018-04-03 13:31



stackoverflow.com/questions/78536/cloning-objects-in-c/... 有一个上面代码的链接[并引用其他两个这样的实现,其中一个在我的上下文中更合适] - Ruben Bartelink
序列化/反序列化涉及不必要的大量开销。请参阅C#中的ICloneable接口和.MemberWise()克隆方法。 - 3Dave
@David,授予,但如果对象很轻,并且使用它时的性能不是太高,不符合您的要求,那么这是一个有用的提示。我承认,我没有在循环中大量使用大量数据,但我从未见过单一的性能问题。 - johnc
@Amir:实际上,不是: typeof(T).IsSerializable 如果类型已标记为,则也是如此 [Serializable] 属性。它没有必要实现 ISerializable 接口。 - Daniel Gehriger
我只是想提一下,虽然这种方法很有用,而且我自己多次使用它,但它与Medium Trust完全兼容 - 所以请注意你是否在编写需要兼容性的代码。 BinaryFormatter访问私有字段,因此无法在部分信任环境的默认权限集中工作。您可以尝试另一个序列化程序,但如果传入的对象依赖于私有字段,请确保您的调用者知道克隆可能不完美。 - Alex Norcliffe


我想要一个非常简单的对象,主要是原始和列表的克隆人。如果你的对象是开箱即用的JSON serializable,那么这个方法就可以了。这不需要修改或实现克隆类上的接口,只需要像JSON.NET这样的JSON序列化程序。

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

183
2017-09-17 01:12



解决方案比BinaryFormatter解决方案更快, .NET序列化性能比较 - esskar
谢谢你。我能够用与C#的MongoDB驱动程序一起提供的BSON序列化器做同样的事情。 - Mark Ewer
这对我来说是最好的方式,但是,我使用 Newtonsoft.Json.JsonConvert 但它是一样的 - Pierre
对于小平面物体,JSON方法是好的,但最初的问题是关于任何物体,包括深物体。只要没有委托和非托管指针,NFX.Slim序列化程序在任何.NET类型上的工作速度都要快几个数量级,这里的源代码非常像BinaryFormnatter,至少快5倍: github.com/aumcode/nfx/blob/master/Source/NFX/Serialization/...    测试套件: github.com/aumcode/serbench证明唯一更快的序列号是Protobuf,缺乏类型动力和参考 - itadapter DKh
为了使这个工作,克隆的对象需要如前所述可序列化 - 这也意味着它可能没有循环依赖 - radomeit


不使用的原因 ICloneable 是  因为它没有通用接口。 不使用它的原因是因为它含糊不清。它不清楚你是否得到浅拷贝或深拷贝;这取决于实施者。

是, MemberwiseClone 做一个浅的副本,但相反 MemberwiseClone 不 Clone;它可能是, DeepClone,这是不存在的。通过其ICloneable接口使用对象时,您无法知道底层对象执行哪种克隆。 (并且XML注释不会说清楚,因为您将获得接口注释而不是对象的Clone方法上的注释。)

我通常做的只是做一个 Copy 完全符合我想要的方法。


147
2017-09-26 20:18



我不清楚为什么ICloneable被认为是模糊的。给定像Dictionary(Of T,U)这样的类型,我希望ICloneable.Clone应该做任何级别的深度和浅层复制,以使新词典成为包含相同T和U的独立字典(结构内容,和/或对象引用)作为原始。歧义在哪里?可以肯定的是,继承了ISelf(Of T)的通用ICloneable(Of T),其中包括“Self”方法,会好得多,但我没有看到深度克隆与浅层克隆的模糊性。 - supercat
您的示例说明了问题。假设您有一个Dictionary <string,Customer>。应该克隆的词典有 相同 客户对象为原始对象,或 副本 那些客户对象?任何一个都有合理的用例。但ICloneable并不清楚你会得到哪一个。这就是为什么它没用。 - Ryan Lundy
@Kyralessa Microsoft MSDN文章实际上表明了这个问题,即不知道您是在请求深层还是浅层副本。 - crush
副本的答案 stackoverflow.com/questions/129389/... 描述基于递归MembershipClone的复制扩展 - Michael Freidgeim


我相信,在阅读了很多与此处相关的选项以及此问题的可能解决方案之后 所有的选项总结得很好 伊恩P.的链接 (所有其他选项都是这些选项的变体),最佳解决方案由提供 Pedro77的链接 关于问题评论。

所以我将在这里复制这两个参考文献的相关部分。这样我们可以:

在c sharp中克隆对象最好的办法!

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

文章快速深层复制表达式树   还有序列化,反射和表达树克隆的性能比较。

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

Venkat Subramaniam先生(此处的冗余链接)详细解释了原因

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

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

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

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

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

注意,如果我们保持对象数量的计数,这里实现的克隆将保持对象数量的正确计数。


84
2017-09-17 00:13



MS建议不要使用 ICloneable 对于公众成员。 “由于Clone的调用者不能依赖执行可预测克隆操作的方法,因此我们建议不要在公共API中实现ICloneable。” msdn.microsoft.com/en-us/library/... 但是,根据Venkat Subramaniam在您的链接文章中给出的解释,我认为在这种情况下使用是有意义的 只要ICloneable对象的创建者深入了解哪些属性应该是深层对比浅层副本 (即深拷贝脑,浅拷贝城) - BateTech
首先,我不是这个主题的专家(公共API)。一世 认为 曾经MS的评论很有意义。而且我认为假设它是安全的 用户 那个API将有如此深刻的理解。因此,只有在实现它时才有意义 公共API 如果对于谁将要使用它真的无关紧要。一世 猜测 非常明确地使用某种UML来区分每个属性可能有所帮助。但我想听听有更多经验的人的意见。 :P - cregox
你可以使用 CGbR克隆生成器 并获得类似的结果,而无需手动编写代码。 - Toxantron
中间语言实现很有用 - Michael Freidgeim


我更喜欢复制构造函数到克隆。意图更清晰。


69
2018-03-16 11:38



.Net没有复制构造函数。 - Pop Catalin
确实如此:new MyObject(objToCloneFrom)只需声明一个ctor,它将对象作为参数进行克隆。 - Nick
这不是一回事。您必须手动将其添加到每个班级,并且您甚至不知道您是否正在申请深层复制。 - Dave Van den Eynde
+1复制ctor。你必须手动为每种类型的对象编写一个clone()函数,并且当你的类层次结构深入几层时,祝你好运。 - Andrew Grant
使用复制构造函数,您会丢失层次结构。 agiledeveloper.com/articles/cloning072002.htm - Will


简单的扩展方法来复制所有公共属性。适用于任何物体和 才不是 要求上课 [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 ) } );
        }
    };
}

36
2017-12-02 17:39



不幸的是,这是有缺陷的。它等同于调用objectOne.MyProperty = objectTwo.MyProperty(即,它只是复制引用)。它不会克隆属性的值。 - Alex Norcliffe
Alex Norcliffe:问题的作者询问“复制每个财产”而不是克隆。在大多数情况下,不需要精确复制属性。 - Konstantin Salavatov
我考虑使用这种方法,但使用递归。因此,如果属性的值是引用,则创建一个新对象并再次调用CopyTo。我只看到一个问题,所有使用的类必须有一个没有参数的构造函数。有人试过这个吗?我也想知道这是否真的适用于包含.net类的属性,如DataRow和DataTable? - Koryu


好吧,我在Silverlight中使用ICloneable时遇到了问题,但是我喜欢seralization的想法,我可以seralize 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);
    }
}

28
2017-10-15 17:55