深拷贝与浅拷贝是很多语言都有的概念,在C#中也不例外
1. 深拷贝与浅拷贝
深拷贝与浅拷贝的区别就是在拷贝的时候是否会建立一个新的对象实体还是引用。而比较直观的就是浅拷贝时,修改拷贝对象的值会改变原对象的值,因为他们在内存里仍然是同一块区域,而浅拷贝修改拷贝对象的值并不会影响原对象的值。
在Python中有copy和deepcopy方法,在c++中可以通过重载构造函数和等号实现拷贝的实际行为,在Rust可以通过trait实现复制语义和移动语义,当然移动语义和复制语义有别于深拷贝与浅拷贝。那么Rust中如何实现深拷贝呢?那么首先我们需要认识C#中的值类型和引用类型。
2. C#类型系统
类型、变量和值
C# 是一种强类型语言。 每个变量和常量都有一个类型,每个求值的表达式也是如此。 每个方法声明都为每个输入参数和返回值指定名称、参数数量以及类型和种类(值、引用或输出)。 .NET 类库定义了一组内置数值类型以及表示各种逻辑构造的更复杂类型(如文件系统、网络连接、对象的集合和数组以及日期)。 典型的 C# 程序使用类库中的类型,以及对程序问题域的专属概念进行建模的用户定义类型。
类型中可存储的信息包括以下项:
- 类型变量所需的存储空间。
- 可以表示的最大值和最小值。
- 包含的成员(方法、字段、事件等)。
- 继承自的基类型。
- 它实现的接口。
- 在运行时分配变量内存的位置。
- 允许执行的运算种类。
编译器使用类型信息来确保在代码中执行的所有操作都是类型安全的。 例如,如果声明 int 类型的变量,那么编译器允许在加法和减法运算中使用此变量。 如果尝试对 bool 类型的变量执行这些相同操作,则编译器将生成错误,如以下示例所示:
int a = 5;
int b = a + 2; //OK
bool test = true;
// Error. Operator '+' cannot be applied to operands of type 'int' and 'bool'.
int c = a + test;
备注:
C 和 C++ 开发人员请注意,在 C# 中,bool 不能转换为 int。
编译器将类型信息作为元数据嵌入可执行文件中。 公共语言运行时 (CLR) 在运行时使用元数据,以在分配和回收内存时进一步保证类型安全性。
在变量声明中指定类型
当在程序中声明变量或常量时,必须指定其类型或使用 var 关键字让编译器推断类型。 以下示例显示了一些使用内置数值类型和复杂用户定义类型的变量声明:
// Declaration only:
float temperature;
string name;
MyClass myClass;
// Declaration with initializers (four examples):
char firstLetter = 'C';
var limit = 3;
int[] source = { 0, 1, 2, 3, 4, 5 };
var query = from item in source
where item <= limit
select item;
方法声明指定方法参数的类型和返回值。 以下签名显示了需要 int 作为输入参数并返回字符串的方法:
public string GetName(int ID)
{
if (ID < names.Length)
return names[ID];
else
return String.Empty;
}
private string[] names = { "Spencer", "Sally", "Doug" };
声明变量后,不能使用新类型重新声明该变量,并且不能分配与其声明的类型不兼容的值。 例如,不能声明 int 后再向它分配 true
的布尔值。 不过,可以将值转换成其他类型。例如,在将值分配给新变量或作为方法自变量传递时。 编译器会自动执行不会导致数据丢失的类型转换。 如果类型转换可能会导致数据丢失,必须在源代码中进行 显式转换。
内置类型
C# 提供了一组标准的内置类型来表示整数、浮点值、布尔表达式、文本字符、十进制值和其他数据类型。 还有内置的 string
和 object
类型。 这些类型可供在任何 C# 程序中使用。 有关内置类型的完整列表,请参阅内置类型。
自定义类型
可以使用结构、类、接口,和枚举构造创建你自己的自定义类型。 .NET 类库本身就是 Microsoft 提供的一组自定义类型,以供你在自己的应用程序中使用。 默认情况下,类库中最常用的类型在任何 C# 程序中均可用。 对于其他类型,只有在显式添加对定义这些类型的程序集的项目引用时才可用。 编译器引用程序集之后,你可以声明在源代码的此程序集中声明的类型的变量(和常量)。 有关详细信息,请参阅 .NET 类库。
通用类型系统
对于 .NET 中的类型系统,请务必了解以下两个基本要点:
- 它支持继承原则。 类型可以派生自其他类型(称为 基类型)。 派生类型继承(有一些限制)基类型的方法、属性和其他成员。 基类型可以继而从某种其他类型派生,在这种情况下,派生类型继承其继承层次结构中的两种基类型的成员。 所有类型(包括 System.Int32C# 关键字:int等内置数值类型)最终都派生自单个基类型,即 System.Object(C# 关键字:object。 这样的统一类型层次结构称为通用类型系统 (CTS)。 若要详细了解 C# 中的继承,请参阅继承。
- CTS 中的每种类型被定义为值类型或引用类型。 这些类型包括 .NET 类库中的所有自定义类型以及你自己的用户定义类型。 使用 struct 关键字定义的类型是值类型;所有内置数值类型都是
structs
。 使用 class 关键字定义的类型是引用类型。 引用类型和值类型遵循不同的编译时规则和运行时行为。
下图展示了 CTS 中值类型和引用类型之间的关系。
备注:
你可能会发现,最常用的类型全都被整理到了 System 命名空间中。 不过,包含类型的命名空间与类型是值类型还是引用类型没有关系。
值类型
值类型派生自System.ValueType(派生自 System.Object)。 派生自 System.ValueType 的类型在 CLR 中具有特殊行为。 值类型变量直接包含它们的值,这意味着在声明变量的任何上下文中内联分配内存。 对于值类型变量,没有单独的堆分配或垃圾回收开销。
内置的数值类型是结构,它们具有可访问的字段和方法:
// constant field on type byte.
byte b = byte.MaxValue;
但可将这些类型视为简单的非聚合类型,为其声明并赋值:
byte num = 0xA;
int i = 5;
char c = 'Z';
值类型已密封,这意味着不能从任何值类型(例如 System.Int32)派生类型。 不能将结构定义为从任何用户定义的类或结构继承,因为结构只能从 System.ValueType 继承。 但是,一个结构可以实现一个或多个接口。 可将结构类型强制转换为它实现的任何接口类型;强制转换会导致装箱操作发生,以将结构包装在托管堆上的引用类型对象内。 当你将值类型传递给使用 System.Object 或任何接口类型作为输入参数的方法时,就会发生装箱操作。 有关详细信息,请参阅装箱和取消装箱。
使用 struct 关键字可以创建你自己的自定义值类型。 结构通常用作一小组相关变量的容器,如以下示例所示:
public struct Coords
{
public int x, y;
public Coords(int p1, int p2)
{
x = p1;
y = p2;
}
}
有关结构的详细信息,请参阅结构类型。 有关值类型的详细信息,请参阅值类型。
另一种值类型是枚举。 枚举定义的是一组已命名的整型常量。 例如,.NET 类库中的 System.IO.FileMode 枚举包含一组已命名的常量整数,用于指定打开文件应采用的方式。 下面的示例展示了具体定义:
public enum FileMode
{
CreateNew = 1,
Create = 2,
Open = 3,
OpenOrCreate = 4,
Truncate = 5,
Append = 6,
}
System.IO.FileMode.Create
常量的值为 2。 不过,名称对于阅读源代码的人来说更有意义,因此,最好使用枚举,而不是常量数字文本。 有关详细信息,请参阅 System.IO.FileMode。
所有枚举从 System.Enum(继承自 System.ValueType)继承。 适用于结构的所有规则也适用于枚举。 有关枚举的详细信息,请参阅枚举类型。
引用类型
定义为 类、委托、数组或 接口的类型是 引用类型。 在运行时,当声明引用类型的变量时,该变量会一直包含值 null,直至使用 new 运算符显式创建对象,或者为该变量分配已经在其他位置使用 new
创建的对象,如下所示:
MyClass mc = new MyClass();
MyClass mc2 = mc;
接口必须与实现它的类对象一起初始化。 如果 MyClass
实现 IMyInterface
,则按以下示例所示创建 IMyInterface
的实例:
IMyInterface iface = new MyClass();
创建对象后,内存会在托管堆上进行分配,并且变量只保留对对象位置的引用。 对于托管堆上的类型,在分配内存和 CLR 自动内存管理功能(称为“垃圾回收”)回收内存时都会产生开销。 但是,垃圾回收已是高度优化,并且在大多数情况下,不会产生性能问题。 有关垃圾回收的详细信息,请参阅自动内存管理。
所有数组都是引用类型,即使元素是值类型,也不例外。 虽然数组隐式派生自 System.Array 类,但可以使用 C# 提供的简化语法声明和使用数组,如以下示例所示:
// Declare and initialize an array of integers.
int[] nums = { 1, 2, 3, 4, 5 };
// Access an instance property of System.Array.
int len = nums.Length;
引用类型完全支持继承。 创建类时,可以从其他任何未定义为密封的接口或类继承,而其他类可以从你的类继承并重写虚拟方法。 若要详细了解如何创建你自己的类,请参阅类和结构。 有关继承和虚方法的详细信息,请参阅继承。
文本值的类型
在 C# 中,文本值从编译器接收类型。 可以通过在数字末尾追加一个字母来指定数字文本应采用的类型。 例如,若要将值 4.56 指定为应按浮点值处理,请在数字后面追加“f”或“F”:4.56f
。 如果没有追加字母,那么编译器就会推断文本值的类型。 若要详细了解可以使用字母后缀指定哪些类型,请参阅整型数值类型和浮点数值类型。
由于文本已类型化,且所有类型最终都是从 System.Object 派生,因此可以编写和编译如下所示的代码:
string s = "The answer is " + 5.ToString();
// Outputs: "The answer is 5"
Console.WriteLine(s);
Type type = 12345.GetType();
// Outputs: "System.Int32"
Console.WriteLine(type);
泛型类型
可使用一个或多个类型参数声明、作为客户端代码在创建类型实例时将提供的实际类型(具体类型)的占位符的类型。 这种类型称为泛型类型。 例如,.NET 类型 System.Collections.Generic.List 具有一个类型参数,它按照惯例被命名为 T。当创建类型的实例时,指定列表将包含的对象的类型,例如字符串:
List<string> stringList = new List<string>();
stringList.Add("String example");
// compile time error adding a type other than a string:
stringList.Add(4);
通过使用类型参数,可重新使用相同类以保存任意类型的元素,且无需将每个元素转换为对象。 泛型集合类称为强类型集合,因为编译器知道集合元素的具体类型,并能在编译时引发错误,例如当尝试向上面示例中的 stringList
对象添加整数时。 有关详细信息,请参阅泛型。
隐式类型、匿名类型和可以为 null 的值类型
如前所述,你可以使用 var 关键字隐式键入一个局部变量(但不是类成员)。 变量仍可在编译时获取类型,但类型是由编译器提供。 有关详细信息,请参阅隐式类型局部变量。
不方便为不打算存储或传递外部方法边界的简单相关值集合创建命名类型。 因此,可以创建 匿名类型。 有关详细信息,请参阅匿名类型。
普通值类型不能具有 null 值。 不过,可以在类型后面追加 ?
,创建可以为 null 的值类型。 例如,int?
是还可以包含值 null 的 int
类型。 可以为 null 的值类型是泛型结构类型 System.Nullable 的实例。 在将数据传入和传出数据库(数值可能为 null)时,可以为 null 的值类型特别有用。 有关详细信息,请参阅可以为 null 的值类型。
编译时类型和运行时类型
变量可以具有不同的编译时和运行时类型。 编译时类型是源代码中变量的声明或推断类型。 运行时类型是该变量所引用的实例的类型。 这两种类型通常是相同的,如以下示例中所示:
string message = "This is a string of characters";
在其他情况下,编译时类型是不同的,如以下两个示例所示:
object anotherMessage = "This is another string of characters";
IEnumerable<char> someCharacters = "abcdefghijklmnopqrstuvwxyz";
在上述两个示例中,运行时类型为 string
。 编译时类型在第一行中为 object
,在第二行中为 IEnumerable<char>
。
如果变量的这两种类型不同,请务必了解编译时类型和运行时类型的应用情况。 编译时类型确定编译器执行的所有操作。 这些编译器操作包括方法调用解析、重载决策以及可用的隐式和显式强制转换。 运行时类型确定在运行时解析的所有操作。 这些运行时操作包括调度虚拟方法调用、计算 is
和 switch
表达式以及其他类型的测试 API。 为了更好地了解代码如何与类型进行交互,请识别哪个操作应用于哪种类型。
3. 实现C#的深拷贝
在了解C#类型系统后,我将介绍一种C#深拷贝的方法。
using System;
namespace TestApplication
{
class Box
{
public double length; // 长度
public double breadth; // 宽度
public double height; // 高度
}
class Program
{
static void Main(string[] args)
{
Box b1 = new Box();
b1.length = 1;
b1.breadth = 1;
b1.height = 1;
Box b2 = b1;
b2.breadth = 2;
Console.WriteLine(b1.breadth);
Console.WriteLine(b2.breadth);
}
}
}
运行结果:
2
2
可以看到原对象的值也被改变了。
下面我们通过定义一个简单的类方法实现深拷贝:
using System;
namespace TestApplication
{
class Box
{
public double length; // 长度
public double breadth; // 宽度
public double height; // 高度
public Box DeepCopy()
{
Box ret = new Box();
ret.length = this.length;
ret.breadth = this.breadth;
ret.height = this.height;
return ret;
}
}
class Program
{
static void Main(string[] args)
{
Box b1 = new Box();
b1.length = 1;
b1.breadth = 1;
b1.height = 1;
Box b2 = b1.DeepCopy();
b2.breadth = 2;
Console.WriteLine(b1.breadth);
Console.WriteLine(b2.breadth);
}
}
}
运行结果:
1
2
可以看到已经实现了深拷贝。但是这种方法对于比较成员变量比较简单的类来说还算可以,但是对于比较复杂的类来说自己实现较为麻烦,这也并不科学撒。
下面介绍一种通过序列化和反序列化的方法实现深拷贝。
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
namespace TestApplication
{
[Serializable]
class Box
{
public double length; // 长度
public double breadth; // 宽度
public double height; // 高度
public Box DeepCopy()
{
Box ret = new Box();
ret.length = this.length;
ret.breadth = this.breadth;
ret.height = this.height;
return ret;
}
}
public class DeepCopy
{
public static T DeepCopyByBin<T>(T obj)
{
object retval;
using (MemoryStream ms = new MemoryStream())
{
BinaryFormatter bf = new BinaryFormatter();
//序列化成流
bf.Serialize(ms, obj);
ms.Seek(0, SeekOrigin.Begin);
//反序列化成对象
retval = bf.Deserialize(ms);
ms.Close();
}
return (T)retval;
}
}
class Program
{
static void Main(string[] args)
{
Box b1 = new Box();
b1.length = 1;
b1.breadth = 1;
b1.height = 1;
Box b2 = DeepCopy.DeepCopyByBin(b1);
b2.breadth = 2;
Console.WriteLine(b1.breadth);
Console.WriteLine(b2.breadth);
}
}
}
运行结果:
1
2