深度克隆对象

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个回答

9
这里是一个深拷贝的实现方式:
public static object CloneObject(object opSource)
{
    //grab the type and create a new instance of that type
    Type opSourceType = opSource.GetType();
    object opTarget = CreateInstanceOfType(opSourceType);

    //grab the properties
    PropertyInfo[] opPropertyInfo = opSourceType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

    //iterate over the properties and if it has a 'set' method assign it from the source TO the target
    foreach (PropertyInfo item in opPropertyInfo)
    {
        if (item.CanWrite)
        {
            //value types can simply be 'set'
            if (item.PropertyType.IsValueType || item.PropertyType.IsEnum || item.PropertyType.Equals(typeof(System.String)))
            {
                item.SetValue(opTarget, item.GetValue(opSource, null), null);
            }
            //object/complex types need to recursively call this method until the end of the tree is reached
            else
            {
                object opPropertyValue = item.GetValue(opSource, null);
                if (opPropertyValue == null)
                {
                    item.SetValue(opTarget, null, null);
                }
                else
                {
                    item.SetValue(opTarget, CloneObject(opPropertyValue), null);
                }
            }
        }
    }
    //return the new item
    return opTarget;
}

2
这似乎是成员逐一克隆,因为它不知道引用类型属性。 - sll
1
如果你想要极快的性能,不要选择这个实现方式:它使用了反射,所以速度不会很快。相反地,“过早优化是万恶之源”,所以在运行分析器之前先忽略性能方面的问题。 - Contango
1
CreateInstanceOfType未定义? - Furkan Gözükara
它在整数上失败了:“非静态方法需要一个目标。” - Mr.B

9

我也见过通过反射实现这个功能的方法。基本上有一个方法会迭代对象的成员,并将它们适当地复制到新对象中。当它遇到引用类型或集合类型时,它会对自身进行递归调用。反射是比较耗费资源的,但它的效果还不错。


8

由于在不同项目中找不到符合我所有要求的克隆器,因此我创建了一个深度克隆器,可以配置和适应不同的代码结构,而不是调整我的代码以满足克隆器的要求。这是通过向将被克隆的代码添加注释来实现的,或者您可以将代码保留为默认行为。它使用反射、类型缓存,并基于fasterflect。与其他基于反射/序列化的算法相比,对于大量数据和高对象层次结构,克隆过程非常快速。

https://github.com/kalisohn/CloneBehave

还可以作为nuget包使用: https://www.nuget.org/packages/Clone.Behave/1.0.0

例如:以下代码将深层复制Address,但仅执行_currentJob字段的浅层复制。

public class Person 
{
  [DeepClone(DeepCloneBehavior.Shallow)]
  private Job _currentJob;      

  public string Name { get; set; }

  public Job CurrentJob 
  { 
    get{ return _currentJob; }
    set{ _currentJob = value; }
  }

  public Person Manager { get; set; }
}

public class Address 
{      
  public Person PersonLivingHere { get; set; }
}

Address adr = new Address();
adr.PersonLivingHere = new Person("John");
adr.PersonLivingHere.BestFriend = new Person("James");
adr.PersonLivingHere.CurrentJob = new Job("Programmer");

Address adrClone = adr.Clone();

//RESULT
adr.PersonLivingHere == adrClone.PersonLivingHere //false
adr.PersonLivingHere.Manager == adrClone.PersonLivingHere.Manager //false
adr.PersonLivingHere.CurrentJob == adrClone.PersonLivingHere.CurrentJob //true
adr.PersonLivingHere.CurrentJob.AnyProperty == adrClone.PersonLivingHere.CurrentJob.AnyProperty //true

8

代码生成器

在IT技术领域,我们见过很多关于序列化、手动实现以及反射的想法,但是我想提出一种全新的方法:使用CGbR代码生成器。生成的克隆方法在内存和CPU效率上都非常高,因此比标准DataContractSerializer快300倍。

你只需要一个带有ICloneable接口的部分类定义,剩下的工作由代码生成器完成:

public partial class Root : ICloneable
{
    public Root(int number)
    {
        _number = number;
    }
    private int _number;

    public Partial[] Partials { get; set; }

    public IList<ulong> Numbers { get; set; }

    public object Clone()
    {
        return Clone(true);
    }

    private Root()
    {
    }
} 

public partial class Root
{
    public Root Clone(bool deep)
    {
        var copy = new Root();
        // All value types can be simply copied
        copy._number = _number; 
        if (deep)
        {
            // In a deep clone the references are cloned 
            var tempPartials = new Partial[Partials.Length];
            for (var i = 0; i < Partials.Length; i++)
            {
                var value = Partials[i];
                value = value.Clone(true);
                tempPartials[i] = value;
            }
            copy.Partials = tempPartials;
            var tempNumbers = new List<ulong>(Numbers.Count);
            for (var i = 0; i < Numbers.Count; i++)
            {
                var value = Numbers[i];
                tempNumbers.Add(value);
            }
            copy.Numbers = tempNumbers;
        }
        else
        {
            // In a shallow clone only references are copied
            copy.Partials = Partials; 
            copy.Numbers = Numbers; 
        }
        return copy;
    }
}

注意:最新版本有更多的空值检查,但是为了更好地理解,我没有包括它们。


7

我喜欢这样的复制构造函数:

    public AnyObject(AnyObject anyObject)
    {
        foreach (var property in typeof(AnyObject).GetProperties())
        {
            property.SetValue(this, property.GetValue(anyObject));
        }
        foreach (var field in typeof(AnyObject).GetFields())
        {
            field.SetValue(this, field.GetValue(anyObject));
        }
    }

如果您要复制更多内容,请添加它们。

