深度克隆对象

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

5
由于此问题的几乎所有答案都不令人满意或在我的情况下根本无法使用,因此我编写了 AnyClone,它完全采用反射实现,并解决了所有这里的需求。在一个具有复杂结构的复杂场景中,我无法使序列化工作,而 IClonable 不是最理想的解决方案 - 实际上它甚至不应该是必要的。

支持标准忽略属性,使用 [IgnoreDataMember][NonSerialized]。支持复杂集合、没有 setter 的属性、只读字段等。

希望它能帮助到其他遇到与我相同问题的人。


使用 AnyClone,我只需安装 NuGet 包,调用 .Clone() 方法,就可以在 Blazor 项目中很好地工作了! - Tony

5

最短的方式,但需要依赖:

using Newtonsoft.Json;
    public static T Clone<T>(T source) =>
        JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(source));

4

好的,在这篇文章中有一些关于反射的明显例子,但是反射通常很慢,除非你开始正确地缓存它。

如果你正确地缓存它,那么它将在4.6秒内通过深度克隆1000000个对象(由观察者测量)。

static readonly Dictionary<Type, PropertyInfo[]> ProperyList = new Dictionary<Type, PropertyInfo[]>();

你可以使用缓存的属性或将新属性添加到字典中,然后简单地使用它们。

foreach (var prop in propList)
{
        var value = prop.GetValue(source, null);   
        prop.SetValue(copyInstance, value, null);
}

请查看我在另一个答案中的完整代码检查

https://dev59.com/sHVC5IYBdhLWcg3wpi98#34365709


2
调用 prop.GetValue(...) 仍然是反射,无法缓存。但在表达式树中编译,因此更快。 - Tseng

4

我已经创建了一个可以与 '[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 https://dev59.com/H3VD5IYBdhLWcg3wHn6d
    /// 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;
    }

} 

3
我将使用以下简单的方法来实现。 只需创建一个抽象类并实现序列化和反序列化方法即可,并返回结果。
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();
}

3
为了克隆你的类对象,你可以使用Object.MemberwiseClone方法,只需将此函数添加到你的类中:
public class yourClass
{
    // ...
    // ...

    public yourClass DeepCopy()
    {
        yourClass othercopy = (yourClass)this.MemberwiseClone();
        return othercopy;
    }
}

如果要进行深度独立复制,只需调用 DeepCopy 方法:

yourClass newLine = oldLine.DeepCopy();

希望这能帮到您。

5
MemberwiseClone 方法创建的是浅表副本,而非深层副本。 http://msdn.microsoft.com/zh-cn/library/system.object.memberwiseclone.aspx - odyth
@odyth 重要注释,将其作为实际代码进行操作。进行浅复制,这里有一篇关于克隆的好文章,其中包含每种类型的示例 https://www.geeksforgeeks.org/shallow-copy-and-deep-copy-in-c-sharp/ - ahmed hamdy
到目前为止,这对我的情况有效。谢谢。 - Christopher

3

欢迎提供解决方案的链接,但请确保您的答案即使没有链接也是有用的:在链接周围添加上下文,以便其他用户了解它的内容和原因,然后引用您链接的页面中最相关的部分,以防目标页面不可用。仅仅提供链接的答案可能会被删除。 - M.A.R.

3

我认为你可以尝试这个。

MyObject myObj = GetMyObj(); // Create and fill a new object
MyObject newObj = new MyObject(myObj); //DeepClone it

两个问题 - 首先,在 C# 中没有自动构造函数可以接受相同类型的对象,因此 new MyObject(myObj); 可能会出错。其次,如果您创建了这样的构造函数,它将是一个浅克隆,或者您必须在对象的属性中使用类似的构造函数来克隆包含的对象和集合。 - Andrew

2

使用ICloneable接口,尤其是在具有重型类层次结构时,你将会花费大量的精力。此外,MemberwiseClone的工作方式有些奇怪——它甚至不能准确地克隆普通List类型的结构。

当然,最有趣的序列化难题是序列化反向引用,例如存在子父关系的类层次结构。我怀疑二进制序列化程序能否在这种情况下帮助你(它将产生递归循环和堆栈溢出)。

我喜欢在这里提出的解决方案:How do you do a deep copy of an object in .NET (C# specifically)?

但是,它不支持Lists,所以我添加了对其的支持,并考虑了重新父级设置的情况。对于父级设置规则,我只是规定字段或属性应该命名为“parent”,然后DeepClone将忽略它。你可能想为反向引用制定自己的规则——对于树形层次结构,它可以是“left/right”等等。

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

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://dev59.com/DnVC5IYBdhLWcg3w-WWs
        /// 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
    }

}

2
如果您的对象树是可序列化的,您也可以使用类似以下方式的方法:
static public MyClass Clone(MyClass myClass)
{
    MyClass clone;
    XmlSerializer ser = new XmlSerializer(typeof(MyClass), _xmlAttributeOverrides);
    using (var ms = new MemoryStream())
    {
        ser.Serialize(ms, myClass);
        ms.Position = 0;
        clone = (MyClass)ser.Deserialize(ms);
    }
    return clone;
}

请注意,此解决方案相当简单,但可能不如其他解决方案的性能好。
如果类增长,确保只克隆那些也被序列化的字段。

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