对象深度克隆的实现方法

4

我需要实现一个通用的扩展深拷贝方法,可以用于任何引用类型实例来获得它的深度副本。我将其实现如下:

static class ClassCopy
{
    static public T DeepClone<T> (this T instance)
    {
        if (instance == null) return null;
        var type = instance.GetType();
        T copy;
        var flags = BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.NonPublic |
                    BindingFlags.Instance;

        var fields = type.GetFields(flags);

        // If type is serializable - create instance copy using BinaryFormatter
        if (type.IsSerializable)
        {
            using (var stream = new MemoryStream())
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(stream, instance);
                stream.Position = 0;
                copy = (T) formatter.Deserialize(stream);
            }

            // Copy all fiels  which are not marked as serializable 
            foreach (var field in fields)
            {
                if (!field.IsNotSerialized) continue;
                var value = field.GetValue(instance);

                //Recursion!!!
                //for each embedded object also create deep copy
                value = value != null  ? value.DeepClone() : value;
                field.SetValue(copy, value);
            }
        }
        else
        {
            // If type is not serializable - create instance copy using Activator
            //(if there is default constructor)
            // or FormatterServices ( if there is no constractor)

            copy = CreateInstance<T>(type);
            foreach (var field in fields)
            {
                var value = field.GetValue(instance);

                //Recursion!!!
                value = value != null  ? value.DeepClone() : value;
                field.SetValue(copy, value);
            }
        }

        //Copy all properties 
        //In order to copy all backing fields  for auto-implemented properties

        var properties = type.GetProperties(flags|BindingFlags.SetProperty);
        foreach (var property in properties)
        {
            if (property.CanWrite)
            {
                var value = property.GetValue(instance);

                //Recursion!!!
                value = value != null ? value.DeepClone() : null;
                property.SetValue(copy, value);
            }
        }
        return copy;
    }

    private static T CreateInstance<T>(Type t) where T: class
    {
        T instance;
        var constructor = t.GetConstructor(Type.EmptyTypes);
        if (constructor != null)
        {
            instance = Activator.CreateInstance(t) as T;
            return instance;
        }
        instance = FormatterServices.GetUninitializedObject(t) as T;
        return instance;
    }
}

它可以正常工作。但是,如果要克隆的对象及其引用类型字段存在相互引用关系,则该代码会导致无限循环。

例如:

private static void Main(string[] args)
{
    var parent = new Parent();
    parent.Child = new Child();
    parent.Child.Parent = parent;
    //Infinite Loop!!!
    var parent1 = parent.DeepClone();
}

class Parent
{
    public Child Child { get; set; }
}
class Child
{
    public Parent Parent { get; set; }
}

有人知道如何实现这个任务吗?它必须按照字面意思实现,不允许任何变化(这是一个实习)。非常感谢任何提示!


1
你不会看到通用的深度克隆方法,这是有原因的。要正确地实现它们,实际上需要根据所涉及的具体类型进行实现。考虑到你极少需要真正使用这样的实现,这通常不是问题。事实上,你需要像这样的东西,可能表明出现了某些问题。 - Servy
你可以尝试这段代码:http://code.msdn.microsoft.com/CSEFDeepCloneObject-12a5cb95 - Milan Raval
我理解。但这是一项实践,所以我必须想办法完成它。我不知道,也许为了解决问题,我应该为已经克隆的对象添加一些存储空间,以免再次克隆它们。但是那是否会为所有对象链提供深拷贝?我在这里真的很绝望。 - user3101007
@marcinjuraszek 如果实例像示例中那样被可变引用,那个dll的方法也会导致无限循环。 - user3101007
1
你必须保留每个克隆对象的字典(以引用或哈希键入),并附带其克隆体的引用。在克隆新对象之前,请检查它是否已经被克隆过。 - Tom Blodget
显示剩余3条评论
2个回答

7
一个克隆对象的老办法是将其序列化和反序列化,从而创建新实例。
public T deepClone<T>(T toClone) where T : class
{
    string tmp = JsonConvert.SerializeObject(toClone);
    return JsonConvert.DeserializeObject<T>(tmp);            
}

我在IT技术方面有丰富的经验,熟悉Newtonsoft.Json,它内置了解决循环引用问题的方案。默认情况下,它会检测对象是否已经被序列化并抛出异常。但是,您可以配置它以便为了避免循环引用而序列化对象引用。它不是将对象内联序列化,而是将对该对象的引用序列化,并保证每个对象引用只被序列化一次。

此外,Newtonsoft.Json默认只序列化公共字段/属性。还有一个额外设置用于序列化私有字段。

public T deepClone<T>(T toClone) where T : class
{
    JsonSerializerSettings settings = new JsonSerializerSettings();
    settings.PreserveReferencesHandling = PreserveReferencesHandling.Objects;

    DefaultContractResolver dcr = new DefaultContractResolver();
    dcr.DefaultMembersSearchFlags |= System.Reflection.BindingFlags.NonPublic;
    settings.ContractResolver = dcr;

    string tmp = JsonConvert.SerializeObject(toClone, settings);
    return JsonConvert.DeserializeObject<T>(tmp);
}

所以你可以“欺骗”并使用以下代码,或者复制其工作原理来实现保留引用的克隆。你提到的父/子示例仅仅是克隆困难的一种方式。1对多是另一种情况。

序列化只能用于可序列化对象和可序列化成员,这是正确的吗?我在示例中使用了二进制序列化,但这并没有解决问题。 - user3101007
好的观点。通常我使用DTO,它们都是公共的。我已更新示例以包括私有字段。 - raider33

1
你可以传递一个映射到其克隆项的Dictionary。现在该方法将如下所示: static private T DeepClone<T> (this T instance, IDictionary<object, object> originalToAlreadyCloned) where T: class 现在,在if(instance == null) return null;之后,第一件事是检查instance是否存在于originalToAlreadyCloned中,如果存在,则返回它。
为了填充它,在以下操作之后:
  1. copy = (T) formatter.Deserialize(stream);
  2. copy = CreateInstance<T>(type);
调用originalToAlreadyCloned.Add(instance, copy); 最后,提供一个新的顶级方法: static private T DeepClone<T> (this T instance) where T: class,它只需调用DeepClone(instance, new Dictionary<object, object>()); 顺便说一下,value = value != null && value.GetType().IsClass? value.DeepClone() : null; 似乎是错误的。你的意思是,如果 value 不是一个类,则将其设置为 null。但如果它不是一个类,通常不能将其设置为 null。我不确定为什么你不直接克隆这些项目,以及为什么 DeepClone 仅限于类。

谢谢,我会尝试这个。还有感谢你注意到了错误——那些都是凌晨三点编码的结果。 - user3101007

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