一、值类型和引用类型
C# 中的类型一共分为两类,一类是值类型(Value Type),一类是引用类型(Reference Type)。
值类型包括结构体(struct)和枚举(enum)。
引用类型包括类(class)、接口(interface)、委托(delegate)、数组(array)等。常见的简单类型如short、int、long、float、double、byte、char等其本质上都是结构体,对应struct System.Int16、System.Int32、System.Int64、System.Single、System.Double、Syetem.Byte、System.Char,因此它们都是值类型。但string和object例外,它们本质上是类,对应class System.String和System.Object,所以它们是引用类型。
1. 值类型
值类型变量本身保存了该类型的全部数据,当声明一个值类型的变量时,该变量会被分配到栈(Stack)上。
2. 引用类型
引用类型变量本身保存的是位于堆(Heap)上的该类型的实例的内存地址,并不包含数据。当声明一个引用类型变量时,该变量会被分配到栈上。如果仅仅只是声明这样一个变量,由于在堆上还没有创建该类型的实例,因此,变量值为null,意思是不指向任何类型实例(堆上的对象)。对于变量的类型声明,用于限制此变量可以保存的类型。
二、值传递和引用传递
C#中方法的参数传递默认的是值传递,引用传递和输出传递需要在参数类型前面对应加上ref、out限制符,由于输出传递和引用传递类似,这里只讨论引用传递。
值传递参数是原变量的拷贝,值传递参数和原变量的内存地址不同,因此方法中对值传递参数的修改不会改变原变量。
引用传递参数是原变量的指针,引用传递参数和原变量的内存地址相同,相应的方法中对引用传递参数的修改会改变原变量。
三、传递值类型参数
1. 通过值传递值类型
class PassingValByVal
{
private static void Change(int x)
{
x = 10;
System.Console.WriteLine("The value inside the method: {0}", x);
}
public static void Execute()
{
int n = 5;
System.Console.WriteLine("The value before calling the method: {0}", n);
Change(n);
System.Console.WriteLine("The value after calling the method: {0}", n);
System.Console.ReadLine();
}
}
- 原变量 n 是 int 值类型,系统为原变量 n 在堆栈Stack上分配的内存地址为:0x088aed2c,该内存地址存储的数据值为5。
- 当调用 Change 方法时,由于是值传递,系统会为局部参数变量 x 在堆栈Stack上分配一个新的内存区域, 内存地址为:0x088aecd0,并将 n 中的数据值 5 复制到局部参数变量 x 中。这里可以看到局部参数变量 x 和原变量 n 的内存地址是不同的。
- 对局部参数变量 x 的赋值操作是对变量内存地址中的数据值进行修改,此处是将局部参数变量 x 的内存地址 0x088aecd0 中的数据值由原来的 5 改为 10。
- 因为原变量 n 和局部参数变量 x 并不是同一块内存区域(内存地址不同),所以 Change 方法中对局部参数变量 x 的赋值操作不会影响到原变量 n。n 的值在调用 Change 方法前后是相同的。实际上,方法内发生的更改只影响局部参数变量 x。
2. 通过引用传递值类型
class PassingRefByVal
{
private static void Change(ref int x)
{
x = 10;
System.Console.WriteLine("The value inside the method: {0}", x);
}
public static void Execute()
{
int n = 5;
System.Console.WriteLine("The value before calling the method: {0}", n);
Change(ref n);
System.Console.WriteLine("The value after calling the method: {0}", n);
System.Console.ReadLine();
}
}
- 原变量 n 是 int 值类型,系统为原变量 n 在堆栈Stack上分配的内存地址为:0x0877eb4c,该内存地址存储的数据值为5。
- 当调用 Change 方法时,由于是引用传递,局部参数变量 x 的内存地址为原变量 n 的内存地址 0x0877eb4c,故局部参数变量 x 的值即是原变量 n 的值。这里可以看到局部参数变量 x 和原变量 n 的内存地址是相同的。
- 对局部参数变量 x 的赋值操作是对变量内存地址中的数据值进行修改,此处是将局部参数变量 x 的内存地址 0x0877eb4c 中的数据值由原来的 5 改为 10。这里监视窗口中 *&n 的值没有改变是因为在 Change 方法代码块中访问不到原变量 n ,数值没有刷新。
- 因为原变量 n 和局部参数变量 x 是同一块内存区域(内存地址相同),所以调用 Change 方法后,可以看到变量 n 的内存地址 0x0877eb4c 中的数据值为 10。
四、传递引用类型参数
1. 通过值传递引用类型
class ClassA
{
public int a;
public ClassB classB;
}
class ClassB
{
public int b;
}
class PassingRefByVal
{
private static void Change(ClassA tempClassA)
{
tempClassA.a = 1;
tempClassA.classB.b = 1;
tempClassA = new ClassA();
tempClassA.a = 2;
tempClassA.classB = new ClassB();
tempClassA.classB.b = 2;
System.Console.WriteLine("The value inside the method: a = {0} ,b = {1}", tempClassA.a, tempClassA.classB.b);
}
public static void Execute()
{
ClassA classA = new ClassA();
classA.a = 0;
classA.classB = new ClassB();
classA.classB.b = 0;
System.Console.WriteLine("The value before calling the method: a = {0} ,b = {1}",
classA.a, classA.classB.b);
Change(classA);
System.Console.WriteLine("The value after calling the method: a = {0} ,b = {1}",
classA.a, classA.classB.b);
System.Console.ReadLine();
}
}
- 原变量 classA 是 ClassA 引用类型,系统为原变量 classA 在堆栈Stack上分配的内存地址为:0x086ce92c,该内存地址存储的数据值为 37492360(十六进制为 0x023c1688),该数据值是系统分配在托管堆Heap上的 ClassA 类型实例的内存地址。从图中可以看到 classA.a 的内存地址是0X023c1690,0x023c1688~0x023c1690保存了 ClassA 类型的方法表指针和 SyncBlockIndex 。
- 当调用 Change 方法时,由于是值传递,系统会为局部参数变量 tempClassA 在堆栈Stack上分配一个新的内存区域, 内存地址为:0x086ce8c0,并将 classA 中的数据值 37492360(十六进制为 0x023c1688) 复制到局部参数变量 tempClassA 中。这里可以看到局部参数变量 tempClassA 和原变量 classA 的内存地址是不同的,但由于它们的内存地址中的数据值相同(均为 37492360,十六进制为0x023c1688),所有它们指向了同一个内存地址,也就是同一个 ClassA 类型实例。
- 因为局部参数变量 tempClassA 和原变量 classA 指向了同一个 ClassA类型实例,所以 tempClassA.a 和 classA.a、tempClassA.classB.b 和 classA.classB.b 是同一块内存地址,因此对 tempClassA.a 和 tempClassA.classB.b 的赋值操作同时也是对 classA.a 和 classA.classB.b 的赋值操作。这里监视窗口中 classA.a 和 classA.classB.b 的数据值还没有刷新。
- new运算符创建了一个新的 ClassA 类型的实例,系统为新实例在托管堆Heap上分配了另一块内存区域,内存地址为 37631124(十六进制为 0x023e3494),赋值操作将新实例的内存地址覆盖 tempClassA 内存地址中的数据值,即 tempClassA 指向了新的对象。
- 因为局部参数变量 tempClassA 指向了新创建的实例,后续对 tempClassA 的修改只会对新实例的内存区域的数值进行修改,而不会影响到原变量 classA。
2. 通过引用传递引用类型
class PassingRefByRef
{
private static void Change(ref ClassA tempClassA)
{
tempClassA.a = 1;
tempClassA.classB.b = 1;
tempClassA = new ClassA();
tempClassA.a = 2;
tempClassA.classB = new ClassB();
tempClassA.classB.b = 2;
System.Console.WriteLine("The value inside the method: a = {0} ,b = {1}",
tempClassA.a, tempClassA.classB.b);
}
public static void Execute()
{
ClassA classA = new ClassA();
classA.a = 0;
classA.classB = new ClassB();
classA.classB.b = 0;
System.Console.WriteLine("The value before calling the method: a = {0} ,b = {1}",
classA.a, classA.classB.b);
Change(ref classA);
System.Console.WriteLine("The value after calling the method: a = {0} ,b = {1}",
classA.a, classA.classB.b);
System.Console.ReadLine();
}
}
- 同理,引用传递的局部参数变量的内存地址为原变量的内存地址,后续对局部参数的修改同时也是对原变量的修改,故最后 classA.a 和 classA.classB.b 的值为2。
五、拷贝
拷贝即是通常所说的复制(Copy)或克隆(Clone),对象的拷贝也就是从当前对象复制一个“一模一样”的新对象出来。虽然都是复制对象,但是不同的复制方法,复制出来的新对象却并非完全一模一样,对象内部存在着一些差异。通常的拷贝方法有两种,即深拷贝和浅拷贝。
在浅拷贝中,拷贝对象会复制当前对象本身的数据(包括子对象的引用,拷贝对象和当前对象引用同一个子对象),但子对象的数据不会被复制。在深拷贝中,拷贝对象会复制当前对象所有的数据,包括当前对象子对象的数据;可以看出,深拷贝和浅拷贝之间的区别在于是否复制了子对象。
1. 浅拷贝
通过 MemberwiseClone 方法创建一个浅拷贝,该方法会创建一个新对象,然后将当前对象的非静态字段复制到新的对象。 如果一个字段是值类型,则执行该字段的逐位复制。 如果一个字段是引用类型,则将引用复制但被引用的对象不会复制;因此,原始对象和其克隆引用同一对象。
class ClassA
{
public int a;
public ClassB classB;
public ClassA ShallowCopy()
{
return (ClassA)MemberwiseClone();
}
}
class TestShallowCopy
{
public static void Execute()
{
ClassA classA = new ClassA();
classA.a = 0;
classA.classB = new ClassB();
classA.classB.b = 0;
ClassA copy = classA.ShallowCopy();
System.Console.WriteLine("The value before change: a = {0} ,b = {1}",
copy.a, copy.classB.b);
classA.a = 1;
classA.classB.b = 1;
System.Console.WriteLine("The value after change: a = {0} ,b = {1}",
copy.a, copy.classB.b);
System.Console.ReadLine();
}
}
2. 深拷贝
class ClassA
{
public int a;
public ClassB classB;
public ClassA DeepCopy()
{
ClassA copy = new ClassA();
copy.a = a;
copy.classB = new ClassB();
copy.classB.b = classB.b;
return copy;
}
}
class TestDeepCopy
{
public static void Execute()
{
ClassA classA = new ClassA();
classA.a = 0;
classA.classB = new ClassB();
classA.classB.b = 0;
ClassA copy = classA.DeepCopy();
System.Console.WriteLine("The value before change: a = {0} ,b = {1}",
copy.a, copy.classB.b);
classA.a = 1;
classA.classB.b = 1;
System.Console.WriteLine("The value after change: a = {0} ,b = {1}",
copy.a, copy.classB.b);
System.Console.ReadLine();
}
}
3. ICloneable 接口
ICloneable 接口使您可以提供一个自定义的实现用于创建一个现有对象的拷贝。包含一个成员 Clone 方法,旨在提供超出 Object.MemberwiseClone 方法的克隆支持。
class ClassA : ICloneable
{
public int a;
public ClassB classB;
public object Clone()
{
ClassA copy = new ClassA();
copy.a = a;
copy.classB = new ClassB();
copy.classB.b = classB.b;
return copy;
}
}