在C#中创建深拷贝

12

我想制作一个对象的深拷贝,这样我就可以更改新副本并仍然有选择取消更改并获取原始对象的选项。

我的问题在于该对象可以是任何类型,甚至来自未知程序集。因为对象不一定有 [Serializable] 属性,所以我不能使用 BinaryFormatterXmlSerializer

我尝试使用 Object.MemberwiseClone() 方法来实现此操作:

public object DeepCopy(object obj)
{
    var memberwiseClone = typeof(object).GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic);

    var newCopy = memberwiseClone.Invoke(obj, new object[0]);

    foreach (var field in newCopy.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
    {
        if (!field.FieldType.IsPrimitive && field.FieldType != typeof(string))
        {
            var fieldCopy = DeepCopy(field.GetValue(newCopy));
            field.SetValue(newCopy, fieldCopy);
        }
    }
    return newCopy;
}

问题在于它不能用于枚举(数组、列表等),也不能用于字典。

那么,我该如何在C#中对未知对象进行深拷贝呢?

非常感谢!


1
在任意对象上调用MemberwiseClone是_邪恶的_。 - SLaks
我已经在数组和字典上尝试过这个方法,它们似乎都可以工作。我在开头添加了一个空值检查。这似乎是一个基本对象的通用解决方案。正如Slaks所提到的,对于文件流等情况,这不是一个好主意,因为它会复制相同的文件流句柄。它们将都依赖于流的关闭或寻找。如果您知道它并不总是深度克隆,那么这是一个有趣的解决方案。 - user295190
我还阅读了一种解决方案,您可以检查ICloneable接口。如果实现了ICloneable,则调用它,否则只需创建一个空实例。 - user295190
我认为大家还没有完全理解我的意图。我想要创建一个深拷贝,也就是复制对象包含的所有属性等层次结构中的所有对象。因此,我展示的方法无法在类数组上工作,只需尝试更改其中一个类,您就会发现值也已更改为“复制”的数组中。我希望数组(或任何其他对象)是完全独立的,因此更改源对象中的值不会更改我创建的副本。 - Orad SA
我们明白您想要做什么。然而,对于任意对象来说,进行深度复制是完全不可能的 - SLaks
这个博客非常方便,涵盖了在C#中克隆对象的所有可能方式。我发现它真的很有用。来看看吧! - RBT
9个回答

14

完全复制任意对象是不可能的。

例如,你该怎么处理一个 Control、一个 FileStream 或者一个 HttpWebResponse 呢?

你的代码无法知道该对象的结构和字段内容。

不要这样做。
这是个灾难的配方。


我不明白你的意思。为什么我不能创建控件的深拷贝?我的目的是创建与现有实例完全相同的新实例,但在内存中的新“位置”,以便如果更改一个实例,第二个实例不会受到影响。如果可能的话,我想做的就是获取对象在内存中“所在”的字节,并将其复制到内存中的新位置,然后创建指针,使我可以像使用.NET中的任何其他对象一样使用它。有办法可以这样做吗? - Orad SA
关于FileStream或HttpWebResponse,我可以肯定该对象不是它们之一。该对象必须是一个简单的对象('数据对象'或类似的东西)。 - Orad SA
1
@Orad:控件涉及本地互操作(它们包含窗口句柄)。如果您尝试深度复制一个控件,您将得到两个包装相同窗口句柄的控件。 - SLaks

4
制作任意对象的深度副本是相当困难的。如果对象包含访问资源(例如具有写入功能的打开文件或网络连接),那么怎么办?不知道对象保存了什么类型的信息,我很难制作一个副本,并使其完全按照原来的方式运行。您可能可以使用反射来完成此操作,但这会变得非常困难。首先,您必须有某种列表来保留所有复制的对象,否则,如果A引用B并且B引用A,则可能会陷入无限循环中。

4

同意SLaks的观点。您始终需要自定义复制语义,只是为了区分您是想进行深层复制还是平面复制。(什么是引用,什么是组合引用中的包含引用。)

您谈论的模式是备忘录模式

阅读如何实现它的文章。但基本上它会创建一个自定义复制工具。可以在类内部或外部的“复制工厂”中实现。


我不知道如何使用任何模式。我不知道我要复制的对象是什么。你能给一些例子吗? - Orad SA
备忘录模式在这里有很好的描述:http://www.business-patterns.com/DesignPatterns/Behavioural/Memento.pdf。 - schoetbi
我仍然不明白如何在我的情况下使用它。在备忘录模式中,我将需要为需要保存其状态的任何类创建特定的备忘录;但在这种情况下,我需要保存任何对象的状态(甚至是未引用到我的程序集的程序集中的类)。此外,我只需要保存一个状态,备忘录模式(据我理解)主要用于保存对象传递的多个状态。 - Orad SA
只是为了明确一下,记忆化模式不是问题的答案。正如我所说:你需要为你的对象编写一个自定义的复制工具。 - schoetbi

