我想做一些事情,比如:

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

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

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

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


当前回答

编辑:项目已停止

如果您想真正克隆到未知类型,可以查看fastclone。

这是基于表达式的克隆,其工作速度比二进制序列化快10倍,并保持完整的对象图完整性。

这意味着:如果多次引用层次结构中的同一对象,克隆也将引用一个实例。

不需要对正在克隆的对象进行接口、属性或任何其他修改。

其他回答

免责声明:我是上述包裹的作者。

我很惊讶2019年这个问题的顶级答案仍然使用序列化或反射。

序列化是有限的(需要属性、特定构造函数等),而且非常慢

BinaryFormatter需要Serializable属性,JsonConverter需要无参数构造函数或属性,既不能很好地处理只读字段或接口,两者都比需要的慢10-30x。

表达式树

您可以改为使用表达式树或Reflection.Emit仅生成一次克隆代码,然后使用编译后的代码,而不是缓慢的反射或序列化。

我自己遇到了这个问题,但没有找到令人满意的解决方案,因此我决定创建一个包,它可以做到这一点,并且可以处理所有类型的代码,而且速度几乎与自定义编写的代码一样快。

您可以在GitHub上找到该项目:https://github.com/marcelltoth/ObjectCloner

用法

您可以从NuGet安装它。获取ObjectCloner包并将其用作:

var clone = ObjectCloner.DeepClone(original);

或者如果您不介意用扩展名污染对象类型,请获取ObjectCloner.extensions,并写下:

var clone = original.DeepClone();

表演

克隆类层次结构的简单基准测试显示,性能比使用Reflection快约3倍,比Newtonsoft.Json序列化快约12倍,比强烈建议的BinaryFormatter快约36倍。

我对当前的答案做了一些基准测试,发现了一些有趣的事实。

使用BinarySerializer=>https://stackoverflow.com/a/78612/6338072

使用XmlSerializer=>https://stackoverflow.com/a/50150204/6338072

使用Activator.CreateInstance=>https://stackoverflow.com/a/56691124/6338072

这些是结果

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.18363.1734 (1909/November2019Update/19H2)

Intel Core i5-6200U CPU 2.30GHz(Skylake),1个CPU,4个逻辑核和2个物理核[主机]:.NET Framework 4.8(4.8.4400.0),X86 LegacyJIT默认作业:.NET Framework 4.8(4.8.4400.0),X86 LegacyJIT

Method Mean Error StdDev Gen 0 Allocated
BinarySerializer 220.69 us 4.374 us 9.963 us 49.8047 77 KB
XmlSerializer 182.72 us 3.619 us 9.405 us 21.9727 34 KB
Activator.CreateInstance 49.99 us 0.992 us 2.861 us 1.9531 3 KB

使用System.Text.Json:

https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-apis/

public static T DeepCopy<T>(this T source)
{
    return source == null ? default : JsonSerializer.Parse<T>(JsonSerializer.ToString(source));
}

新的API使用Span<T>。这应该很快,最好做一些基准测试。

注意:不需要像Json.NET那样使用ObjectCreationHandling。Replace,因为默认情况下它将替换集合值。您现在应该忘记Json.NET,因为所有的东西都将被新的官方API所取代。

我不确定这是否适用于私人领域。

C#9.0引入了需要记录的with关键字(谢谢Mark Nading)。这应该允许非常简单的对象克隆(如果需要,还可以进行突变),只需要很少的样板,但只需要一个记录。

您似乎无法通过将类放入通用记录来克隆(按值)类;

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

这写着“再见世界:()”-字符串是通过引用复制的(不需要)。https://dotnetfiddle.net/w3IJgG

(令人难以置信的是,上面的方法可以正确地用于结构!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

我将使用下面的简单方法来实现这一点。只需创建一个抽象类和实现方法以再次序列化和反序列化并返回。

public abstract class CloneablePrototype<T>
{
    public T DeepCopy()
    {
        string result = JsonConvert.SerializeObject(this);
        return JsonConvert.DeserializeObject<T>(result);
    }
}
public class YourClass : CloneablePrototype< YourClass>
…
…
…

然后像这样使用它来创建深度副本。

YourClass newObj = (YourClass)oldObj.DeepCopy();

如果您还需要实现浅层复制方法,则此解决方案也很容易扩展。

只需在抽象类中实现一个新方法。

public T ShallowCopy()
{
    return (T)this.MemberwiseClone();
}