我想做一些事情,比如:

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

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

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

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


当前回答

基本上,您需要实现ICloneable接口,然后实现对象结构复制。如果它是所有成员的深度拷贝,您需要确保(与您选择的解决方案无关)所有子级都是可克隆的。有时,在这个过程中,您需要注意一些限制,例如,如果您复制ORM对象,大多数框架只允许一个对象附加到会话,并且您不能克隆该对象,或者如果可能,您需要关注这些对象的会话附加。

干杯

其他回答

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

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

我通常只做一个Copy方法,它完全符合我的要求。

在我使用的代码库中,我们有一个来自GitHub项目Burtsev Alexey/net对象深度副本的ObjectExtension.cs文件副本。它已经9岁了。虽然我们后来意识到,对于更大的对象结构来说,它是非常慢的,但它还是有效的。

相反,我们在GitHub项目jpmikkers/Baksteen.Extensions.DeepCopy中找到了ObjectExtension.cs文件的一个分支。以前对大型数据结构的深度复制操作需要大约30分钟,现在感觉几乎是瞬间完成的。

此改进版本包含以下文档:

用于快速对象克隆的C#扩展方法。这是Alexey Burtsev深度复印机的速度优化叉。根据您的使用情况,这将比原始版本快2倍-3倍。它还修复了原始代码中存在的一些错误。与经典的二进制序列化/反序列化深度克隆技术相比,此版本的速度大约快七倍(对象包含的数组越多,加速因子越大)。通过以下技术实现加速:缓存对象反射结果不要深度复制原语或不可变结构&类(例如枚举和字符串)为了提高引用的局部性,处理内部循环中的“快速”维度或多维数组使用编译的lambda表达式调用MemberwiseClone如何使用:使用Baksteen.Extensions.DeepCopy;...var myobject=新建SomeClass();...var myclone=myobject.DepCopy()!;//创建原始对象的新深度副本注意:只有在项目中启用了可为null的引用类型时,才需要感叹号(null原谅运算符)

基于@craastad的答案,对于派生类。

在最初的答案中,如果调用者在基类对象上调用DeepCopy,则克隆的对象属于基类。但下面的代码将返回派生类。

using Newtonsoft.Json;

public static T DeepCopy<T>(this T source)
{
    return (T)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(source), source.GetType());
}

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

有关使用ICloneable克隆的详细说明,请参阅本文。

长篇大论的答案是“视情况而定”。正如其他人所提到的,ICloneable不受泛型支持,需要对循环类引用进行特殊考虑,并且实际上被一些人视为.NET Framework中的“错误”。序列化方法取决于对象是否可序列化,而这些对象可能不可序列化,并且您可能无法控制。对于哪种做法是“最佳”做法,社会上仍有很多争论。事实上,没有一个解决方案是适用于所有情况的一刀切的最佳实践,就像ICloneable最初被解释的那样。

请参阅本开发者角文章,了解更多选项(归功于Ian)。

我找到了一种新的方法,那就是Emit。

我们可以使用Emit将IL添加到应用程序并运行它。但我认为这不是一个好方法,因为我想完善这一点,所以我写了我的答案。

Emit可以看到官方文件和指南

你应该学习一些IL来阅读代码。我将编写可以在类中复制属性的代码。

public static class Clone
{        
    // ReSharper disable once InconsistentNaming
    public static void CloneObjectWithIL<T>(T source, T los)
    {
        //see http://lindexi.oschina.io/lindexi/post/C-%E4%BD%BF%E7%94%A8Emit%E6%B7%B1%E5%85%8B%E9%9A%86/
        if (CachedIl.ContainsKey(typeof(T)))
        {
            ((Action<T, T>) CachedIl[typeof(T)])(source, los);
            return;
        }
        var dynamicMethod = new DynamicMethod("Clone", null, new[] { typeof(T), typeof(T) });
        ILGenerator generator = dynamicMethod.GetILGenerator();

        foreach (var temp in typeof(T).GetProperties().Where(temp => temp.CanRead && temp.CanWrite))
        {
            //do not copy static that will except
            if (temp.GetAccessors(true)[0].IsStatic)
            {
                continue;
            }

            generator.Emit(OpCodes.Ldarg_1);// los
            generator.Emit(OpCodes.Ldarg_0);// s
            generator.Emit(OpCodes.Callvirt, temp.GetMethod);
            generator.Emit(OpCodes.Callvirt, temp.SetMethod);
        }
        generator.Emit(OpCodes.Ret);
        var clone = (Action<T, T>) dynamicMethod.CreateDelegate(typeof(Action<T, T>));
        CachedIl[typeof(T)] = clone;
        clone(source, los);
    }

    private static Dictionary<Type, Delegate> CachedIl { set; get; } = new Dictionary<Type, Delegate>();
}

代码可以是深度复制,但它可以复制属性。如果你想让它成为深度副本,你可以改变它,因为IL太难了,我做不到。