7
这个方法解决了我的问题:
private static MyObj DeepCopy(MyObj source)
        {

            var DeserializeSettings = new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace };

            return JsonConvert.DeserializeObject<MyObj >(JsonConvert.SerializeObject(source), DeserializeSettings);

        }

Use it like this: MyObj a = DeepCopy(b);


7

以下是一个快速且易于实现的解决方案,无需依赖Serialization/Deserialization(序列化/反序列化)。

public class MyClass
{
    public virtual MyClass DeepClone()
    {
        var returnObj = (MyClass)MemberwiseClone();
        var type = returnObj.GetType();
        var fieldInfoArray = type.GetRuntimeFields().ToArray();

        foreach (var fieldInfo in fieldInfoArray)
        {
            object sourceFieldValue = fieldInfo.GetValue(this);
            if (!(sourceFieldValue is MyClass))
            {
                continue;
            }

            var sourceObj = (MyClass)sourceFieldValue;
            var clonedObj = sourceObj.DeepClone();
            fieldInfo.SetValue(returnObj, clonedObj);
        }
        return returnObj;
    }
}

编辑:需要

    using System.Linq;
    using System.Reflection;

这是我如何使用它的方法

public MyClass Clone(MyClass theObjectIneededToClone)
{
    MyClass clonedObj = theObjectIneededToClone.DeepClone();
}

6
请按照以下步骤操作:
1. 定义一个 `ISelf` 接口,该接口包含一个只读的 `Self` 属性并返回类型为`T`。另外还需要定义一个 `ICloneable` 接口,该接口继承自 `ISelf` 并包含一个 `T Clone()` 方法。 2. 然后定义一个名为 `CloneBase` 的类型,该类型实现了一个受保护的虚拟泛型 `VirtualClone` 方法,将 `MemberwiseClone` 强制转换为传入的类型。 3. 每个派生类型应通过调用基础克隆方法来实现 `VirtualClone`,然后执行必要的操作以正确克隆父 VirtualClone 方法尚未处理的派生类型方面的内容。
为了最大化继承的灵活性,公开克隆功能的类应该是密封的,但是派生自一个除克隆之外其他完全相同的基类。而不是传递明确可克隆类型的变量,应采用类型为 `ICloneable` 的参数。这将允许期望派生自 `Foo` 的可克隆派生类与 `DerivedFoo` 的可克隆派生类一起工作,并且还允许创建非可克隆的 `Foo` 派生类。

5
C# 9.0引入了“with”关键字,需要使用“record”(感谢Mark Nading)。这应该允许非常简单的对象克隆(如果需要,还可以进行变异),并且只需要很少的样板文件,但是只能使用“record”进行操作。
您似乎无法通过将类放入通用的“record”中来克隆(按值)。
using System;
                
public class Program
{
    public class Example
    {
        public string A { get; set; }
    }
    
    public record ClonerRecord<T>(T a)
    {
    }

    public static void Main()
    {
        var foo = new Example {A = "Hello World"};
        var bar = (new ClonerRecord<Example>(foo) with {}).a;
        foo.A = "Goodbye World :(";
        Console.WriteLine(bar.A);
    }
}

这是通过引用复制字符串(不希望的方式):"Goodbye World :()"。https://dotnetfiddle.net/w3IJgG 令人难以置信的是,上述代码在使用 struct 时可以正常工作!https://dotnetfiddle.net/469NJv 但是,克隆记录似乎按预期工作,通过值进行克隆。
using System;

public class Program
{
    public record Example
    {
        public string A { get; set; }
    }
    
    public static void Main()
    {
        var foo = new Example {A = "Hello World"};
        var bar = foo with {};
        foo.A = "Goodbye World :(";
        Console.WriteLine(bar.A);
    }
}

这将返回"Hello World",该字符串按值复制! https://dotnetfiddle.net/MCHGEL 更多信息可以在博客文章中找到:

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/with-expression


1
根据我所读的,这仅适用于新的“记录”类型。 我们中的一个人应该在.net fiddle上尝试一下这个:P - Mark Nadig
@MarkNadig 我甚至没有注意到这个问题!看起来使用 record 克隆一个 class 是不起作用的- https://dotnetfiddle.net/w3IJgG;但是对于一个扁平的 record 进行克隆似乎可以按值复制!https://dotnetfiddle.net/MCHGEL - Izzy

5
在我所使用的代码库中,我们有一个来自GitHub项目Burtsev-Alexey/net-object-deep-copy的文件ObjectExtensions.cs的副本。它已经有9年历史了。虽然它能够工作,但我们后来意识到对于较大的对象结构来说它非常缓慢。
相反,我们在GitHub项目jpmikkers/Baksteen.Extensions.DeepCopy中找到了该文件的一个分支。对于之前需要大约30分钟才能完成的大型数据结构的深拷贝操作,现在几乎瞬间就能完成。
这个改进版本有以下文档:

C# extension method for fast object cloning.

This is a speed-optimized fork of Alexey Burtsev's deep copier. Depending on your usecase, this will be 2x - 3x faster than the original. It also fixes some bugs which are present in the original code. Compared to the classic binary serialization/deserialization deep clone technique, this version is about seven times faster (the more arrays your objects contain, the bigger the speedup factor).

The speedup is achieved via the following techniques:

  • object reflection results are cached
  • don't deep copy primitives or immutable structs & classes (e.g. enum and string)
  • to improve locality of reference, process the 'fast' dimensions or multidimensional arrays in the inner loops
  • use a compiled lamba expression to call MemberwiseClone

How to use:

using Baksteen.Extensions.DeepCopy;
...
var myobject = new SomeClass();
...
var myclone = myobject.DeepCopy()!;    // creates a new deep copy of the original object 

Note: the exclamation mark (null-forgiving operator) is only required if you enabled nullable referency types in your project


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