在C#中创建一个对象的副本

233
请看下面的代码(摘自一本C#书籍):
public class MyClass 
{
    public int val;
}
public struct myStruct 
{
    public int val;
}
public class Program 
{
    private static void Main(string[] args) 
    {
        MyClass objectA = new MyClass();
        MyClass objectB = objectA;

        objectA.val = 10;
        objectB.val = 20;

        myStruct structA = new myStruct();
        myStruct structB = structA;

        structA.val = 30;
        structB.val = 40;

        Console.WriteLine("objectA.val = {0}", objectA.val);
        Console.WriteLine("objectB.val = {0}", objectB.val);
        Console.WriteLine("structA.val = {0}", structA.val);
        Console.WriteLine("structB.val = {0}", structB.val);

        Console.ReadKey();
    }
}

我理解它会产生以下输出:

objectA.val = 20
objectB.val = 20
structA.val = 30
structB.val = 40

我对输出的最后两行没有问题,但前两行告诉我objectAobjectB指向同一内存块(由于在C#中,对象是引用类型)。

问题是如何使objectB成为objectA的副本,以便它指向内存中不同的区域。我明白尝试分配它们的成员可能不起作用,因为这些成员可能也是引用。那么我该怎么做才能使objectB成为与objectA完全不同的实体呢?


1
这可能会有所帮助:https://dev59.com/DnVC5IYBdhLWcg3w-WWs - vines
5
将myobject序列化为JSON字符串: string json = Newtonsoft.Json.JsonConvert.SerializeObject(myobject);将JSON字符串反序列化为myObjType对象: myObjType rCopy = Newtonsoft.Json.JsonConvert.DeserializeObject<myObjType>(json); - hal9000
@hal9000 我已经使用了序列化/反序列化方法。相比下面的克隆解决方案,这种方法有什么缺点吗?看起来是一个直截了当的解决方案。 - amartin
我没有经历任何不利影响。但是我不能评论下面的克隆解决方案。我没有尝试过它们。 - hal9000
1
@SiavashGhanbari 那确实没有帮助。那个问题仍然被暂停了。这个特定的问题已经被标记为重复。那个问题是一个再再再次重复的问题。 - afaolek
4个回答

200

你可以这样做:

class myClass : ICloneable
{
    public String test;
    public object Clone()
    {
        return this.MemberwiseClone();
    }
}

然后你可以这样做

myClass a = new myClass();
myClass b = (myClass)a.Clone();

注意:MemberwiseClone() 方法创建当前 System.Object 的浅表副本。


42
如果没有提到浅克隆和深克隆及其影响,那么使用MemberwiseClone()会很棘手。 - anar khalilov
9
未提及 ICloneable 已经被弃用,不应再使用,因此给出 -1 分。 - Tom
158
@Tom:如果我能的话,我会给你的评论打-1,因为你没有提供任何关于你说话的链接,因为ICloneable在官方文档里没有被标记为过时。你更多地谈论的是最佳实践而不是官方弃用状态。 - JYL
7
直接从文档中摘录:“如果一个字段是值类型,则执行按位复制。如果一个字段是引用类型,则复制引用但不复制被引用的对象;因此,原始对象和它的克隆体引用同一对象。”https://msdn.microsoft.com/zh-cn/library/system.object.memberwiseclone(v=vs.110).aspx - Brent Rittenhouse
2
嗯...它并不是被弃用了,只是建议在公共API中不要使用它,因为消费者无法清楚地了解其实现方式,但对于内部使用或开源项目,我认为可以使用。他们所说的是“由于Clone的调用者不能依赖该方法执行可预测的克隆操作,因此我们建议不要在公共API中实现ICloneable。” - MetalGeorge
显示剩余2条评论

150

没有内置的方法。您可以让MyClass实现 IClonable 接口(但这有点过时),或者编写自己的 Copy/Clone 方法。无论哪种情况,都需要编写一些代码。

对于大型对象,您可以考虑使用序列化 + 反序列化(通过MemoryStream)来重用现有代码。

无论使用哪种方法,都应仔细考虑“复制”的确切含义。它应该有多深?是否有要排除的 Id 字段等。


7
很抱歉,我知道这是一个非常古老的帖子,但您能提供一个说明ICloneable已经被废弃的文档链接吗?我查看了.NET 4.5的文档,ICloneable没有任何关于被废弃的说明。如果它已经被废弃,那么我想使用其他东西。 - vane
15
没事了,我找到了。在.NET文档中,他们“建议不要在公共API中实现ICloneable”。因此,它没有被弃用,只是不建议使用。 - vane
20
“Deprecated” 的意思是 “表示不赞成”,在软件领域中,deprecated 代表着不建议使用。当一个 API 是公开的时候,你不能说它已经过时,而当它是私有的时候则可以。过时的 API 应该避免使用,除非确实必要。微软并没有因为某个 API 过时而完全禁止使用它(通常提供替代方案),只是建议在公共 API 中避免使用。 - vane
2
@LeeTaylor 维基百科表示,“弃用”是应用于计算机软件功能、特性或实践的属性,以表明它应该被避免使用(通常是因为它正在被取代);避免意味着完全避免,而不是基于可访问性避免。 - vane
52
作为最后一点,微软会给他们认为已经弃用的类、结构体、枚举、委托、方法、构造函数、属性、字段、事件和接口添加 Obsolete 属性,这会导致编译器发出一个 CS0618 警告,表示该内容已经被弃用。ICloneable 接口并没有应用 Obsolete 属性。 - vane
我讨厌 ICloneable => Clone() 强制你返回一个 object。这只会导致运行时异常。这种情况告诉我不要选择这条路。 - Kellen Stuart

60

最简单的方法是在MyClass类中编写一个复制构造函数。

就像这样:

namespace Example
{
    class MyClass
    {
        public int val;

        public MyClass()
        {
        }

        public MyClass(MyClass other)
        {
            val = other.val;
        }
    }
}

第二个构造函数只是接受一个同类型的参数(你想要复制的那个)并创建一个新对象,这个对象被赋予相同的值。

class Program
{
    static void Main(string[] args)
    {
        MyClass objectA = new MyClass();
        MyClass objectB = new MyClass(objectA);
        objectA.val = 10;
        objectB.val = 20;
        Console.WriteLine("objectA.val = {0}", objectA.val);
        Console.WriteLine("objectB.val = {0}", objectB.val);
        Console.ReadKey();
    }
}

输出:

objectA.val = 10

objectB.val = 20               

10
这对于学习很有好处,但不适合练习。 - Shinigamae
输出的第二行应该是 objectB.val = 20,因为编辑太小而无法修改。 - jingtao
20
复制构造函数的问题在于,如果你增加/删除了字段,你也必须修改复制构造函数。这可能会变成一个维护的噩梦,尤其是对于具有许多字段(50个以上,即DataContract)的对象。 - crush
2
我喜欢这个答案。克隆方法返回类对象,因此它是一种构造函数。那么,为什么称之为克隆而不使用类名呢?那么,为什么会有“维护噩梦”呢?如果你需要维护一个大型字段列表,这是最好的地方。关于“已弃用”:没有人强迫你从IClonable接口继承。你可以在不使用接口的情况下实现一个Clone()方法。但归根结底,我会在我的项目中实现拷贝构造函数。 - Roland
1
我同意@Roland的观点。使用new关键字,您可以确保创建一个具有不同地址的新对象。我没有看到任何问题。你可能更喜欢使用反射? - Béranger
我走了这条路。我喜欢这个解决方案,因为它非常明确,但我更愿意将细节放在一个方法中。在我的情况下,我有一个额外的参数来指示是否也应克隆“Children”属性。由于某种原因,这使我倾向于使用一个方法。var objB = objA.Clone(isDeep = false); - christo8989

13

这个问题已经有了一个相关的提问,您可以阅读一下:

如何深度复制对象

虽然C++中没有像Java那样的Clone()方法,但您可以在您的类中包含一个拷贝构造函数,这也是另一种不错的方法。

class A
{
  private int attr

  public int Attr
  {
     get { return attr; }
     set { attr = value }
  }

  public A()
  {
  }

  public A(A p)
  {
     this.attr = p.Attr;
  }  
}

这是一个示例,当构建新对象时复制成员“Attr”。


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