我想做一些事情,比如:

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

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

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

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


当前回答

通常,您实现ICloneable接口并自己实现克隆。C#对象有一个内置的MemberwiseColone方法,该方法执行浅层复制,可以帮助您处理所有原语。

对于深度复制,它无法知道如何自动执行。

其他回答

通常,您实现ICloneable接口并自己实现克隆。C#对象有一个内置的MemberwiseColone方法,该方法执行浅层复制,可以帮助您处理所有原语。

对于深度复制,它无法知道如何自动执行。

另一个JSON.NET答案。此版本适用于不实现ISerializable的类。

public static class Cloner
{
    public static T Clone<T>(T source)
    {
        if (ReferenceEquals(source, null))
            return default(T);

        var settings = new JsonSerializerSettings { ContractResolver = new ContractResolver() };

        return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(source, settings), settings);
    }

    class ContractResolver : DefaultContractResolver
    {
        protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
        {
            var props = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                .Select(p => base.CreateProperty(p, memberSerialization))
                .Union(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                    .Select(f => base.CreateProperty(f, memberSerialization)))
                .ToList();
            props.ForEach(p => { p.Writable = true; p.Readable = true; });
            return props;
        }
    }
}

我已经创建了一个可接受答案的版本,该版本同时适用于“[Serializable]”和“[DataContract]”。我写它已经有一段时间了,但如果我没有记错,[DataContract]需要一个不同的序列化程序。

需要System、System.IO、System.Runtime.Serialization、System.Runtime.Serialization.Formatters.Binary、System.Xml;

public static class ObjectCopier
{

    /// <summary>
    /// Perform a deep Copy of an object that is marked with '[Serializable]' or '[DataContract]'
    /// </summary>
    /// <typeparam name="T">The type of object being copied.</typeparam>
    /// <param name="source">The object instance to copy.</param>
    /// <returns>The copied object.</returns>
    public static T Clone<T>(T source)
    {
        if (typeof(T).IsSerializable == true)
        {
            return CloneUsingSerializable<T>(source);
        }

        if (IsDataContract(typeof(T)) == true)
        {
            return CloneUsingDataContracts<T>(source);
        }

        throw new ArgumentException("The type must be Serializable or use DataContracts.", "source");
    }


