深度克隆对象

2627

我想做类似这样的事情:

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

然后对新对象进行更改,这些更改不会反映在原始对象中。

我很少需要此功能,因此在必要时,我经常会创建一个新对象,然后逐个复制每个属性,但这总让我感觉有更好或更优雅的方法来处理这种情况。

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


109
可能有用:「为什么复制一个对象是可怕的事情?」http://www.agiledeveloper.com/articles/cloning072002.htm - Pedro77
2
另一个解决方案... - Felix K.
27
你应该看一下AutoMapper。 - Daniel Little
4
您的解决方案过于复杂,我看得有点迷糊... 哈哈哈。 我正在使用一个DeepClone接口。public interface IDeepCloneable<T> { T DeepClone(); } - Pedro77
4
@Pedro77 -- 有趣的是,那篇文章最终建议在类上创建一个“克隆”方法,然后让它调用一个内部的私有构造函数并传递“this”。因此,简单地复制是糟糕的 [sic],但是小心地复制(这篇文章绝对值得一读)是可以的。;^) - ruffin
显示剩余8条评论
59个回答

1917
一种方法是实现 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 via serialization.
    /// </summary>
    /// <typeparam name="T">The type of object being copied.</typeparam>
    /// <param name="source">The object instance to copy.</param>
    /// <returns>A deep copy of the 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 (ReferenceEquals(source, null)) return default;

        using var Stream stream = new MemoryStream();
        IFormatter formatter = new BinaryFormatter();
        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 serialization 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 (ReferenceEquals(source, null)) return default;

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

30
https://dev59.com/H3VD5IYBdhLWcg3wHn6d#78551 这个链接引用了上面的代码[并提到另外两个实现,其中一个更适合我的情况]。 - Ruben Bartelink
121
序列化/反序列化涉及很大的开销,这是不必要的。请参考C#中的ICloneable接口和.MemberWise()克隆方法。 - 3Dave
22
@David,虽然如此,如果对象轻量,并且在使用时性能损失不会对您的要求产生太大影响,那么这是一个有用的小技巧。我承认我没有在循环中使用大量数据进行过密集测试,但我从未见过一个性能问题。 - johnc
20
@Amir:实际上不是这样的:如果一个类型被标记为[Serializable],那么typeof(T).IsSerializable也会返回true,不一定要实现ISerializable接口。 - Daniel Gehriger
13
提醒一下,尽管这种方法很有用,我自己也用过很多次,但它与中等信任(Medium Trust)完全不兼容 - 因此请注意,如果您正在编写需要兼容性的代码。BinaryFormatter访问私有字段,因此不能在部分信任环境的默认权限集中工作。您可以尝试另一个序列化程序,但请确保调用者知道,如果传入的对象依赖于私有字段,则克隆可能不完美。 - Alex Norcliffe
显示剩余20条评论

428

我需要一个克隆器,可以用于大部分基本数据类型和列表。如果你的对象可以直接转化为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);
    }
}

21
该解决方案比BinaryFormatter方案更快,详见.NET序列化性能对比 - esskar
5
谢谢。我能够使用C# MongoDB驱动程序中提供的BSON序列化器实现基本相同的操作。 - Mark Ewer
6
这对我来说是最好的方式,不过我使用的是 Newtonsoft.Json.JsonConvert,但效果一样。 - Pierre
4
为了使此操作成功,需要将要克隆的对象序列化,正如先前提到的那样 - 这也意味着例如它可能没有循环依赖。 - radomeit
4
我认为这是最佳方案,因为它可以应用于大多数编程语言的实现。 - mr5
显示剩余6条评论

201
不使用ICloneable 的原因并非它没有通用接口。不使用它的原因是因为它含糊不清。它没有明确表明您正在获取浅层副本还是深层副本;这取决于实现者。
是的,MemberwiseClone 会进行浅层复制,但相反的MemberwiseClone 不是Clone;也许应该是DeepClone,但这种方法不存在。当您通过其ICloneable接口使用对象时,您无法知道基础对象执行哪种克隆。 (XML注释也无法清楚地说明此点,因为您将获得接口注释而不是对象的Clone方法上的注释。)
我通常做的就是简单地创建一个Copy方法,以完全满足我的需求。

