C# 中的浅拷贝和深拷贝(附示例)

2025年2月5日 | 阅读10分钟

理解 C# 中的浅拷贝

使用浅拷贝时,当从一个对象创建新对象后,当前对象的值类型字段会被复制到新对象中。但是,对于引用类型,它只会复制引用本身,而不是被引用的对象。对于引用类型,原始对象和克隆对象都指向同一个对象。请查看附带的图表以更好地理解这一点。我们有 Employee 和 Address 类,如下图所示。

此外,您还会看到我们在 Employee 类中指定了一个引用类型属性 empAddress 和两个值类型属性 Name 和 Department。使用浅拷贝时,引用类型的引用地址被复制到新对象中,而值类型属性的值被复制。当在 C# 中使用引用类型属性时,由于浅拷贝,克隆对象和现有对象都将指向相同的内存地址。

Shallow Copy and Deep Copy in C# with examples

附带的图表说明了我们首先创建一个名为 emp1 的对象,然后用一些值对其进行初始化。接下来,我们使用 GetClone 函数构造第二个对象,即 emp2。内存表示显示,值类型字段(Name 和 Department)被复制并保存在不同的内存位置。另一方面,引用类型字段 empAddress 继续指向相同的旧内存地址。这表明 Address 对象现在由 emp1 和 emp2 引用。因此,如果我们对员工地址进行任何修改,它们将相互影响。

注意:C# 中的“浅拷贝”描述了一种复制技术,其中对象的引用被复制,但对象本身并未复制。在浅拷贝中,如果原始对象引用了其他对象(例如数组或类对象),则原始对象和副本将引用相同的对象。

何时在 C# 中使用浅拷贝?

当原始对象和复制对象之间共享引用是可取或允许的时,浅拷贝可能很有用。在 C# 中,您可能希望在以下情况下使用浅拷贝:

  • 性能问题:由于浅拷贝涉及复制引用而不是完整对象,因此它们通常比深拷贝更快。如果性能是一个问题并且共享引用不是问题,则浅拷贝是一种选择。
  • 内存效率:如果复制的对象很大或包含大型嵌套对象,则深拷贝会很快占用大量内存。通过交换引用,浅拷贝避免了这种情况。
  • 共享状态:如果您希望多个对象通过其某些属性共享状态,则可以使用浅拷贝来实现。对共享状态所做的更改将反映在持有引用的所有对象中。
  • 临时副本:如果您只需要对象的临时副本(例如,用于一些快速计算或操作),并且您确信不会不当地更改共享信息,那么浅拷贝就可以正常工作。
  • 原型和设计模式:当模式实现不需要完全独立的克隆时,可以使用浅拷贝,例如在原型模式的情况下。
  • 不可变内部对象:如果主对象的内部对象是不可变的,这意味着它们一旦形成就不能更改,则浅拷贝是安全的。这是因为浅拷贝在任何情况下都不能更改。
  • 数组克隆:当您在 C# 中使用 Clone() 函数时,它会创建数组的浅拷贝。如果数组的项是值类型或不可变引用类型,这可能就是所需的一切。

理解 C# 中的深拷贝

深拷贝是通过复制现有对象的字段来将其转换为新对象的过程。如果字段是成本类型的一部分,则可以执行该字段的逐位复制。如果字段是引用类型,则会创建被引用对象的新副本。请查看下图以更好地理解。

此示例与我们之前介绍的浅拷贝示例相同。但是,您可以看到 Employee 和 Address 类现在都使用了 GetClone 函数。

我们从 Employee 类的 GetClone 方法中调用 Address 类的 GetClone 方法。这是 C# 中深拷贝的实现。

Shallow Copy and Deep Copy in C# with examples

因为 Name 和 Department 属性是值类型,如上图所示,它会复制该属性并将其保存在其他地方。对于 empAddress(一个引用类型属性),深拷贝中存在引用类型字段的克隆,该克隆将保存在不同的位置。因此,要记住的一点是,在深拷贝中,字段类型是引用还是值都无关紧要。每次都会创建整个文件的副本并保存在单独的内存位置。

理解 C# 中深拷贝的示例

以下示例演示了 C# 中深拷贝的使用。以下示例代码是自解释的,请仔细阅读注释行以更好地理解。

输出

Shallow Copy and Deep Copy in C# with examples

注意:递归地生成一个新对象,该对象是旧对象及其所有内容(包括其包含的任何其他对象)的副本,称为 C# 中的“深拷贝”。通过这种方式,对一个对象所做的修改不会影响另一个对象,反之亦然。通常,您必须手动复制对象中的每个字段才能实现深拷贝,确保也深拷贝任何引用类型。

何时在 C# 中使用深拷贝?