3
回答你的问题,你可以通过调用Array.CreateInstance来进行数组的深拷贝,然后通过调用GetValueSetValue来复制该数组的内容。
但是,对于任意对象而言,你不应该这样做。
例如:
  • 事件处理程序怎么办?
  • 有些对象具有唯一的ID;你最终会创建重复的ID。
  • 对于引用其父级的对象该怎么办?

2

好的,我稍微修改了你的例程。你需要整理一下,但是它应该可以实现你想要的功能。我没有测试它对控件或文件流的影响,这些情况需要注意。

我避免了使用Memberwise clone来创建Activator.CreateInstance。这将创建新的引用类型实例并复制值类型。如果你使用带有多维数组的对象,则需要使用数组秩并为每个秩进行迭代。

    static object DeepCopyMine(object obj)
    {
        if (obj == null) return null;

        object newCopy;
        if (obj.GetType().IsArray)
        {
            var t = obj.GetType();
            var e = t.GetElementType();
            var r = t.GetArrayRank();
            Array a = (Array)obj;
            newCopy = Array.CreateInstance(e, a.Length);
            Array n = (Array)newCopy;
            for (int i=0; i<a.Length; i++)
            {
                n.SetValue(DeepCopyMine(a.GetValue(i)), i);
            }
            return newCopy;
        }
        else
        {
            newCopy = Activator.CreateInstance(obj.GetType(), true);
        }

        foreach (var field in newCopy.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
        {
            if (!field.FieldType.IsPrimitive && field.FieldType != typeof(string))
            {
                var fieldCopy = DeepCopyMine(field.GetValue(obj));
                field.SetValue(newCopy, fieldCopy);
            }
            else
            {
                field.SetValue(newCopy, field.GetValue(obj));
            }
        }
        return newCopy;
    }

谢谢你,犯错误的开发者 - 我差点就准确地做到了,然后在最后一步里我卡住了... 嗯!... 但还是谢谢你! - Code Jockey

2
如其他人所说,深度复制任意对象可能会导致灾难性后果。但是,如果您对要克隆的对象相当确定,仍然可以尝试此操作。
关于您最初的方法有两件事情:
  • 在存在循环引用的情况下,您将陷入无限循环;
  • 您需要担心的唯一特殊情况是复制数组成员。其他所有内容(列表、哈希集、字典等)最终都将归结为这一点(或在树和链表的情况下,将按标准方式进行深层复制)。
还请注意,曾经有一种方法,可以创建任意类对象,而不需要调用其任何构造函数。二进制格式化程序使用了它,并且它是公开可用的。不幸的是,我不记得它叫什么,也不知道它在哪里。大概是关于运行时或封送之类的东西。
更新:System.Runtime.Serialization.FormatterServices.GetUninitializedObject。请注意,

您不能使用GetUninitializedObject方法创建从ContextBoundObject类派生的类型的实例。


2
http://msdn.microsoft.com/en-us/library/system.runtime.serialization.formatterservices.getuninitializedobject.aspx - SLaks
非常感谢,您向我展示了一些以前没有见过的东西。我会尝试升级我的方法,以便处理您所写的要点。 - Orad SA

1
另一个不复制任意对象的原因:无法知道一个对象与系统中其他对象的关系,或者某些值是否是魔术值,而不检查对象后面的代码。例如,两个对象都可能持有引用到包含单个整数且等于五的数组。这两个数组在程序的其他地方都被使用。重要的可能不是这两个数组恰好都包含值为五的元素,而是写入数组可能对其他程序执行产生的影响。如果序列化器的作者不知道这些数组的用途,那么就没有办法保留它们之间的关系。

1

对于深拷贝,我使用了Newtonsoft并创建了一个通用方法,例如:

public T DeepCopy<T>(T objectToCopy){
   var objectSerialized = JsonConvert.SerializeObject(objectToCopy);
   return JsonConvert.DeserializeObject<T>(objectSerialized);
}

我可以用的最佳解决方案。


0

这里是对@schoetbi的回答的详细阐述。你需要告诉类如何克隆自己。C#不区分聚合和组合,并且对两者都使用对象引用。

如果你有一个存储汽车信息的类,它可以有实例字段,比如发动机、轮子等(组合),但也可以有制造商(聚合)。两者都以引用的形式存储。

如果你克隆了汽车,你会期望发动机和轮子也被克隆,但你肯定不希望制造商也被克隆。


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