我不清楚为什么ICloneable被认为是模糊的。例如像Dictionary(Of T,U)这样的类型,我期望ICloneable.Clone应该做任何必要的深度和浅度复制,使新的字典成为一个包含与原始字典相同的T和U(结构内容和/或对象引用)的独立字典。哪里有歧义?确保一个泛型ICloneable(Of T),它继承了ISelf(Of T),其中包括一个“Self”方法,会更好,但我没有看到深度复制与浅度复制之间的歧义。 - supercat
45
你的例子展示了问题。假设你有一个Dictionary<string, Customer>。那么克隆后的Dictionary应该拥有和原始对象相同的Customer对象,还是这些Customer对象的副本?对于这两种情况,都存在合理的使用场景。但是ICloneable并没有明确表明你将得到哪一种方式。这就是为什么它并不实用的原因。 - Ryan Lundy
@Kyralessa,微软MSDN文章实际上指出了这个问题,即不知道您是否正在请求深层复制还是浅层复制。 - crush
重复的答案来自 https://dev59.com/DnVC5IYBdhLWcg3w-WWs#11308879,它描述了基于递归MembershipClone的Copy扩展。 - Michael Freidgeim

160

经过大量阅读这里提供的许多选项和解决方案,我相信Ian P 的链接已经很好地总结了所有选项(其它所有选项都是那些选项的变体),而最佳的解决方案由问题评论中Pedro77的链接提供。

所以我只会在这里复制那两个参考文献的相关部分。这样我们就可以得到:

C#中克隆对象最好的方法!

首先,这些是我们的全部选项:

Fast Deep Copy by Expression Trees文章还进行了克隆性能比较,包括使用序列化、反射和表达式树的克隆。

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

Venkat Subramaniam先生(这里是冗余链接)详细解释了为什么

他的所有文章都围绕着一个例子展开,试图适用于大多数情况,使用3个对象:PersonBrainCity。我们想要克隆一个人,这个人将有自己的大脑但城市相同。您可以想象任何其他上面提到的方法可能

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

需要注意的是,如果我们保持对对象数量的计数,那么这里实现的克隆将会正确地保留对象数量的计数。


9
微软建议不要将 ICloneable 用于公共成员。"因为调用方无法依赖 Clone 方法执行可预测的克隆操作,我们建议不要在公共 API 中实现 ICloneable。" 但是,根据您链接文章中Venkat Subramaniam 的解释,我认为在这种情况下使用 ICloneable 是有意义的,只要创建 ICloneable 对象的人深刻理解哪些属性应该进行深拷贝和浅拷贝(例如深拷贝 Brain,浅拷贝 City)。 - BateTech
中间语言实现非常有用。 - Michael Freidgeim
2
C# 中没有 final。 - Konrad
感谢您阅读来自文章 https://www.codeproject.com/Articles/1111658/Fast-Deep-Copy-of-Objects-by-Expression-Trees-Csha 的不同方法性能比较。 - Artemious
@RyanLundy 我在某种程度上同意你的观点,但我认为成功编程的巨大部分是“只需记住/知道”一些东西(包括记住添加XML注释、使用哪些接口、反模式等)。无论您使用iCloneable还是iMyCustomDeepClone,您仍然必须处理在该接口的实现中哪些属性应该是深层复制,哪些应该是浅层复制,因为在许多情况下,属性级别不会全部是深层复制或全部是浅层复制。我的评论意图是,实现接口的人需要具备这种理解能力。 - BateTech
显示剩余5条评论

96

我更喜欢使用拷贝构造函数而非克隆。这样意图更加清晰。


7
.Net没有复制构造函数。 - Pop Catalin
57
当然可以:new MyObject(objToCloneFrom)。只需声明一个以要克隆的对象为参数的构造函数即可。 - Nick
37
不是同一回事,你需要手动将其添加到每个类中,而且你甚至不知道是否保证了深拷贝。 - Dave Van den Eynde
18
+1 用于复制构造函数。您还需要为每种类型的对象手动编写一个克隆(clone)函数,当您的类层次结构变得较深时,祝您好运。 - Andrew Grant
6
使用复制构造函数会导致层次结构丢失。http://www.agiledeveloper.com/articles/cloning072002.htm - Will
显示剩余8条评论

