参考资料
- 《.NET之美–.NET关键技术深入与解析》
前言
C#中的类型一共分为两类,一类是值类型(Value Type),一类是引用类型(Reference Type)。值类型和引用类型是以它们在计算机内存中是如何被分配来划分的。值类型包括了 结构 和 枚举,引用类型则包括了 类、接口、委托 等。还有一种特殊的值类型,称为简单类型,比如byte、int等,这些简单类型实际上是BCL基类库类型的别名。比如,声明一个int类型,实际上是声明一个 System.Int32 结构类型。因此,在 Int32 类型中定义的方法或属性,都可以在int类型上调用,比如:
123.Equals(2);
所有的值类型都隐式地继承自 System.ValueType 类型(注意 System.ValueType 本身是一个类类型)。之所以说是“隐式地”,是因为在C#代码中,是看不到这个继承关系的,这个关系只有通过MSIL代码才能看到。 System.ValueType 类型和所有的引用类型都继承自 System.Object 基类。
C#不支持多重继承,因为结构已经隐式地继承自ValueType,所以结构不支持继承
说明:
栈(stack)是一种 后进先出的数据结构,在内存中,变量会被分配在栈上来进行操作。堆(heap)是用于为==引用类型的实例(对象)==分配空间的内存区域,在堆上创建一个对象,会将对象的地址传给栈上的变量(反过来叫变量指向此对象,或者变量引用此对象)。
值类型
当声明一个值类型的变量的时候,变量本身包含了值类型的全部字段,该变量会被分配在线程堆栈(Thread Stack)上。
假如有下面这个值类型,代表了直线上的一点:
public struct ValPoint{
public int x;
public ValPoint(int x){
this.x=x;
}
}
当在程序中声明一个变量:
ValPoint vPoint1;
vPoint1.x=10;
Console.WriteLine(vPoint1.x);//输出10
上面代码中,因为变量已经包含了值类型的所有字段,所以已经可以进行操作,并且只有对变量进行操作(vPoint1.x=10
),才会进行入栈。对变量进行操作,实际上是一系列入栈、出栈操作。
如果将ValPoint改为引用类型class,则会出现编译错误:使用了未赋值的局部变量“vPoint1”。除此之外,引用类型在运行时经常会抛出NullReferenceException
异常。
如果不对vPoint1.x进行赋值,直接写Console.WriteLine(vPoint1.x)
,则会出现编译错误:使用了可能为赋值的字段x
。这是因为.NET的一个约束:所有的元素使用前都必须初始化。比如下面语句也会引发这个错误:
int i;
Console.WriteLine(i);
虽然结构类型变量本身不需要像类一样使用new操作符创建一个实例(其本身就相当于一个实例),但如果要使用它的内部成员,则要在使用前对它进行赋值。结构还有一个特性:调用结构上的方法前,需要对其所有字段进行赋值。 修改ValPoint:
public struct ValPoint{
public int x;
public void Blank(){
}
}
那么下面的代码将会发生编译错误:
ValPoint vPoint1;
vPoint1.Blank();//使用了未赋值的变量vPoint1
Console.WriteLine(vPoint1);//使用了未赋值的变量vPoint1
解决上述问题可以通过这样一种方式:编译器隐式地为结构类型创建无参数的构造函数。在这个构造函数中会对结构成员进行初始化,所有的值类型成员被赋予0或者相当于0的值,所有的引用类型被赋予null值。(因此,Struct类型不可以自行声明无参数的构造函数)。所以,可以通过隐式声明的构造函数去创建一个ValPoint类型变量:
ValPoint vPoint1=new ValPoint();
Console.WriteLine(vPoint1.x);//输出0
引用类型
当声明一个引用类型变量,并使用new操作符创建引用类型实例的时候,该引用类型的变量会被分配到线程栈上,变量保存了位于堆上的引用类型的实例的内存地址。变量本身不包含任何类型所定义的数据。如果仅仅声明一个变量,但不使用new操作符,由于在堆上还没有创建类型的实例,因此,变量值为null,即不指向任何对象。
如果有这样一个类,它依然代表直线上的一点:
public class RefPoint{
public int x;
public RefPoint(int x){this.x=x}
public RefPoint(){}
}
当仅仅写下
RefPoint rPoint1;
它会在线程栈上创建一个不包含任何数据,也不指向任何对象(不包含内存地址)的变量。
而当使用new操作符时:
rPoint1=new RefPoint(1)
则会完成下面几件事:
- 在应用程序堆上创建一个引用类型对象的实例,并分配内存地址
- 自动传递该实例的引用给构造函数。(正因为如此,才可以使用
this
来访问这个实例) - 调用该类型的构造函数。
- 返回该实例的引用(内存地址),赋值给人Point1变量
装箱和拆箱
简单来说,装箱就是将一个值类型转换为等价的引用类型。它的过程分为这样几步:
- 在堆上为新生成的对象实例分配内存。该对象实例包含数据,但没有名称。
- 将栈上值类型变量的值复制到堆上的对象中。
- 将堆上创建的对象的地址返回给引用类型变量。
装箱实例代码如下:
int i=1;
Object boxed=i;
Console.WriteLine("Boxed Point:"+boxed);
拆箱则是将一个已装箱的引用类型转换为值类型:
int i=1;
Object boxed=i;
int j;
j=(int)boxed;
Console.WriteLine("UnBoxed Point: " + j);
需要注意的是:拆箱操作需要显示声明拆箱后转换的类型。它分为两步来完成
- 获取已装箱的对象的地址。
- 将值从堆上的对象中复制到堆栈上的值变量中。
可见,装箱和拆箱需要反复在堆上进行操作,因此,在程序中应该尽量避免无意义的装箱和拆箱。