我想做一些事情,比如:

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

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

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

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


当前回答

在大量阅读了这里链接的许多选项以及这个问题的可能解决方案之后,我相信所有选项都在Ian P的链接中得到了很好的总结(所有其他选项都是这些选项的变体),Pedro77的问题评论链接提供了最佳解决方案。

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

在C sharp中克隆对象的最佳方法!

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

手动使用ICloneable,它是浅层的,不属于类型安全MemberwiseColone,它使用ICloneable使用Activator.CreateInstance和递归MemberwiseClone进行反射正如johnc的首选答案所指出的序列化中级语言,我不知道它是如何工作的扩展方法,如Havard Straden的自定义克隆框架表达式树

通过表达式树进行快速深度复制一文还对通过序列化、反射和表达式树进行克隆的性能进行了比较。

为什么选择ICloneable(即手动)

Venkat Subramaniam先生(此处为冗余链接)详细解释了原因。

他的所有文章都围绕着一个例子,试图适用于大多数情况,使用三个对象:人、大脑和城市。我们想克隆一个人,这个人将拥有自己的大脑,但同样的城市。你可以想象上面任何其他方法都会带来的所有问题,也可以阅读本文。

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

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

注意,如果我们对对象的数量进行计数,这里实现的克隆将保持正确的对象数量计数。

其他回答

这里有一个快速而简单的解决方案,不依赖于序列化/反序列化。

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

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

我找到了一种新的方法,那就是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太难了,我做不到。

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

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

using Newtonsoft.Json;

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

当使用Marc Gravells protobuf net作为序列化程序时,接受的答案需要一些轻微的修改,因为要复制的对象不会被归因于[Serializable],因此不可序列化,Clone方法将抛出异常。我修改了它以与protobuf net一起使用:

public static T Clone<T>(this T source)
{
    if(Attribute.GetCustomAttribute(typeof(T), typeof(ProtoBuf.ProtoContractAttribute))
           == null)
    {
        throw new ArgumentException("Type has no ProtoContract!", "source");
    }

    if(Object.ReferenceEquals(source, null))
    {
        return default(T);
    }

    IFormatter formatter = ProtoBuf.Serializer.CreateFormatter<T>();
    using (Stream stream = new MemoryStream())
    {
        formatter.Serialize(stream, source);
        stream.Seek(0, SeekOrigin.Begin);
        return (T)formatter.Deserialize(stream);
    }
}

这将检查[ProtoControl]属性的存在,并使用protobufs自己的格式化程序来序列化对象。