.NET Framework 2.0 程序设计
图 2.2-1通用类型系统
2.2.1 值类型和引用类型
值类型继承自ValueType类,值类型的变量直接存储数据,实例是被分配在栈中的,并且永远不可能为空。这一类的类型包括Byte、Char、各种长度的有符号和无符号整型、单精度和双精度浮点型、Decimal、Boolean 等等。
引用类型是继承自Object,引用类型变量存储的是数据内存的地址,而实例则被分配在可以进行垃圾回收的堆中。由于一份数据可以被多个变量引用,使用这种变量类型能够起到节省内存资源的作用。但是数据只有一份,一个引用对数据进行的修改也会影响到其它的引用,因为它们都指向同一个数据源。引用类型的变量是可以为空的,那表示它不指向任何对象。相比之下,引用类型通常更为复杂。例如,如图 2-1中所示,Class、Interface、Array和String是引用类型。
现在来理解值类型和引用类型之间的区别。CTS 中您必须首先理解的基本特点是内存对于每种类型的实例是如何分配的。在托管代码中,值可通过两种主要方式中的任一种分配其内存,两种方式都由 CLR 管理,它们是:在堆栈(Stack)上分配或在堆(Heap)上分配。分配在托管堆栈上的变量通常是在调用方法时或者正在运行的方法创建这些变量时创建的。无论哪种情况,堆栈变量所使用的内存将在创建它们的方法返回时自动获得释放。例如,在一个方法中声明了一个临时整型变量A,它的值是169(如图 2-2所示),在该方法调用返回时,A在堆栈上所占用内存将会自动释放。但是,分配在托管堆上的变量不会在创建它们的方法结束时释放其内存。相反,这些变量所使用的内存通过称为垃圾回收的过程进行释放。例如,在方法中声明了临时String类型变量B,它指向的值是Hello(如图 2-2所示),在该方法调用返回时,B在堆栈上所占用内存将会自动释放,但是在堆上的值会在垃圾回收时才释放。
值类型和引用类型之间的基本区别是:
12
第2章 公共语言运行库和类型
值类型的单独实例分配在堆栈上
z 引用类型的实例只在堆栈上分配了的对其实际值的引用,值本身分配在堆上。
图 2-2显示了此情况的抽象图。图中显示三个值类型 Int16、Char 和 Int32 的实例已创建在托管堆栈上,而引用类型String 的一个实例存在于托管堆上。理解值类型和引用类型之间区别是理解CTS类型系统,并最终理解基于CLR的语言所使用的类型的根本点。
z
图 2.2-2 值类型和引用类型
13
.NET Framework 2.0 程序设计
2.2.2 值类型和引用类型之间的相互转换:装箱和拆箱
图 2.2-3 装箱和拆箱
将值类型数据指定给引用类型数据时,系统会先从堆之中配置一块内存,然后将值类型数据复制到这一内存,最后再使引用类型数据指向到这一内存,这样的过程称为装箱(Boxing)。反之即为拆箱(UnBoxing),而且只有引用类型数据需要拆箱。
当集合仅接受引用类型数据,而你希望向该集合添加一个值类型的整数时,你该怎么
做?.NET Framework 提供了一个解决方案。在将数据添加到集合之前,可以通过装箱来转换数据。装箱可以简单理解为把值类型数据从堆栈上迁移到堆上的操作。图 2-3中演示了将Int32:169这个整型从堆栈上转移到堆上的装箱过程。而它的逆操作拆箱则刚好相反,是把值类型从堆上迁移回堆栈的操作。装箱和拆箱使你能方便操作数据,使其在引用类型和值类型之间转化。
构建于CLR的语言通常隐藏了装箱过程,因此开发人员可能不需要显式请求此转换。但是,装箱对性能有影响,它耗费时间,并且对装箱值的引用比对未装箱值的引用稍慢,装箱值的行为稍稍不同于未装箱值。尽管该过程通常隐蔽发生,但它仍值得我们一探究竟。
在大多数情况下,装箱是隐式进行的。例如,当向接受引用类型的方法传递一个值类型参数时,将隐式地进行装箱。和装箱不同的是,拆箱总是显式地执行。在拆箱的过程中,需要注意的是,只能将已装箱的数据拆箱为装箱前的类型。例如,如果将Int32装箱为一个对象,则只能将已装箱的Int32值拆箱为Int32类型。
以下代码示例显示了如何使用装箱将整型值变量 a 转换为引用变量o。存储在变量 a 中的值从 100 更改为 200。该示例显示对象o 保持原始值 100。 [C#]
14
第2章 公共语言运行库和类型
int a = 100; //装箱操作 object o = a; a = 200;
Console.WriteLine(\ Console.WriteLine(\
[VB.NET]
Dim a as Integer = 100 '装箱操作
Dim o as Object = a a = 200
Console.WriteLine(\ Console.WriteLine(\
显示的结果为:
The value-type value = 200 The object-type value = 100
以下代码示例实现拆箱操作。显式地将引用变量 o 转换回整型值变量 a。 [C#]
int a = 1; Object o = a; a = 100;
Console.WriteLine(a); //拆箱操作 a = (int) o;
Console.WriteLine(a);
[VB.NET]
Dim a as Integer = 1 Dim o as Object = a a = 100
Console.WriteLine(a) '拆箱操作 a = CInt(o)
Console.WriteLine(a)
显示结果为: 100 1
15
.NET Framework 2.0 程序设计
2.2.3 类型转型
.NET Framework中进行的转换操作不仅仅是装箱和拆箱。除了这些方法以外,.NET
Framework 允许数据类型转换,又称为类型强制转换。在需要将某种类型的值传递给另一种类型时,可以使用类型转换。例如,当将浮点值传递给 Int32 类型时,需要将 Single 类型强制转换为 Int32类型。在这种情况下,可以对值类型和引用类型应用类型强制转换。可以显式或隐式地执行类型强制转换。
2.2.3.1 隐式强制转换
隐式强制转换不要求在源代码中使用任何特殊语法。编译器自动执行隐式强制转换。 例如,如果将变量 x 定义为 Single 并执行某些乘法操作,则这些操作可能产生一个 Single 变量无法存储的大值。因此,需要另外声明一个 Double 变量 y 以存储结果值。此处,在将结果 Single 值存储到 Double 变量前,编译器会对其执行隐式强制转换。
隐式强制转换也称为“扩展转换”,因为要将窄数据类型转换为宽数据类型。这还将确保不会在转换过程中丢失数据。
以下代码示例显示了如何实现隐式强制转换。在该代码中,将 Int32 类型的值赋给 Double 类型的变量。 [C#]
Int32 a; Double b; a = 100; b = a;
[VB.NET]
Dim a As Int32 Dim b As Double a = 100 b = a
2.2.3.2 显式强制转换
在显式强制转换中,将宽数据类型转换为窄数据类型。例如,可以将 Double 类型的值转换为 Int32 类型。因此,显式强制转换也称为“收缩转换”。当要转换的值超过目标数据类型的范围时,该转换类型可能导致数据丢失。
在VB.NET中,可以使用多种转换函数,例如CInt和CStr,而在 C# 中,需要使用强制转换运算符来执行显式强制转换。
.NET Framework 还提供了一些对VB.NET和C#通用的转换方法。这些方法在 System.Convert类中定义为静态,以执行显式强制转换。
以下代码示例实现了显式强制转换。在该代码中,会将 Int64 类型的值转换为 Int32 类型。 [C#]
16