    /// <summary>
    /// Perform a deep Copy of an object that is marked with '[Serializable]'
    /// </summary>
    /// <remarks>
    /// Found on http://stackoverflow.com/questions/78536/cloning-objects-in-c-sharp
    /// Uses code found on CodeProject, which allows free use in third party apps
    /// - http://www.codeproject.com/KB/tips/SerializedObjectCloner.aspx
    /// </remarks>
    /// <typeparam name="T">The type of object being copied.</typeparam>
    /// <param name="source">The object instance to copy.</param>
    /// <returns>The copied object.</returns>
    public static T CloneUsingSerializable<T>(T source)
    {
        if (!typeof(T).IsSerializable)
        {
            throw new ArgumentException("The type must be serializable.", "source");
        }

        // Don't serialize a null object, simply return the default for that object
        if (Object.ReferenceEquals(source, null))
        {
            return default(T);
        }

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


    /// <summary>
    /// Perform a deep Copy of an object that is marked with '[DataContract]'
    /// </summary>
    /// <typeparam name="T">The type of object being copied.</typeparam>
    /// <param name="source">The object instance to copy.</param>
    /// <returns>The copied object.</returns>
    public static T CloneUsingDataContracts<T>(T source)
    {
        if (IsDataContract(typeof(T)) == false)
        {
            throw new ArgumentException("The type must be a data contract.", "source");
        }

        // ** Don't serialize a null object, simply return the default for that object
        if (Object.ReferenceEquals(source, null))
        {
            return default(T);
        }

        DataContractSerializer dcs = new DataContractSerializer(typeof(T));
        using(Stream stream = new MemoryStream())
        {
            using (XmlDictionaryWriter writer = XmlDictionaryWriter.CreateBinaryWriter(stream))
            {
                dcs.WriteObject(writer, source);
                writer.Flush();
                stream.Seek(0, SeekOrigin.Begin);
                using (XmlDictionaryReader reader = XmlDictionaryReader.CreateBinaryReader(stream, XmlDictionaryReaderQuotas.Max))
                {
                    return (T)dcs.ReadObject(reader);
                }
            }
        }
    }


    /// <summary>
    /// Helper function to check if a class is a [DataContract]
    /// </summary>
    /// <param name="type">The type of the object to check.</param>
    /// <returns>Boolean flag indicating if the class is a DataContract (true) or not (false) </returns>
    public static bool IsDataContract(Type type)
    {
        object[] attributes = type.GetCustomAttributes(typeof(DataContractAttribute), false);
        return attributes.Length == 1;
    }

} 

使用IClonable接口可以花费多少精力是令人难以置信的,尤其是当您有大量的类层次结构时。MemberwiseColone的工作方式也很奇怪——它甚至不能完全克隆普通的List类型的结构。

当然,串行化最有趣的困境是串行化反向引用——例如,具有子-父关系的类层次结构。我怀疑二进制序列化程序能否在这种情况下帮助您。(最终将导致递归循环+堆栈溢出)。

不知怎么的,我喜欢这里提出的解决方案:如何在.NET(特别是C#)中对对象进行深度复制?

然而,它不支持Lists,并补充说,该支持还考虑到了重新养育子女的问题。对于我制定的仅为父项的规则,该字段或属性应命名为“parent”,则DeepClone将忽略它。您可能需要决定自己的反向引用规则——对于树层次结构,它可能是“左/右”等。。。

以下是包含测试代码的完整代码片段:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;

namespace TestDeepClone
{
    class Program
    {
        static void Main(string[] args)
        {
            A a = new A();
            a.name = "main_A";
            a.b_list.Add(new B(a) { name = "b1" });
            a.b_list.Add(new B(a) { name = "b2" });

            A a2 = (A)a.DeepClone();
            a2.name = "second_A";

            // Perform re-parenting manually after deep copy.
            foreach( var b in a2.b_list )
                b.parent = a2;


            Debug.WriteLine("ok");

        }
    }

    public class A
    {
        public String name = "one";
        public List<String> list = new List<string>();
        public List<String> null_list;
        public List<B> b_list = new List<B>();
        private int private_pleaseCopyMeAsWell = 5;

        public override string ToString()
        {
            return "A(" + name + ")";
        }
    }

    public class B
    {
        public B() { }
        public B(A _parent) { parent = _parent; }
        public A parent;
        public String name = "two";
    }


    public static class ReflectionEx
    {
        public static Type GetUnderlyingType(this MemberInfo member)
        {
            Type type;
            switch (member.MemberType)
            {
                case MemberTypes.Field:
                    type = ((FieldInfo)member).FieldType;
                    break;
                case MemberTypes.Property:
                    type = ((PropertyInfo)member).PropertyType;
                    break;
                case MemberTypes.Event:
                    type = ((EventInfo)member).EventHandlerType;
                    break;
                default:
                    throw new ArgumentException("member must be if type FieldInfo, PropertyInfo or EventInfo", "member");
            }
            return Nullable.GetUnderlyingType(type) ?? type;
        }

        /// <summary>
        /// Gets fields and properties into one array.
        /// Order of properties / fields will be preserved in order of appearance in class / struct. (MetadataToken is used for sorting such cases)
        /// </summary>
        /// <param name="type">Type from which to get</param>
        /// <returns>array of fields and properties</returns>
        public static MemberInfo[] GetFieldsAndProperties(this Type type)
        {
            List<MemberInfo> fps = new List<MemberInfo>();
            fps.AddRange(type.GetFields());
            fps.AddRange(type.GetProperties());
            fps = fps.OrderBy(x => x.MetadataToken).ToList();
            return fps.ToArray();
        }

        public static object GetValue(this MemberInfo member, object target)
        {
            if (member is PropertyInfo)
            {
                return (member as PropertyInfo).GetValue(target, null);
            }
            else if (member is FieldInfo)
            {
                return (member as FieldInfo).GetValue(target);
            }
            else
            {
                throw new Exception("member must be either PropertyInfo or FieldInfo");
            }
        }

        public static void SetValue(this MemberInfo member, object target, object value)
        {
            if (member is PropertyInfo)
            {
                (member as PropertyInfo).SetValue(target, value, null);
            }
            else if (member is FieldInfo)
            {
                (member as FieldInfo).SetValue(target, value);
            }
            else
            {
                throw new Exception("destinationMember must be either PropertyInfo or FieldInfo");
            }
        }

        /// <summary>
        /// Deep clones specific object.
        /// Analogue can be found here: https://stackoverflow.com/questions/129389/how-do-you-do-a-deep-copy-an-object-in-net-c-specifically
        /// This is now improved version (list support added)
        /// </summary>
        /// <param name="obj">object to be cloned</param>
        /// <returns>full copy of object.</returns>
        public static object DeepClone(this object obj)
        {
            if (obj == null)
                return null;

            Type type = obj.GetType();

            if (obj is IList)
            {
                IList list = ((IList)obj);
                IList newlist = (IList)Activator.CreateInstance(obj.GetType(), list.Count);

                foreach (object elem in list)
                    newlist.Add(DeepClone(elem));

                return newlist;
            } //if

            if (type.IsValueType || type == typeof(string))
            {
                return obj;
            }
            else if (type.IsArray)
            {
                Type elementType = Type.GetType(type.FullName.Replace("[]", string.Empty));
                var array = obj as Array;
                Array copied = Array.CreateInstance(elementType, array.Length);

                for (int i = 0; i < array.Length; i++)
                    copied.SetValue(DeepClone(array.GetValue(i)), i);

                return Convert.ChangeType(copied, obj.GetType());
            }
            else if (type.IsClass)
            {
                object toret = Activator.CreateInstance(obj.GetType());

                MemberInfo[] fields = type.GetFieldsAndProperties();
                foreach (MemberInfo field in fields)
                {
                    // Don't clone parent back-reference classes. (Using special kind of naming 'parent' 
                    // to indicate child's parent class.
                    if (field.Name == "parent")
                    {
                        continue;
                    }

                    object fieldValue = field.GetValue(obj);

                    if (fieldValue == null)
                        continue;

                    field.SetValue(toret, DeepClone(fieldValue));
                }

                return toret;
            }
            else
            {
                // Don't know that type, don't know how to clone it.
                if (Debugger.IsAttached)
                    Debugger.Break();

                return null;
            }
        } //DeepClone
    }

}

在大量阅读了这里链接的许多选项以及这个问题的可能解决方案之后,我相信所有选项都在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

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