C#中类型分为两类:
- 值类型(Value Type)
- 引用类型(Reference Type)
值类型和引用类型是以它们在计算机内存中是如何被分配的来划分的。
值类型包括结构和枚举,值类型又包含一种特殊的值类型,称为简单类型,如:int
byte
所有值类型都隐式(在C#代码中,无法看到继承关系,但通过MSIL代码才可以看到)继承自System.ValueType
,而System.ValueType
和引用类型都继承自System.Object
基类
Tips:C#不支持多重继承,结构 已经隐式继承至System.ValueType,所以 结构 不支持继承。
什么是堆和栈?
堆和栈的概念:
- 栈(Stack)是一种后进先出的数据结构,在内存中,变量会被分配在栈上来进行操作
- 堆(heap)是用于为引用类型的实例(对象),分配空间的内存区域,在堆上创建一个对象,会将对象的地址传给栈上的变量(反过来叫变量指向此对象,或者变量引用此对象)-----也就是栈上的变量指向了堆上地址为XXX的实例(对象)
值类型
public struct ValPoint{
public int x; // 该值类型中的字段
}
ValPoint vPoint1; // ValPoint:结构值类型,vPoint1:变量,此时并没有被压到栈上
vPoint1.x = 10; // 进行入栈操作
Console.WriteLine(vPoint1.x); //进行出栈操作
变量包含了值类型中所有字段,该变量vPoint1 被分配在线程堆栈(Thread Stack)上。
Tips:只有在对变量操作时,变量才会进行入栈。对变量的操作,实际上是一系列的出栈和入栈操作。
结构特点1:
定义的结构内所有字段都必须初始化赋值,否则会报出“使用了可能为赋值的字段x”.因为这是 .Net 的一个约束,所有的元素使用前都必须初始化,如:
int i;
Console.WriteLine(i); // 未初始化变量,使用结构int中内部成员在使用前都必须对它赋值
结构特点2:
调用结构内的方法前,需要对结构内所有字段进行赋值
// 修改ValPoint结构
public struct ValPoint{
public int x;
public void Block( ){ }
}
//下面代码将会编译错误
ValPoint vPoint1;
// vPoint1.x = 200; // 在声明变量vPoint1后给结构中x变量赋值,才能编译通过
vPoint1.Block(); //使用了为赋值的局部变量vPoint1
Console.WriteLine(vPoint); //使用了为赋值的局部变量vPoint1
Tips:如果结构中有多个字段,则都需要为多个字段进行赋值
Q:上面例子中,结构内方法都没有使用字段x,为什么还要进行初始化赋值?这样子后如果结构中有若干个字段,初始化赋值岂不麻烦?
A:编译器隐式地为结构类型创建无参的够着函数,在这个构造函数中会对结构成员进行初始化,所有值类型被赋予0或相当于0的值,引用类型被赋予为null值(因此,Struct类型不可自行声明无参数的构造函数),所以通过隐式声明的构造函数去创建一个ValPoint类型变量:
ValPoint vPoint1 = new ValPoint();
Console.WriteLine(vPoint1.x); // 输出为 0
引用类型
声明一个引用类型变量,并使用new操作符创建引用类型实例的时候,该引用类型的变量会被分配到线程栈上,该变量只保存了位于堆上的引用类型的实例的内存地址,变量本身不包含任何类型的数据。
仅仅定义了变量,没有使用new操作符的话,由于没有在堆上创建类型的实例,因此,该变量值为null,不指向任何对象(堆上对象的实例)
Tips:容易混淆的:变量(Variable)、对象(Object)、实例(Instance)。变量可以是值类型,也可以是引用类型,如果是引用类型的话,由于本身只包含对实例对象的引用(内存地址),因此也叫对象引用;而在堆上创建的对象,称为对象的实例。
public class RefPoint{
public int x;
public RefPoint(int x){this.x = x;}
public RefPoint(){}
}
仅写下面一条声明语句时:
RefPoint rPoint1; // 在线程栈上创建一个不包含任何数据,也不指向任何对象(不包含内存地址)的变量
当使用new操作符时:
rPoint1 = new RefPoint(1);
- 在应用程序堆上创建一个引用类型对象的实例,并为它分配内存地址
- 自动传递该实例的引用给构造函数(正因如此,在构造函数中才能使用this来访问这个实例)
- 调用该类型的构造函数
- 返回该实例的引用内存地址,复制给 rPoint1 变量,该rPoint 引用对象保存的数据是指向在堆上创建改类型的实例地址。
简单类型
想比较两个int类型是否相等,通常会:
int i = 3;
int j = 3;
if(i == j){ Console.WriteLine("i equals to j");}
但是,对于自定义的引用类型,如结构,就不能使用 “==”来判断它们是否相等,而需要 Equals() 方法来完成。
string 类型是引用类型,如果:
string a = "123";
string b = "123";
if(a == b){Console.WirteLine("a equals to b");}
其比较的是他们是否指向堆上同一个对象。上面显然不同,但他们所包含的数值相同,所以,对string类型用 ‘==’比较,实际上是比较引用类型内包含的值,而不是其引用。
装箱和拆箱
- 装箱: 值类型 -> 等价的引用类型
int i = 1;
Object boxed = i;
Console.WriteLine("Boxed Point:" + boxed);
- 在堆上为新的对象实例分配内存。该对象实例包含数据,但没有名称;
- 在栈上值类型变量的值复制到堆上的对象中
- 将堆上创建的对象的地址返回给引用类型变量
- 拆箱:将一个已装箱的引用类型 -> 值类型
int i = 5;
Object boxed = i;
j = (int) boxed;
Console.WriteLine("UnBoxed Point : " + j);
- 获取已装箱的对象的地址
- 将值从堆上的对象中复制到堆栈上的值变量中
可见,装箱与拆箱需要反复在堆上进行操作,所以,在程序中尽量避免无意义的装箱与拆箱。