使用深拷贝可以保证复制对象和原始对象之间完全独立,这意味着对一个对象所做的修改不会影响另一个对象。在以下情况下,C# 中的深拷贝是合适的,甚至是必需的:

  • 完全独立:如果您希望两个对象彼此独立运行而不共享任何状态,则应使用深拷贝。如果对一个项目所做的更改不应该影响另一个项目,这一点尤其重要。
  • 撤销/重做功能:在需要保存对象状态历史记录以进行撤销/重做操作的程序(例如文字处理器或图形编辑器)中,深拷贝对于独立保留状态是必要的。
  • 设计模式中的对象克隆:某些设计模式(例如原型模式)需要深拷贝,如果您希望每个克隆都是一个独立的实体,没有共享引用。
  • 防止副作用:深拷贝可以帮助避免由于多个线程读取和更改相同引用而导致的意外副作用,尤其是在多线程上下文中将对象发送到方法时。
  • 序列化:序列化过程通常默认生成项目的深拷贝。通过将原始项目序列化为某种格式(例如 JSON 或 XML),然后反序列化它,可以获得原始项目的深拷贝。
  • 数据隔离:在测试或模拟等情况下,您必须重复对未更改的数据集执行相同的过程,深拷贝可以保证数据在运行过程中保持一致。
  • 安全和数据完整性:如果您正在处理敏感数据,深拷贝可以帮助防止意外的数据泄漏或修改。通过使用深拷贝,您可以确保原始数据没有任何更改。
  • 临时对象修改:当您需要对现有对象进行更改而不影响原始对象,并且可能在使用后将其丢弃时,深拷贝是合适的。
  • 网络通信:您可以使用对象的深拷贝来保证它在通过网络通信时保持完整和一致。
  • 防止循环引用:当深拷贝正确完成时,它可以处理和保留具有循环引用(A 引用 B,B 又引用 A)的对象,而不会创建无限循环。

C# 中浅拷贝和深拷贝的区别

使用浅拷贝和深拷贝从现有对象创建克隆对象。浅拷贝与其他方法的区别在于,当从原始对象创建对象克隆时,原始对象中的值类型字段会复制到新对象中。但是,对于引用类型属性,它只会复制引用,而不是实际对象。因此,对于引用类型,原始对象和复制对象指向相同的内存地址。

相反,使用深拷贝时,将使用原始对象生成克隆,并将原始对象的字段复制到新生成的克隆中。如果字段属于值类型,则将执行字段的逐位复制。如果字段是引用类型,则将创建被引用对象的新副本。在深拷贝的情况下,与浅拷贝不同,引用类型属性在原始对象和复制对象中都存储在不同的内存位置。

因此,即使您可以在类中声明两种方法,您也必须根据应用程序要求选择使用浅拷贝或深拷贝。有两种方法可用:浅拷贝和深拷贝。请查看以下示例以更好地理解这一点。在这里,我们在 Employee 类中有两种方法。克隆对象将使用 GetShallowCopy 方法进行浅拷贝,使用 GetDeepCopy 方法进行深拷贝。

通常,如果我们尝试将一个对象复制到另一个对象,它们将共享相同的内存位置。除非存在值类型字段,否则我们通常使用赋值运算符 = 复制引用而不是对象。此操作将始终复制引用而不是实际对象。例如,如果 P1 引用内存地址 5000,则 P2 也将引用相同的地址。因此,如果地址 5000 处存储的数据值发生更改,P1 和 P2 都将显示相同的数据。

在 C# 中,浅拷贝和深拷贝都指对象复制;它们之间唯一的区别是复制的深度。下面是它们区别的详细分析:

定义

  • 浅拷贝:浅拷贝是创建新对象并添加对旧对象的引用的过程。假设原始对象中存在引用字段。在这种情况下,只复制引用(而不是实际对象),因此原始对象及其副本中的引用指向相同的对象。
  • 深拷贝以递归方式构造一个新对象并复制源对象的引用和值字段。因此,对一个对象所做的修改不会影响另一个对象,因为原始对象及其副本是完全独立的。

引用类型

  • 浅拷贝:原始对象和复制对象共享原始对象中的相同引用类型。
  • 深拷贝:原始对象被深拷贝以包含各种形式的引用。复制对象中的每个引用类型都被分配给一个唯一的实例。

修改的影响

  • 浅拷贝:由于它们共享引用,因此更改复制对象内的引用类型也将更改原始对象内的相同数据。
  • 深拷贝:由于复制对象和原始对象完全独立,因此对一个对象所做的更改不会影响另一个对象。

执行

  • 浅拷贝:在 C# 中,对于类,通常使用 MemberwiseClone() 等技术来完成此操作。
  • 深拷贝:使用自定义代码递归复制内部对象,或使用序列化-反序列化技术作为解决方法。

成就

  • 浅拷贝:因为它只需要复制引用,所以它通常更快。
  • 深拷贝:由于对象的每个组件都必须重新创建,因此此方法可能较慢,特别是对于大型或深度分层的对象。

用例

  • 浅拷贝:当不需要深拷贝,并且您希望创建一个具有共享内部对象的副本时,这很有用。
  • 深拷贝:当您希望两个项目完全独立时,例如在撤销-重做功能中,或者在特定设计模式中克隆对象时,深拷贝至关重要。

限制

  • 浅拷贝:如果不够小心,对复制对象的引用类型所做的更改将反映在原始对象中,这可能会产生不可预见的后果。
  • 深拷贝:更难以使用,特别是对于结构复杂的对象或对象具有循环引用时。

结论

总之,在 C# 中很好地处理对象复制需要理解浅拷贝和深拷贝背后的思想,尤其是在处理复杂的对象结构时。您对浅拷贝与深拷贝的决定将取决于您的独特需求。在内存考虑至关重要且允许共享引用的情况下,浅拷贝可能适用。

当您需要对象的独立副本及其层次结构时,深拷贝是更好的选择。在 C# 中处理复杂数据结构时,正确实施这些复制过程可以保护对象实例的完整性并防止意外的副作用。