深度克隆对象

问:

我想做类似的事情:

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

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

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

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

答1:

huntsbot.com – 高效赚钱,自由工作

一种方法是实现 ICloneable 接口(描述为 here,因此我不会重复),这是我不久前在 The Code Project 上找到的一个很好的深度克隆对象复制器,并将其合并到我们的代码中。如其他地方所述,它要求您的对象是可序列化的。

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

/// 
/// 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.
/// 
public static class ObjectCopier
{
    /// 
    /// Perform a deep copy of the object via serialization.
    /// 
    /// The type of object being copied.
    /// The object instance to copy.
    /// A deep copy of the object.
    public static T Clone(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 的新 extension methods,请将方法更改为具有以下签名:

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

现在方法调用简单地变为 objectBeingCloned.Clone();。

编辑(2015 年 1 月 10 日)我想我会重新审视这一点,提到我最近开始使用 (Newtonsoft) Json 来执行此操作,它should be更轻,并且避免了 [Serializable] 标记的开销。 (NB @atconway 在评论中指出私有成员不使用 JSON 方法克隆)

/// 
/// Perform a deep Copy of the object, using Json as a serialization method. NOTE: Private members are not cloned using this method.
/// 
/// The type of object being copied.
/// The object instance to copy.
/// The copied object.
public static T CloneJson(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(JsonConvert.SerializeObject(source), deserializeSettings);
}

stackoverflow.com/questions/78536/cloning-objects-in-c/… 有一个指向上述代码的链接 [并引用了另外两个这样的实现,其中一个更适合我的上下文]

序列化/反序列化涉及大量不必要的开销。请参阅 C# 中的 ICloneable 接口和 .MemberWise() 克隆方法。

@David,当然,但是如果对象很轻,并且使用它时的性能对您的要求来说不是太高,那么这是一个有用的提示。我承认,我没有在循环中大量使用它来处理大量数据,但我从未见过任何性能问题。

@Amir:实际上,不:如果类型已用 [Serializable] 属性标记,typeof(T).IsSerializable 也是如此。它不必实现 ISerializable 接口。

只是想我会提到,虽然这种方法很有用,而且我自己也用过很多次,但它与 Medium Trust 完全不兼容——所以要注意你是否正在编写需要兼容性的代码。 BinaryFormatter 访问私有字段,因此不能在部分信任环境的默认权限集中工作。您可以尝试另一个序列化程序,但请确保您的调用者知道如果传入对象依赖于私有字段,则克隆可能并不完美。

答2:

与HuntsBot一起,探索全球自由职业机会–huntsbot.com

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

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

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

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

解决方案比 BinaryFormatter 解决方案更快,.NET Serialization Performance Comparison

谢谢你。我可以用 C# 的 MongoDB 驱动程序附带的 BSON 序列化程序做同样的事情。

这对我来说是最好的方法,但是,我使用 Newtonsoft.Json.JsonConvert 但它是一样的

为此,要克隆的对象需要是可序列化的,如前所述 - 这也意味着例如它可能没有循环依赖项

我认为这是最好的解决方案,因为该实现可以应用于大多数编程语言。

答3:

打造属于自己的副业,开启自由职业之旅,从huntsbot.com开始!

不使用 ICloneable 的原因是 not,因为它没有通用接口。 The reason not to use it is because it’s vague。不清楚你得到的是浅拷贝还是深拷贝。这取决于实施者。

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

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

我不清楚为什么 ICloneable 被认为是模糊的。给定一个像 Dictionary(Of T,U) 这样的类型,我希望 ICloneable.Clone 应该执行任何级别的深浅复制,以使新字典成为包含相同 T 和 U 的独立字典(结构内容,和/或对象引用)作为原始文件。哪里来的暧昧?可以肯定的是,继承了包含“Self”方法的 ISelf(Of T) 的通用 ICloneable(Of T) 会好得多,但我认为深克隆与浅克隆没有歧义。

你的例子说明了这个问题。假设您有一个 Dictionary。克隆的 Dictionary 是否应该与原始 Dictionary 具有相同的 Customer 对象,或者这些 Customer 对象的副本?任何一个都有合理的用例。但 ICloneable 并不清楚你会得到哪一个。这就是为什么它没有用。

@Kyralessa Microsoft MSDN 文章实际上说明了这个问题,即不知道您是在请求深拷贝还是浅拷贝。

来自重复 stackoverflow.com/questions/129389/… 的答案描述了基于递归 MembershipClone 的复制扩展

答4:

huntsbot.com – 程序员副业首选,一站式外包任务、远程工作、创意产品分享订阅平台。

在大量阅读了此处链接的许多选项以及此问题的可能解决方案之后,我相信 all the options are summarized pretty well at Ian P’s link(所有其他选项都是这些选项的变体),并且Pedro77’s link 在问题评论中提供了最佳解决方案。

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

用 C 语言克隆对象的最佳方法!

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

手动使用 ICloneable,它是浅层的,不是类型安全的

MemberwiseClone,它使用 ICloneable

使用 Activator.CreateInstance 和递归 MemberwiseClone 进行反射

序列化,正如 johnc 的首选答案所指出的那样

中级语言,我不知道它是如何工作的

扩展方法,例如 Havard Straden 的这个自定义克隆框架

表达式树

article Fast Deep Copy by Expression Trees 还具有通过序列化、反射和表达式树进行克隆的性能比较。

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

Mr Venkat Subramaniam (redundant link here) explains in much detail why。

他的所有文章都围绕着一个试图适用于大多数情况的示例,使用 3 个对象:Person、Brain 和 City。我们想克隆一个人,它有自己的大脑,但在同一个城市。您可以描绘上述任何其他方法可能带来的所有问题,也可以阅读本文。

这是我对他的结论稍作修改的版本:

通过指定 New 后跟类名来复制对象通常会导致代码不可扩展。使用克隆,原型模式的应用,是实现这一点的更好方法。但是,使用 C#(和 Java)中提供的 clone 也可能存在很大问题。最好提供一个受保护的(非公共)复制构造函数并从 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

请注意,如果我们保持对象数量的计数,则此处实现的克隆将保持对象数量的正确计数。

MS 建议不要将 ICloneable 用于公共成员。 “由于 Clone 的调用者不能依赖于执行可预测克隆操作的方法,我们建议不要在公共 API 中实现 ICloneable。” msdn.microsoft.com/en-us/library/… 但是,根据 Venkat Subramaniam 在您的链接文章中给出的解释,我认为在这种情况下使用是有意义的只要 ICloneable 对象的创建者对哪些属性应该深入了解vs. 浅拷贝(即深拷贝Brain,浅拷贝City)

首先,我远不是这个主题(公共 API)的专家。我认为这一次 MS 的评论很有意义。而且我认为假设该 API 的用户会有如此深刻的理解是不安全的。因此,只有在公共 API 上实现它对于使用它的人来说真的无关紧要时才有意义。我想有某种 UML 非常明确地对每个属性进行区分可能会有所帮助。但我想听听有更多经验的人的意见。 :P

您可以使用 CGbR Clone Generator 并获得类似的结果,而无需手动编写代码。

中间语言实现很有用

C#中没有final

答5:

huntsbot.com洞察每一个产品背后的需求与收益,从而捕获灵感

我更喜欢复制构造函数而不是克隆。意图更明确。

.Net 没有复制构造函数。

当然可以: new MyObject(objToCloneFrom) 只需声明一个将要克隆的对象作为参数的 ctor。

这不是一回事。您必须手动将其添加到每个课程中,您甚至不知道您是否要保证深层副本。

+1 用于复制 ctor。您还必须为每种类型的对象手动编写一个 clone() 函数,当您的类层次结构深入几级时,祝您好运。

但是,使用复制构造函数,您会失去层次结构。 agiledeveloper.com/articles/cloning072002.htm

答6:

huntsbot.com – 程序员副业首选,一站式外包任务、远程工作、创意产品分享订阅平台。

复制所有公共属性的简单扩展方法。适用于任何对象并且不要求类为 [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 ) } );
        }
    };
}

