3.3 数据类型
C#中的变量类型分为两种:值类型和引用类型。两者的差异在于数据存储方式不同,值类型变量直接存储实际数据值,引用类型变量则存储实际数据的引用,程序可通过此引用找到真正的数据。
3.3.1 值类型
值类型变量直接存储变量的数据值,包含整数类型、浮点类型以及布尔类型等。值类型变量在栈中进行分配,因此效率很高。值类型变量具有如下特性。
值类型变量存储在栈中。
访问值类型变量时,一般都是直接访问其实例。
每个值类型变量都有自己的数据副本,因此对某个值类型变量的操作不会影响到其他变量。
复制值类型变量时,复制的是变量的值,而不是变量的地址。
值类型变量不能为null,必须具有一个确定的值。
值类型是从System.ValueType类继承而来的类型,下面详细介绍其包含的几种数据类型。
1.整数类型
整数类型用来存储整数数值,即没有小数部分的数值。可以是正数,也可以是负数。C#中内置的整数类型如表3.1所示。
表3.1 C#内置的整数类型
其中,sbyte、byte、short、ushort类型用于表示范围较小的整数。使用这种类型时,必须特别注意数值的大小,稍不注意就会导致运算溢出,产生错误。
注意
C#中,整型数据包括3种表示形式:十进制、八进制和十六进制。
十进制:用0~9表示,逢十进位,不能以0作为开头(0除外),如120、0、-127。
八进制:用0~7表示,逢八进位,必须以0开头,如0123(十进制数为83)、-0123(十进制数为-83)。
十六进制:用0~9和A~F(或a~f)表示,逢十六进位,必须以0X或0x开头,如0x25(十进制数为37)、0Xb01e(十进制数为45086)。
【例3.2】使用int和byte变量(实例位置:资源包\TM\sl\3\2)
创建一个控制台应用程序,声明一个int类型变量ls并初始化为927,一个byte类型变量shj并初始化为255,最后输出,代码如下。
程序运行结果如下。
ls=927 shj=255
上述代码中,如果为变量shj赋值266并再次编译,就会出现错误。这是因为byte类型的变量范围为0~255,266超出了范围,所以编译会出错。
注意
在定义局部变量时,要对其进行初始化。
2.浮点类型
浮点类型变量主要用于存储含有小数的数值,包含float和double两种数值类型,如表3.2所示。
表3.2 浮点类型及描述
一般情况下,包含小数点的数值会被系统默认为double类型。在数值后添加f或F,可将其强制指定为float类型。例如:
float theMySum = 9.27f; //使用f或F强制指定为float类型
误区警示
要使用float类型变量,必须在数值后添加f或F,否则编译器会将其作为double类型处理。也可以在double类型的值前面添加(float),对其进行强制类型转换。
3.decimal类型
decimal类型表示128位数据类型,它是一种精度更高的浮点类型,精度可达到28~29位,取值范围为±1.0×10-28~±7.9×1028。
decimal类型的高精度特性,使得它更适合用于财务和货币计算。如果希望一个小数被当成decimal类型使用,需要使用后缀m或M,例如:
decimal myMoney = 1.12m;
4.布尔类型
布尔类型主要用来表示true和false,通常用在流程控制语句中,作为判断条件使用。
布尔变量的值只能是true或false,不能将其他值指定给布尔类型变量,也不能将布尔类型变量转换为其他类型。例如,下面的赋值方式是错误的,编译器会提示“常量值927无法转换为bool”。
bool x = 927;
布尔类型的正确定义方式如下:
bool flag = true; bool flag2 = false;
说明
(1)在定义全局变量时,如果没有特定的要求,不用对其进行初始化,整数类型和浮点类型的默认初始化为0,布尔类型的初始化为false。
(2)布尔类型变量大多数被应用到流程控制语句当中,例如,循环语句或者if语句等。
3.3.2 引用类型
引用类型是构建C#应用程序的主要对象类型数据。所有被称为“类”的都是引用类型,主要包括类、接口、数组和委托。
程序执行过程中,预先定义的对象类型使用new关键字创建对象实例,并存储在堆中。堆是一种由系统弹性配置的内存空间,没有特定大小及存活时间,可弹性运用于对象访问。简单来说,引用类型就类似于生活中的代理商,代理商没有自己的产品,而是代理厂家的产品,这些被代理的产品就好像自己的产品一样。
引用类型具有如下特征。
必须在托管堆中为引用类型变量分配内存。
使用new关键字来创建引用类型变量。
在托管堆中分配的每个对象都有与之相关联的附加成员,这些成员必须被初始化。
引用类型变量是由垃圾回收机制来管理的。
多个引用类型变量可以引用同一对象,在这种情形下,对一个变量的操作会影响另一个变量所引用的同一对象。
引用类型被赋值前,值都是null。
【例3.3】通过引用类型改变变量的值(实例位置:资源包\TM\sl\3\3)
创建一个控制台应用程序,在其中创建一个类C,在类中建立一个字段Value,初始化为0,然后在程序其他位置通过new关键字创建对此类的引用类型变量,最后输出,代码如下。
程序运行结果如下。
Values:0,927 Refs:112,112
3.3.3 值类型与引用类型的区别
从概念上看,值类型直接存储数据值,引用类型存储对数据值的引用,两者存储在内存的不同地方。C#中,必须在设计类型时就决定类型实例的行为。
从内存空间上看,值类型在栈中操作,引用类型在堆中分配存储单元。栈在编译时就分配好内存空间,在代码中有栈的明确定义,而堆是程序运行中动态分配的内存空间,可以根据程序的运行情况动态地分配内存大小。因此,值类型总在内存中占用一个预定义的字节数,而引用类型变量则在堆中分配一个内存空间,这个内存空间包含的是对另一个内存位置的引用,这个位置是托管堆中的一个地址,即存放此变量实际值的地方。
说明
C#的所有值类型均隐式派生自System.ValueType,而System.ValueType直接派生于System.Object,即System.ValueType本身是一个类类型,而不是值类型。其关键在于ValueType重写了Equals()方法,从而对值类型按照实例的值来比较,而不是引用地址来比较。
下面来看一段代码,仔细体会值类型与引用类型的区别。
运行结果如图3.2所示。可以看出,改变Stamp_1.Age的值时,age没跟着变;改变Stamp_2.Name的值后,guru.Name跟着变了。这是为什么呢?
图3.2 值类型与引用类型
这就是值类型和引用类型的区别。声明age值类型变量时,将Stamp_1.Age的值赋给它,这时编译器在栈上分配了一块空间,然后把Stamp_1.Age的值填进去,二者没有任何关联,就像在计算机中复制文件一样,只是把Stamp_1.Age的值复制给age。引用类型则不同,在声明guru时把Stamp_2赋给它。引用类型包含的只是堆上数据区域地址的引用,其实就是把Stamp_2的引用也赋给guru,因此它们指向了同一块内存区域。既然指向同一块区域,不管修改谁,另一个的值都会跟着改变。
这种关联关系就好比信用卡和亲情卡,用亲情卡取了钱,与之关联的信用卡账户也会发生变化。
3.3.4 枚举类型
枚举类型是一种独特的值类型,通常用于声明一组具有相同性质的常量。
编写与日期相关的应用程序时,经常需要使用年、月、日、星期等日期数据,可以将这些数据组织成多个不同名称的枚举类型。使用枚举可以增加程序的可读性和可维护性。同时,枚举类型可以避免类型错误。
说明
在定义枚举类型时,如果不对其进行赋值,默认情况下,第一个枚举数的值为0,后面每个枚举数的值依次递增1。
C#中使用关键字enum类声明枚举,语法形式如下。
其中,大括号“{}”中的内容为枚举值列表,每个枚举值均对应一个枚举值名称,value1~valueN为整数数据类型,list1~listN则为枚举值的标识名称。
【例3.4】当前系统日期是星期几(实例位置:资源包\TM\sl\3\4)
创建一个控制台应用程序,通过使用枚举来判断当前系统日期是星期几,代码如下。
上述代码中,首先通过enum关键字建立一个枚举,枚举值名称分别代表一周中的7天。如果枚举值名称为Sun,代表星期日,其枚举值为0,以此类推。然后声明一个int类型的变量k,用于获取当前表示的日期是星期几。最后,调用switch语句,输出当天是星期几。
这里,当前日期是2022年12月23日星期五,所以输出结果显示当天是星期五。
3.3.5 类型转换
类型转换就是将一种类型转换成另一种类型,转换可以是隐式转换,也可以是显式转换。
1.隐式转换
所谓隐式转换,就是不需要声明就能进行的转换,即编译器不需要进行检查就能自动进行转换。表3.3列出了可以进行隐式转换的数据类型。
表3.3 隐式类型转换表
从int、uint、long或ulong到float,以及从long或ulong到double的转换可能导致精度损失,但是不会影响其数量级。其他的隐式转换不会丢失任何信息。
例如,将int类型的值隐式转换成long类型,代码如下。
int i = 927; //声明一个整型变量i并初始化为927 long j = i; //隐式转换成long类型
2.显式转换
显式转换也称为强制转换,需要在代码中明确声明要转换的类型。如果要把高精度的变量值赋给低精度的变量,就需要使用显式转换。表3.4列出了需要进行显式转换的数据类型。
表3.4 显式类型转换表
由于显式转换包括所有隐式转换和显式转换,因此使用强制转换可将任何数值类型转换为其他数值类型。
【例3.5】显式类型转换的使用(实例位置:资源包\TM\sl\3\5)
创建一个控制台应用程序,将double类型的x进行显式类型转换,代码如下。
程序运行结果为19810927。
也可以通过Convert关键字进行显式类型转换,上述例子还可以通过下面的代码实现。
3.装箱和拆箱
将值类型转换为引用类型的过程叫作装箱,相反,将引用类型转换为值类型的过程叫作拆箱。装箱允许将值类型隐式转换成引用类型,拆箱允许将引用类型显式转换为值类型。下面来看两个例子。
【例3.6】整型变量的装箱操作(实例位置:资源包\TM\sl\3\6)
创建一个控制台应用程序,声明一个整型变量i并赋值为2048,然后将其复制到装箱对象obj中,最后再改变变量i的值,代码如下。
程序运行结果如下。
1、i的值为2048,装箱之后的对象为2048 2、i的值为927,装箱之后的对象为2048
从程序运行结果可以看出,值类型变量的值复制到装箱得到的对象中,装箱后改变值类型变量的值,并不会影响装箱对象的值。
【例3.7】拆箱操作的实现(实例位置:资源包\TM\sl\3\7)
创建一个控制台应用程序,声明一个整型变量i并赋值为112,然后将其复制到装箱对象obj中,最后,进行拆箱操作将装箱对象obj赋值给整型变量j,代码如下。
程序运行结果如下。
装箱操作:值为112,装箱之后对象为112 拆箱操作:装箱对象为112,值为112
查看程序运行结果,不难看出,拆箱后得到的值类型数据的值与装箱对象相等。需要注意的是,执行拆箱操作时要符合类型一致的原则,否则会出现异常。
误区警示
装箱是将一个值类型转换为一个对象类型(object),拆箱则是将一个对象类型显式转换为一个值类型。对于装箱而言,它是复制出一个被装箱的值类型的副本来进行转换;而对于拆箱而言,需要注意类型的兼容性,如不能将一个值为string的object类型转换为int类型。
编程训练(答案位置:资源包\TM\sl\3\编程训练\)
【训练 3】模拟输出中国联通流量提醒 定义两个浮点型变量,分别表示已用流量(3.592)和剩余流量(3.408),定义一个字符型变量,用来表示网址(http://u.10010.cn/tAE3v),编写一个程序,输出中国联通流量提醒。
【训练 4】记录你的密码 编写一个程序,让用户输入密码,假设密码为0oO1Il,要求把每次用户输入的密码保存到变量pass中,输入6次后输出每次输入的密码并退出程序。