12

我想制作一个对象的深层副本,这样我就可以更改新副本,并且仍然可以选择取消我的更改并取回原始对象。

我的问题是该对象可以是任何类型,即使来自未知程序集。我不能使用BinaryFormatteror XmlSerializer,因为该对象不必要地具有 [Serializable] 属性。

我尝试使用以下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# 中制作未知对象的深层副本?

TNX很多!

4

9 回答 9

14

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

例如,您将如何处理 aControl或 aFileStream或 a HttpWebResponse

您的代码无法知道该对象是如何工作的以及它的字段应该包含什么。

不要这样做
这是灾难的秘诀。

于 2010-09-05T17:46:25.923 回答
4

制作任意对象的深层副本非常困难。如果对象包含对资源的访问权限,例如具有写入功能的打开文件或网络连接,该怎么办?如果不知道对象包含什么类型的信息,我将很难复制一个对象,并让它以完全相同的方式运行。您也许可以使用反射来做到这一点,但这会变得非常困难。对于初学者,您必须有某种列表来保存您复制的所有对象,否则,如果 A 引用 B 和 B 引用 A,那么您可能会陷入无限循环。

于 2010-09-05T17:50:36.300 回答
4

同意 SLaks。您总是需要自定义复制语义来区分您想要深拷贝或平面拷贝的天气。(什么是引用,什么是复合引用中的包含引用。)

您所说的模式是纪念品模式

阅读有关如何实现它的文章。但基本上事实证明,它创建了一个自定义复制设施。在类的内部或“复制工厂”的外部。

于 2010-09-05T17:51:15.957 回答
3

要回答您的问题,您可以通过调用Array.CreateInstance和复制数组的内容GetValue来深度复制数组SetValue

但是,您不应该对任意对象执行此操作。

例如:

  • 事件处理程序呢?
  • 一些对象具有唯一的 ID;您最终将创建重复的 ID
  • 引用其父母的对象呢?
于 2010-09-06T12:05:31.723 回答
2

正如其他人所说,深度复制任意对象可能是灾难性的。但是,如果您非常确定要克隆的对象,您仍然可以尝试这样做。

关于您的原始方法的两件事:

  • 在循环引用的情况下,您将陷入无限循环;
  • 您需要担心的唯一特殊情况是复制数组成员。其他一切(列表、哈希集、字典等)最终都会归结为这一点(或者在树和链表的情况下,标准方式将是可深度复制的)。

另请注意,有一种方法允许在不调用任何构造函数的情况下创建任意类对象。BinaryFormatter 使用了它并且它是公开的。不幸的是,我不记得它叫什么以及它住在哪里。关于运行时或编组的一些事情。

更新: System.Runtime.Serialization.FormatterServices.GetUninitializedObject请注意

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

于 2010-09-05T17:58:39.030 回答
2

好的,我稍微修改了你的例程。你需要清理它,但它应该完成你想要的。我没有针对控件或文件流对此进行测试,应该小心处理这些实例。

我避免了 Activator.CreateInstance 的成员克隆。这将创建引用类型和复制值类型的新实例。如果您使用具有多维数组的对象,则需要使用 Array Rank 并针对每个 rank 进行迭代。

    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;
    }
于 2010-09-06T20:08:48.940 回答
1

不复制任意对象的另一个原因是:如果不检查对象背后的代码,就不可能知道该对象将如何与系统中的其他对象相关联,或者某些值是否具有魔力。例如,两个对象可能都持有对数组的引用,这些数组包含一个等于 5 的整数。这两个数组都在程序的其他地方使用。重要的可能不是任何一个数组恰好保存值 5,而是写入数组可能对其他程序执行产生的影响。一个序列化程序的作者不知道数组的用途是不可能保持这种关系的。

于 2010-09-05T19:55:29.473 回答
0

这是@schoetbi 对答案的详细说明。你需要告诉班级如何克隆自己。C# 不区分聚合和组合,并为两者使用对象引用。

如果你有一个用于存储汽车信息的类,它可能有实例字段,如引擎、车轮等(组合),还有制造商(聚合)。两者都存储为参考。

如果你克隆了汽车,你会期望引擎和车轮也被克隆,但你当然不希望制造商也被克隆。

于 2014-09-29T12:46:10.047 回答
0

对于深拷贝,我使用了 Newtonsoft 和 create 和通用方法,例如:

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

我可以用来解决这个问题的最佳解决方案。

于 2021-07-13T20:42:47.833 回答