48

这是一个简单的扩展方法,可以复制所有公共属性。适用于任何对象,不需要将类标记为[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 ) } );
        }
    };
}

20
很遗憾,这个方法有缺陷。它相当于将objectOne.MyProperty = objectTwo.MyProperty(即,只会复制引用)。它不会克隆属性的值。 - Alex Norcliffe
1
致Alex Norcliffe:你是那个关于“复制每个属性”而不是克隆的问题提问者。在大多数情况下,不需要完全复制属性。 - Konstantin Salavatov
2
我在考虑使用递归的方式来实现这个方法。如果属性的值是一个引用,那么创建一个新对象并再次调用CopyTo方法。但我发现一个问题,就是所有被使用的类必须有一个无参构造函数。有人已经尝试过这种方法吗?我也想知道这是否适用于包含DataRow和DataTable等.NET类的属性。 - Koryu
作者要求深克隆,以便“对新对象进行更改,这些更改不会反映在原始对象中。” 这个答案创建了一个浅克隆,在克隆内对象的任何更改都将更改原始对象。 - Andrew

38
我刚刚创建了一个名为CloneExtensions library的项目。它使用表达式树运行时代码编译生成简单的赋值操作来执行快速、深度克隆。 如何使用? 不需要自己编写大量的字段和属性之间的赋值语句来定义CloneCopy方法,可以使用Expression Tree让程序自动完成。标记为扩展方法的GetClone<T>()方法允许您在实例上轻松调用它:
var newInstance = source.GetClone();

您可以使用CloningFlags枚举来选择从复制到newInstance的内容:
var newInstance 
    = source.GetClone(CloningFlags.Properties | CloningFlags.CollectionItems);

可以克隆什么?

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

以下类/结构成员在内部进行克隆:

  • 公共非只读字段的值
  • 同时具有获取和设置访问器的公共属性的值
  • 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 方法吗?


2
这个能否被放到NuGet上的机会有多大?它似乎是最好的解决方案。与NClone相比如何? - crush
我认为这个答案应该得到更多的赞同。手动实现 ICloneable 很繁琐且容易出错,如果性能很重要并且需要在短时间内复制数千个对象,则使用反射或序列化会很慢。 - nightcoder
完全不是这样,你对反射的理解有误,你应该适当地进行缓存。请查看我下面的答案: https://dev59.com/H3VD5IYBdhLWcg3wHn6d#34368738 - Roma Borodov

37

如果您已经使用第三方应用程序,如ValueInjectorAutomapper,则可以执行以下操作:

MyObject oldObj; // The existing object to clone

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

使用这种方法,您无需在对象上实现ISerializableICloneable。这在MVC/MVVM模式中很常见,因此已经创建了像这样的简单工具。

请参阅GitHub上的ValueInjecter深拷贝示例


36
最好的方法是实现一个扩展方法,如下所示:
public static T DeepClone<T>(this T originalObject)
{ /* the cloning code */ }

然后可以通过以下方式在解决方案中的任何位置使用它

var copy = anyObject.DeepClone();

我们可以有以下三种实现方式:
  1. 通过序列化(代码最短)
  2. 通过反射 - 速度快5倍
  3. 通过表达式树 - 速度快20倍
所有链接的方法都能正常工作并经过了深入测试。

2
ExpressionTree的实现看起来非常不错。它甚至可以处理循环引用和私有成员,无需属性。这是我找到的最佳答案。 - N73k
最佳答案,非常有效,你救了我的一天。 - Adel Mourad
@MrinalKamboj,这个错误有解决方案吗?因为我也遇到了同样的问题,当尝试克隆复杂对象时,迄今为止还没有找到解决方案。 - aca
1
@aca 当时我没有找到解决方案。 - Mrinal Kamboj

36

我在使用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);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接