不幸的是,这是有缺陷的。它相当于调用 objectOne.MyProperty = objectTwo.MyProperty(即,它只会复制引用)。它不会克隆属性的值。

致亚历克斯·诺克利夫:问题的作者询问“复制每个属性”而不是克隆。在大多数情况下,不需要精确复制属性。

我考虑使用这种方法,但使用递归。因此,如果属性的值是引用,请创建一个新对象并再次调用 CopyTo。我只看到一个问题,所有使用的类都必须有一个没有参数的构造函数。有人试过这个吗?我还想知道这是否真的适用于包含 .net 类(如 DataRow 和 DataTable)的属性?

作者要求进行深度克隆,以便他们可以“对未反映在原始对象中的新对象进行更改”。这个答案创建了一个浅克隆,其中对克隆中对象的任何更改都会更改原始内容。

答7:

huntsbot.com全球7大洲远程工作机会,探索不一样的工作方式

我刚刚创建了 CloneExtensions library 项目。它使用表达式树运行时代码编译生成的简单赋值操作执行快速、深度克隆。

如何使用它?

无需编写自己的 Clone 或 Copy 方法,在字段和属性之间进行分配,而是使用表达式树让程序自己完成。 GetClone() 标记为扩展方法的方法允许您在实例上简单地调用它:

var newInstance = source.GetClone();

您可以使用 CloningFlags 枚举选择应从 source 复制到 newInstance 的内容:

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

什么可以克隆?

Primitive(int、uint、byte、double、char 等)、已知的不可变类型(DateTime、TimeSpan、String)和委托(包括 Action、Func 等)

可空的

T[] 数组

自定义类和结构,包括泛型类和结构。

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

公共而非只读字段的值

