本文将介绍C#类型系统中的值类型和引用类型,以及两者之间的一些区别。同时,还会介绍一下装箱和拆箱操作。
值类型和引用类型
首先,我们看看在C#中哪些类型是值类型,哪些类型是引用类型。
值类型:
- 基础数据类型(string类型除外):包括整型、浮点型、十进制型、布尔型。
- 整型(sbyte、byte、char、short、ushort、int、uint、long、ulong )
- 浮点型(float 和 double )
- 十进制型(decimal )
- 布尔型(bool )
- 结构类型(struct)
- 枚举类型(enum)
引用类型:
- class、interface、delegate、object、string、Array
默认值
变量的初始化中,都会有一个默认值,在C#中,我们可以通过default关键字去查看某个类型的默认值。
通过default(int)可以看到,int的默认值是0,default(bool)显示布尔类型的默认值是false。
对于所有的引用类型,默认值都会是null。
注意,这里有个特殊的情况就是结构struct,如果对一个结构进行default操作,我们将得到每个结构成员的初始值状态。也就是说,值类型成员赋予值类型的默认值,引用类型成员赋予引用类型的默认值。
简单对比值类型和引用类型
下面,我们通过一个简单的例子看看。假设有一个Point类型,有x和y两个坐标成员。
同样是下面一段代码
Point p1 = new Point(5,9); Point p2 = p1;
如果Point类型是通过结构struct实现,那么p2将会是p1的一个副本,也就是说任何一个的修改都不会影响另外一个;如果Point类型是通过类class实现,那么p2和p1的引用值将会指向同一个对象。
为了进一步了解值类型和引用类型,我们需要介绍一下栈和堆这两个基本概念。
栈和堆
当我们在32为系统上运行一个程序的时候,这个程序就会有一个4GB的进程运行空间。我们所要讨论的栈和堆就存放在这个4GB的空间中。
栈和堆的简介
在C#中,栈(Stack)是指堆栈;堆(Heap)是指托管堆,由.NET垃圾收集器自动管理。
这里就不对栈和堆进行详细的分析了,只是举一个简单的例子来大致描述栈和堆的工作原理。
从图中可以看到,局部变量在栈上的变化(入栈),当函数执行结束后,栈上的空间将会被清理;但是我们在堆上分配的空间始终从在,只能等待GC去帮我们清理不会被引用到的空间。
值类型和引用类型的存放
介绍过栈和堆之后,下面我们看看值类型和引用类型是怎么存放的。
对于值类型的变量,这个变量本身就代表这个值类型的值;但是,对于引用类型的变量,这个引用类型的实例是在托管堆上分配的空间,而这个变量本身只是代表一个指向托管堆实例的引用(指针)。
所以这里,我们可以对值类型和引用类型变量的存储有两个概括:
- 引用类型永远存储在堆里
- 值类型和引用(指针)永远存储在它们声明时所在的堆或栈里
- 如果一个值类型不是在方法中定义的,而是在一个引用类型里,那么此值类型将会被放在这个引用类型里并存储在堆上
注意:根据上面第二点概括,可以得到"值类型一定存储在栈中"这个说法是错误的。例如,我们有一个Student类,在这个类中的Age属性是一个值类型,但是这个值类型是存储在Student类实例的空间中,也就是在堆上。
class Student { public string Name { get; set; } public int Age{ get; set; } }
装箱和拆箱
由于C#中所有的数据类型都是由基类System.Object继承而来的,所以值类型和引用类型的值可以通过显式(或隐式)操作相互转换。
这里,可以将装箱和拆箱描述为:
- 装箱是将值类型转换为引用类型
- 拆箱是将引用类型转换为值类型
装箱/拆箱的内部操作
其实,在装箱和拆箱的过程中都对应一系列的转换,这里就通过下图表示了。
在值类型进行装箱时,生成的是全新的引用对象,这会有时间损耗,也就是造成效率降低。所以在C# 2.0中就引入了泛型来减少装箱操作和拆箱操作消耗。
总结
本文介绍了C#中的值类型和引用类型,以及栈和堆的基本概念。然后分析了值类型和引用类型在栈和堆中的存放。
同时,我们也了解到了:
- 当使用引用类型时,我们是在和指向引用类型的引用(指针)打交道,而不是引用类型本身
- 当使用值类型时,我们是在和值类型本身打交道