ZetCode

C# 浅拷贝与深拷贝

最后修改于 2023 年 7 月 5 日

在本文中,我们比较 C# 中对象的浅拷贝和深拷贝。

数据复制是编程中的一项重要任务。 对象是 OOP 中的一种复合数据类型。 对象中的成员字段可以按值或按引用存储。 复制可以通过两种方式进行。

浅拷贝将所有值和引用复制到新实例中。 指针指向的数据不会被复制; 仅复制指针。 新的引用指向原始对象。 对引用成员的任何更改都会影响这两个对象。

深拷贝将所有值复制到新实例中。 对于存储为引用的成员,深拷贝会对正在引用的数据执行深拷贝。 创建被引用对象的新副本。 并且存储指向新创建对象的指针。 对这些引用对象的任何更改都不会影响对象的其他副本。 深拷贝是完全复制的对象。

如果成员字段是值类型,则执行该字段的逐位复制。 如果该字段是引用类型,则复制引用,但不复制被引用的对象; 因此,原始对象中的引用和克隆中的引用指向同一个对象。

C# 浅拷贝示例

以下程序执行浅拷贝。

Program.cs
namespace ShallowCopy;

class Color
{
    public int red;
    public int green;
    public int blue;

    public Color(int red, int green, int blue)
    {
        this.red = red;
        this.green = green;
        this.blue = blue;
    }
}

class MyObject : ICloneable
{
    public int id;
    public string size;
    public Color col;

    public MyObject(int id, string size, Color col)
    {
        this.id = id;
        this.size = size;
        this.col = col;
    }

    public object Clone()
    {
        return new MyObject(this.id, this.size, this.col);
    }

    public override string ToString()
    {
        var s = String.Format("id: {0}, size: {1}, color:({2}, {3}, {4})",
            this.id, this.size, this.col.red, this.col.green, this.col.blue);
        return s;
    }
}

class Program
{
    static void Main(string[] args)
    {
        var col = new Color(23, 42, 223);
        var obj1 = new MyObject(23, "small", col);

        var obj2 = (MyObject)obj1.Clone();

        obj2.id += 1;
        obj2.size = "big";
        obj2.col.red = 255;

        Console.WriteLine(obj1);
        Console.WriteLine(obj2);
    }
}

这是一个浅拷贝的示例。 我们定义两个自定义对象:MyObjectColorMyObject 对象将引用 Color 对象。

class MyObject : ICloneable

对于我们要克隆的对象,我们应该实现 ICloneable 接口。

public object Clone()
{
    return new MyObject(this.id, this.size, this.col);
}

ICloneable 接口强制我们创建一个 Clone 方法。 此方法返回一个带有复制值的新对象。

var col = new Color(23, 42, 223);

我们创建一个 Color 对象的实例。

var obj1 = new MyObject(23, "small", col);

创建 MyObject 类的实例。 Color 对象的实例被传递给构造函数。

var obj2 = (MyObject) obj1.Clone();

我们创建 obj1 对象的浅拷贝并将其分配给 obj2 变量。 Clone 方法返回一个 Object,我们期望的是 MyObject。 这就是我们进行显式转换的原因。

obj2.id += 1;
obj2.size = "big";
obj2.col.red = 255;

在这里,我们修改复制对象的成员字段。 我们增加 id,将大小更改为“big”,并更改颜色对象的红色部分。

Console.WriteLine(obj1);
Console.WriteLine(obj2);

Console.WriteLine 方法调用 obj2 对象的 ToString 方法,该方法返回对象的字符串表示形式。

$ dotnet run
id: 23, size: small, color:(255, 42, 223)
id: 24, size: big, color:(255, 42, 223)

我们可以看到 id 不同(23 与 24)。 大小不同(“small”与“big”)。 但是颜色对象的红色部分对于两个实例是相同的(255)。 更改克隆对象的成员值(id、大小)不会影响原始对象。 更改引用对象 (col) 的成员也会影响原始对象。 换句话说,两个对象都引用内存中的同一个颜色对象。

C# 深拷贝示例

为了改变这种行为,我们接下来进行深拷贝。

Program.cs
namespace DeepCopy;

class Color : ICloneable
{
    public int red;
    public int green;
    public int blue;

    public Color(int red, int green, int blue)
    {
        this.red = red;
        this.green = green;
        this.blue = blue;
    }

    public object Clone()
    {
        return new Color(this.red, this.green, this.blue);
    }
}

class MyObject : ICloneable
{
    public int id;
    public string size;
    public Color col;

    public MyObject(int id, string size, Color col)
    {
        this.id = id;
        this.size = size;
        this.col = col;
    }

    public object Clone()
    {
        return new MyObject(this.id, this.size,
            (Color)this.col.Clone());
    }

    public override string ToString()
    {
        var s = String.Format("id: {0}, size: {1}, color:({2}, {3}, {4})",
            this.id, this.size, this.col.red, this.col.green, this.col.blue);
        return s;
    }
}

class Program
{
    static void Main(string[] args)
    {
        var col = new Color(23, 42, 223);
        var obj1 = new MyObject(23, "small", col);

        var obj2 = (MyObject)obj1.Clone();

        obj2.id += 1;
        obj2.size = "big";
        obj2.col.red = 255;

        Console.WriteLine(obj1);
        Console.WriteLine(obj2);
    }
}

在这个程序中,我们对一个对象执行深拷贝。

class Color : ICloneable

现在 Color 类实现了 ICloneable 接口。

public object Clone()
{
    return new Color(this.red, this.green, this.blue);
}

我们也有 Color 类的 Clone 方法。 这有助于创建引用对象的副本。

public object Clone()
{
    return new MyObject(this.id, this.size,
        (Color) this.col.Clone());
}

当我们克隆 MyObject 时,我们会在 col 引用类型上调用 Clone 方法。 这样我们也有了一个颜色值的副本。

$ dotnet run
id: 23, size: small, color:(23, 42, 223)
id: 24, size: big, color:(255, 42, 223)

现在引用 Color 对象的红色部分不一样了。 原始对象保留了其先前的值 (23)。

来源

Object.MemberwiseClone 方法

在本文中,我们比较了 C# 中对象的浅拷贝和深拷贝。

作者

我叫 Jan Bodnar,是一位充满热情的程序员,拥有丰富的编程经验。 我从 2007 年开始撰写编程文章。到目前为止,我已经撰写了超过 1,400 篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有 C# 教程