具有 get 和 set 访问器的公共属性的值

实现 ICollection 的类型的集合项

它有多快?

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

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

和更多…

在 documentation 上阅读有关生成表达式的更多信息。

List 的示例表达式调试列表:

.Lambda #Lambda1(
    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.Collections.Generic.List`1[System.Int32]))
        ) {
            $target = (System.Collections.Generic.List`1[System.Int32]).Call ($initializers.Item[.Constant(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(Fields)) == (System.Byte).Constant(Fields)
        ) {
            .Default(System.Void)
        } .Else {
            .Default(System.Void)
        };
        .If (
            ((System.Byte)$flags & (System.Byte).Constant(Properties)) == (System.Byte).Constant(Properties)
        ) {
            .Block() {
                $target.Capacity = .Call CloneExtensions.CloneFactory.GetClone(
                    $source.Capacity,
                    $flags,
                    $initializers)
            }
        } .Else {
            .Default(System.Void)
        };
        .If (
            ((System.Byte)$flags & (System.Byte).Constant(CollectionItems)) == (System.Byte).Constant(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))
        target = (List)initializers[typeof(List)].Invoke((object)source);
    else
        target = new List();

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

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

    return target;
}

这不是很像您为 List 编写自己的 Clone 方法吗?

这在 NuGet 上的机会有多大?这似乎是最好的解决方案。它与NClone相比如何?

我认为这个答案应该被更多次投票。手动实现 ICloneable 繁琐且容易出错,如果性能很重要并且需要在短时间内复制数千个对象,则使用反射或序列化会很慢。

一点也不,你对反射有误,你应该正确地缓存它。在 stackoverflow.com/a/34368738/4711853 下方查看我的答案

答8:

huntsbot.com洞察每一个产品背后的需求与收益,从而捕获灵感

如果您已经在使用像 ValueInjecter 或 Automapper 这样的第三方应用程序,您可以执行以下操作:

MyObject oldObj; // The existing object to clone

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

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

见the ValueInjecter deep cloning sample on GitHub。

答9:

huntsbot.com高效搞钱,一站式跟进超10+任务平台外包需求

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

static public class SerializeHelper
{
    //Michael White, Holly Springs Consulting, 2009
    //michael@hollyspringsconsulting.com
    public static T DeserializeXML(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 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 objClone) 
        where T:new()
    {
        string GetString = SerializeHelper.SeralizeObjectToXML(objClone);
        return SerializeHelper.DeserializeXML(GetString);
    }
}

答10:

huntsbot.com提供全网独家一站式外包任务、远程工作、创意产品分享与订阅服务!

最好的方法是实现一个扩展方法,比如

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

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

var copy = anyObject.DeepClone();

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

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

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

使用您已发布 codeproject.com/Articles/1111658/… 的表达式树克隆代码,使用较新版本的 .Net 框架失败并出现安全异常,操作可能会破坏运行时,这基本上是由于表达式树格式错误而导致的异常,用于在运行时生成 Func,请检查您是否有解决方案。事实上,我只看到层次结构较深的复杂对象存在问题,简单的对象很容易被复制

ExpressionTree 的实现似乎非常好。它甚至适用于循环引用和私有成员。不需要属性。我找到的最佳答案。

最佳答案,效果很好,你拯救了我的一天

答11:

保持自己快人一步,享受全网独家提供的一站式外包任务、远程工作、创意产品订阅服务–huntsbot.com

简短的回答是您从 ICloneable 接口继承,然后实现 .clone 函数。克隆应该进行成员复制并对任何需要它的成员执行深度复制,然后返回结果对象。这是一个递归操作(它要求您要克隆的类的所有成员都是值类型或实现 ICloneable,并且它们的成员是值类型或实现 ICloneable,等等)。

有关使用 ICloneable 进行克隆的更详细说明,请查看 this article。

long 的答案是“视情况而定”。正如其他人所提到的,ICloneable 不受泛型支持,需要对循环类引用进行特殊考虑,并且实际上被某些人视为 .NET Framework 中的 “mistake”。序列化方法取决于您的对象是可序列化的,它们可能不是并且您可能无法控制。社区中仍然存在很多关于哪种是“最佳”实践的争论。实际上,没有一个解决方案是一刀切的最佳实践,适用于 ICloneable 最初被解释为的所有情况。

有关更多选项,请参阅此 Developer’s Corner article(感谢 Ian)。

ICloneable 没有通用接口,因此不建议使用该接口。

您的解决方案一直有效,直到它需要处理循环引用,然后事情开始复杂化,最好尝试使用深度序列化实现深度克隆。

不幸的是,也不是所有的对象都是可序列化的,所以你也不能总是使用那个方法。伊恩的链接是迄今为止最全面的答案。

原文链接:https://www.huntsbot.com/qa/aL86/deep-cloning-objects?lang=zh_CN&from=csdn

huntsbot.com – 高效赚钱,自由工作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值