第3章 数据类型
本章视频教学录像:53分钟
通过第2章的学习,我们对编写C#程序有了一个简单的了解。要想真正编出自己想要的程序,实现需要的功能,必须熟练掌握C#语法知识,这样在以后的练习中才能顺利进阶,达到高手水平。本章将介绍数据类型相关的内容。
本章要点(已掌握的在方框中打钩)
□ 值类型
□ 引用数据类型
□ 类型转换
□ 装箱和拆箱
3.1 数据类型概述
本节视频教学录像:3分钟
数据类型就是指数据的种类。在应用程序中,要使数据能被计算机识别并处理,需要将数据分为不同的类型,这样做的好处是方便存储和计算。比如对姓名和地址的处理需要使用字符型,在对货币和数量的处理中又需要使用数值类型,这些数据都是不同类型的数据。如姓名“张三”为字符型,年龄“25”为整型等。
提示
为什么要定义数据类型?
因为计算机是没有思维的,你只有告诉它,它才知道这是什么。
比如你定义“int a”,计算机才知道a是一个整数,否则就因识别不出来它是什么而出错。
C#的数据类型分为值类型、引用类型和指针类型3大类。值类型包括简单类型、结构类型和枚举类型等。引用类型包括类类型、接口类型、委托类型和数组类型等。指针类型只能用于安全模式。下图展示了各种数据类型及其之间的关系。
值类型的数据存储在内存的堆栈中,可以提供快速访问。如果变量是值类型的,这个变量就包含实际数据,在一个独立的内存区域保存自己的值;如果在代码中修改其值,在内存中会保存修改后的值。C#中的大多数基本数据类型,如整型、字符型、浮点型、布尔型等都是值类型,结构、枚举也属于值类型。
引用类型是指向存储在内存堆中的数据的指针或引用。与纯粹的地址不同,引用总是指向一个对象,而且这个对象具有指定的类型,并且在堆上分配了存储空间。字符串、数组、接口、类等都属于引用类型。引用类型很抽象,就像是一个门牌号码,可以根据门牌号码找到办公室所在位置。值类型和引用类型的基本区别是内存中的存储方式。
所有的值类型均隐式地派生自System.ValueType,并且值类型不能派生出新的类。引用类型的变量又称为对象,可存储对实际数据的引用。常见的引用类型有class、interface、delegate、object和string等。多个引用变量可以附加于一个对象,而且某些引用可以不附加于任何对象,如果声明了一个引用类型的变量,却不将它赋给任何对象,那么它的默认值就是null。相比之下,值类型的值不能是null。
3.2 值类型
本节视频教学录像:23分钟
C#语言的值类型包括整数类型、浮点数类型、布尔类型、字符类型等简单类型及枚举类型和结构类型。本小节介绍简单类型,枚举类型和结构类型将在后面章节中介绍。
1. 整数类型
整数类型的变量值为整数。计算机语言提供的整数类型的值总是在一定的范围之内。根据数据在计算机内存中所占的位数来划分,C#有8种整数类型的数据,这些数据及其在计算机中表示的整数的范围如下表所示。
例如,定义整型变量如下。
01 int s; //定义int类型的变量s 02 s=50; //将50的值赋给s
上述两行代码也可合并为:
int s=50; //定义int类型的变量s,将50的值赋给s
如果是同类型的多个变量,也可将其声明语句简写入同一行代码中,例如,
int a=10,b=20,c; //c还未被赋值
对于一个整数没有进行任何明确的声明,则该变量默认为int类型,一般情况下,应根据程序的需要选择合适的数据类型。为了把键入的值指定为其他的数据类型,可以在数字后面加上如下字符。
01 uint ui=20 U; //uint类型可以在数字后加上字符U 02 long l=20 L; //long类型可以在数字后加上字符L 03 ulong ul=20 UL; //ulong类型可以在数字后加上字符UL
提示
这些大写字符也可以用相应的小写字符代替,但字符“l”易与整数“1”混淆,所以推荐使用大写字符。
【范例3-1】 创建一个控制台程序,声明一个int类型的变量x并初始化为1000,一个byte类型的变量y并初始化为245,最后输出。
⑴ 在Visual Studio 2013中新建C#控制台程序,项目名为“Text”。
⑵ 在Program.cs的Main方法中输入以下代码(代码3-1.txt)。
01 int x=1000; //声明一个int类型的变量x并初始化值 02 byte y=245; //声明一个int类型的变量y并初始化值 03 Console.WriteLine("x={0}",x); //在控制台输出x结果 04 Console.WriteLine("y={0}",y); //在控制台输出y结果 05 Console.ReadLine();
【运行结果】
单击工具栏中的按钮,即可在控制台中输出如下图所示的结果。
此时,如果将byte类型的变量y赋值为260,重新编译程序,则会出现错误。主要原因是byte类型的变量是8位无符号整数,它的范围是0~255,260超过了byte类型的范围,因此编译程序时会出现错误。
2. 浮点数类型
浮点数类型又称为实数类型,是指带有小数部分的数值。C#支持两种浮点数类型:单精度(float)和双精度(double)。它们的差别在于取值范围和精度不同。浮点数类型数据的特征如下表所示。
若对于浮点数没有进行任何明确的声明,则该变量默认为double类型。如果想强制将其指定为float类型,则在其后面加上字符“F”或者“f”。
float f=10.5 F; //float类型在数字后加上字符F
如果想将数值强制指定为double类型,则在其后面加上字符“D”或者“d”。
double d=112 D; //double类型在数字后加上字符D
技巧
浮点数有一定的取值范围和有效数字限制,超出规定范围的数据是无法表示的。float类型精度为7位有效数字,因此float的值经常会有些误差。例如,10减去9.90得到的结果不是0.10,而是一个接近0.099999999的值。
3. 字符类型
除了数字外,计算机处理的信息主要是字符。C#字符类型采用Unicode字符集,一个Unicode标准字符长度为16位,允许用单个编码方案表示世界上所有的字符。计算机中对字符型数据的存储并不是把该字符本身放到内存单元中去,而是将该字符相应的Unicode代码放到存储单元中,即一个字符占两个字节的存储单元,存储单元存放的是该字符相应的Unicode码值。
在C#中,字符常量是用单引号(即撇号)括起来的一个字符,如'a'、'x'、'D'、'?'、'$'等都是字符常量。将字符放在双引号中,编译器会把它看做字符串,从而产生错误。注意,'a'和'A'是不同的字符常量。如下所示。
char c1='X'; //将字符X赋给字符型变量c1
除了以上形式的字符常量外,C#还允许使用一种特殊形式的字符常量,即以“\”开头的字符序列。它们一般用来实现一定的控制功能,并没有一定的字型,这种非显示字符难以用一般形式的字符表示,故规定用这种特殊的形式来表示,这种形式的字符也称为“转义字符”。在C#中,转义字符及其含义如下表所示。
转义字符的使用如下所示。
char c2="\"; //c2表示单引号
char c3="\0"; //c3表示空字符
C#中用string类型表示字符串,所以不需要使用char数组表示字符串。
4. decimal类型
为了适应高精度的财务和货币计算的需要,C#提供了十进制decimal类型。decimal类型数据的特征如下表所示。
要把数字指定为decimal类型,而不是double、float或者整型,可以在数字后面加上字符“M”或者“m”,例如,
decimal d=2.718 M; //定义decimal类型的变量d,将2.718的值赋给d
提示
如果计算的结果对精度要求非常高,如财务金融计算,就应该使用decimal类型,而不是浮点数类型。这是因为decimal类型有比浮点数类型数据较高的精度和较小的值域,decimal类型不受舍入错误的影响。
【范例3-2】 已知圆的半径为12cm,编程计算圆的面积。
⑴ 在Visual Studio 2013中新建C#控制台程序,项目名为“Circle”。
⑵ 在Program.cs的Main方法中输入以下代码(代码3-2.txt)。
01 decimal pi=3.14159M; //字母M表示数据是decimal类型 02 int r=12; //定义int型变量r表示圆的半径 03 decimal s=0; //用来存放圆的面积 04 s=pi*r*r; //计算圆的面积 05 Console.WriteLine("圆的半径是{0},\n圆的面积是:{1}",r,s); //在控制台输出结果 06 Console.ReadKey(); //暂停运行,按任意键继续 07 Console.ReadLine();
【运行结果】
单击工具栏中的按钮,即可在控制台中输出如下图所示的结果。
【范例分析】
在这个实例中定义了decimal类型的变量pi和s来代表圆周率和圆的面积,int型的变量r表示圆的半径。第1行代码中数据的后面加M表示数据是Decimal类型;第4行是计算圆的面积;第5行是在控制台输出结果,“\n”是转义字符,起换行作用,{0}和{1}中的0和1是占位符号,分别将r和s的结果显示在{0}和{1}所在的位置;第6行是暂停程序的运行,以便能看清运行的结果。
【拓展训练】
把【范例3-2】的输出结果以消息框的形式显示(拓展代码3-2.txt)。
⑴ 在【范例3-2】中添加对“System.Windows.Forms”的引用。在【解决方案资源管理器】中的【引用】上单击鼠标右键,单击弹出的【添加引用】项,在出现的【添加引用】窗口中选择【.NET】选项卡,找到“System.Windows.Forms”,然后单击【确定】按钮即可。
⑵ 在Program.cs中添加如下的导入命名空间语句。
using System.Windows.Forms; //使用创建基于Windows的应用程序的类
⑶ 将【范例3-2】步骤⑵中的第5行语句改为使用MessageBox.Show输出运行结果。
MessageBox.Show("圆的半径是:"+r+"\n圆的面积是:"+s); //以消息框输出圆的面积
【运行结果】
运行结果如下图所示。
MessageBox.Show能以消息框的形式显示结果,要求以字符串形式表示输出的结果。“\n”是转义字符,起换行作用。
5. 布尔类型
布尔类型(bool)是一种用来表示“真”或“假”的逻辑数据类型,布尔类型占用一个字节的内存。在C#中,布尔类型变量只有两种取值:true(代表“真”)和false(代表“假”)。并且true值不能被其他任何非0值所代替。如下所示。
01 bool flag=true; //正确 02 bool flag=1; //错误,不能将一个整型数据赋给布尔类型的变量
3.3 引用类型
本节视频教学录像:2分钟
C#中的值类型比较简单,但对更加复杂的数据处理效率很低。C#的引用类型主要用来描述结构复杂、抽象能力比较强的数据,它与值类型数据是相并列的。同为引用类型的两个变量,可以指向同一个对象,也可以针对同一个变量产生作用,或者被其他同为引用类型的变量所影响。字符串、类、接口、委托和数组等均属于引用类型。下面介绍字符串类型。
字符串是一种引用类型,是由放在一对双引号中的多个字符组成的一个串,可以将其看做一个由字符组成的数组。通常使用string来声明字符串变量,例如,
string name="Tom" //定义一个值为Tom的字符串变量name
关于字符串的详细操作,将在8.1节详细介绍。
3.4 数据类型之间的转换
本节视频教学录像:20分钟
在输出结果时经常要把整型、浮点型等类型转换为字符串。不同类型的数据需要转换为同一类型才能正常计算,所有的操作过程中经常会涉及数据类型之间的转换。C#中数据类型的转换可以分为两类:隐式转换和显式转换。
3.4.1 隐式转换
隐式转换就是系统默认的、不需要加以声明就可以进行的转换。在该过程中,编译器不需要对转换进行详细检查,就能安全地进行转换。例如,
01 short st=250; 02 int i=st; //将短整型隐式转换成整型
在C#引入var类型的变量之前,隐式转换仅存在于数值类型的数据之间。引入var类型之后,用var定义的变量可以实现隐式数据转换。
1. 数值类型数据间的隐式转换
隐式数据类型转换适用于数值类型的数据之间,如整型数据(int)可以隐式转换为浮点型(float)和双精度型(double)数据,浮点型(float)可以隐式转换为双精度型(double)数据。隐式数据类型转换只有遵循如下表所示的规则才能实现。
从int、uint、long或ulong到float,以及从long或ulong到double的转换可能导致精度损失,但不会影响数量级。其他的隐式转换不会丢失任何信息。
2. var类型数据隐式转换
用var定义的变量的数据类型是由赋值的数据决定的。如var Name="Johnson",此时变量Name就是字符串类型进行了隐式转换。如下所示。
01 var intNum=250; 02 int i=intNum; //var型变量intNum隐式转换成整型 03 var Name="Johnson"; 04 string strName=Name; //var型变量Name隐式转换成string型
3.4.2 显式转换
显式转换又叫做强制类型转换,需要用户明确地指定转换的类型。通过显式数据转换,可以把取值范围大的数据转换为取值范围小的数据。显式转换可以发生在表达式的计算过程中,但可能引起信息的丢失。例如,下面的代码把float类型的变量pi强制转换为int,小数部分的信息就丢失了。
01 float pi=3.14f; //定义一个单精度的实数
02 int i=(int)pi; //将单精度强制转换为整型来计算,i的值是3,不是3.14,造成信息丢失
3.4.3 使用Convert类转换
.Net Framework提供了很多类库,其中System.Convert类就是专门进行类型转换的类,通过Convert类提供的方法可以实现各种基本数据类型间的转换。Convert类的常用方法如下表所示。
例如,
01 String MyString="true"; 02 Bool MyBool=Convert.ToBoolean(MyString); //将String转换为Boolean型,MyBool=true 03 String newString="123456789"; 04 Int MyInt=Convert.ToInt32(newString); //将字符串转换为数字值,MyInt=123456789
【范例3-3】 隐式转换、显式转换使用举例。
⑴ 在Visual Studio 2013中新建C#控制台程序,项目名为“TypeConvert”。
⑵ 在Program.cs的Main方法中输入以下代码(代码3-3.txt)。
01 Console.WriteLine("隐式、显式转换例子:"); 02 short r=25; //表示圆的半径 03 int i=r; //将短整型r隐式转换成整型 04 float pi=3.14f; //定义一个单精度的实数 05 double s1=pi*i*i; //s1为double型,表示圆的面积 06 int s2; 07 s2=(int)pi*i*i; //s2为int型,表示圆的面积 08 var Name="Johnson"; 09 string strName=Name; //var型变量Name隐式转换为string型 10 Console.WriteLine("r=25,圆的面积={0},{1}",s1,s2);
11 Console.WriteLine("转换成功!"); 12 Console.ReadLine();
【代码详解】
第3行表示将short类型隐式转换为int;第5行表示将int和float类型隐式转换为double类型;第7行表示显式把float转换为int;第8~9行表示var类型隐式转换为string类型。
【运行结果】
单击工具栏中的按钮,即可在控制台中输出如下图所示的结果。
技巧
第7行代码如果写成s2=pi*i*i,隐式转换将失败,因为float不能隐式转换为int类型。
【范例分析】
这个范例主要演示了类型的转换。从运行结果看,强制转换存在信息丢失的现象,如第7行将pi强制转换为int,原来是3.14,转换后变成了3。隐式类型转换要遵循前面给出的规则,否则转换就会失败。
【拓展训练】
扩展【范例3-3】,使用Convert类的方法来实现类型的转换。代码如下(拓展代码3-3.txt)。
01 Console.WriteLine("隐式、显式转换例子:"); 02 short r=25; //r表示圆的半径 03 int i=Convert.ToInt32(r); //将短整型r隐式转换成整型 04 float pi=3.14f; //定义一个单精度的实数 05 double s1=pi*i*i; //s1为double型,表示圆的面积 06 int s2; 07 s2= Convert.ToInt32(pi)*i*i; //s2为int型,表示圆的面积 08 var memberName="Johnson"; 09 string strName= Convert.ToString(memberName); //var型变量隐式转换为string型 10 Console.WriteLine("r=25,圆的面积={0},{1}",s1,s2); 11 Console.WriteLine("memberName={0}",memberName); 12 Console.WriteLine("转换成功!"); 13 Console.ReadLine();
【运行结果】
运行结果如下图所示。
3.4.4 数值和字符串之间的转换
在C#中字符串和数值经常需要互相转换,下面介绍二者之间的转换方法。
ToString()方法:数值类型的ToString()方法可以将数值型数据转换为字符串。
Parse()方法:数值类型的Parse()方法可以将字符串转换为数值型,例如,字符串转换为整型使用int.Parse(string),字符串转换为双精度浮点型使用double.Parse(string)等。
例如,
01 int num1=25; 02 string str1=num1.ToString(); //num1的ToString()方法将num1转换为string赋给str1 03 string str2="38"; 04 int num2=int.Parse(str2); //int.Parse()方法将字符串str2转换为int类型 05 string str3="21"; 06 double num3=double.Parse(str3); //double.Parse()将字符串转换为双精度浮点型 07 string str4="56"; 08 float num4=float.Parse(str4); //float.Parse()将字符串转换为单精度浮点型
3.4.5 装箱和拆箱
拆箱是把“引用”类型转换成“值”类型,装箱是把“值”类型转换成“引用”类型,这是数据类型转换的一种特殊应用。有时某些方法的参数要求使用“引用”类型,而想把“值”类型的变量通过这个参数传入,就需要使用这个操作。例如,
01 int n=4; //n是值类型 02 object obj=n; //封箱,把任何值类型隐式地转换为object类型,其中object为引用类型 03 Console.WriteLine("n的初始值为:{0},装箱后的值为{1}",n,obj.ToString()); 04 int m=(int)obj; //拆箱,把一个object类型隐式地转换为值类型 05 Console.WriteLine("引用类型的值为:{0},拆箱后的值为{1}",obj.ToString(),m)
3.5 高手点拨
本节视频教学录像:4分钟
引用类型和值类型变量的使用是C#中的高级技巧之一。值类型变量中保存的是自己的实际数据,在赋值的时候会把源变量的数据复制一份,然后赋给目的变量;引用类型变量中保存的是“指向实际数据的指针”,即实际对象数据的内存地址,在进行赋值操作的时候,它和值类型一样,也是先有一个复制的操作,不过它复制的不是实际的数据,而是引用(真实数据的内存地址)。
1. 怎么区分值类型与引用类型
以struct关键字定义的数据类型就是值类型,另外,枚举类型也是值类型;以class关键字定义的数据类型就是引用类型。
2. 值类型变量与引用类型变量的使用区别
值类型变量不需要使用new关键字来分配内存。相信大家使用int型变量时,就没有使用new来为其分配内存,定义完之后,就可以直接使用;引用类型变量则需要为其赋值后,才能使用。下面举例说明值类型与引用类型在使用上的区别。
首先定义两种类型的员工:结构体SEmployee和类CEmployee。
01 struct SEmployee 02 { 03 public int Age; 04 } 05 class CEmployee 06 { 07 public int Age; 08 }
在main函数中,按照如下编写:
01 { 02 SEmployee se; 03 se.Age=4; 04 Console.WriteLine("年龄:{0}",se.Age); 05 Console.ReadKey(); 06 }
程序编译没有任何问题,运行后输出【年龄:4】。现在按照如下的代码编写main函数:
01 { 02 CEmployee ce; 03 ce=new CEmployee(); 04 ce.Age=5; 05 Console.WriteLine("年龄:{0}",ce.Age); 06 Console.ReadKey(); 07 }
这时程序就不能通过编译,提示使用了未赋值的变量“ce”,把注释掉的那一行恢复就可以顺利通过编译,运行后输出【年龄:5】。
通过这个小例子可以看出,值类型变量在定义后,就直接可以使用,而引用类型必须使用new关键字后才能使用。对于引用类型会有“未将对象引用设置到对象的实例”错误,而值类型不会发生这种错误。
3.值类型与引用类型的内存分配区别
内存分配时间:值类型变量在定义后就完成内存分配,引用类型必须显式用new关键字来分配内存。
内存分配区域:值类型的实例一般在线程栈上分配(也可以作为字段嵌入引用类型的对象中),在代表值类型的变量中,并不包含一个指向实例的指针,变量已经包含了实例本身的字段;引用类型总是从托管堆分配的,C#的new操作符会返回对象的内存地址,也就是指向对象数据的内存地址。
4.值类型与引用类型的优缺点
值类型变量在定义后,不论使用与否都会进行内存分配,因此当定义值类型而又没有使用的情况下,这些无用的变量就白白占用了内存。
值类型是按值传递的,也就是当用一个变量给另外一个变量赋值时,会把源变量的值赋值一遍,传给目的变量,当值类型(结构体)定义的比较大时,值类型的传递效率就非常低。
每个值类型的变量都有自己的内存空间,很难做到几个值类型的变量共享一个内存空间。
引用类型变量在定义后,不会自动分配内存,因此我们可以在用到引用类型变量的时候,用new来分配空间,不会造成内存的浪费;但是,引用类型是在托管堆上分配的,也就是其内存回收通过垃圾回收器来回收,引用类型的使用会增加应用程序在其生存期内需要进行的垃圾回收次数。
引用类型是按引用传递的,也就是内存地址在引用类型变量之间传递,这种传递方式效率非常高,当对象非常大时,可以优先考虑引用类型。
每个引用变量存储的是内存地址,可以实现多个变量共享一个内存空间。
当数据传递后,要求对目的变量的修改不能影响源变量的值,可以考虑将数据类型设计为值类型;当数据可以传递后,要求其后的操作必须影响到源数据,可以考虑将数据类型设计为引用类型。值类型与引用类型各有优缺点,使用时应根据实际的需要,选择合适的类型。
3.6 实战练习
操作题
创建一个控制台应用程序,声明一个整型变量x并赋值为100,然后将其复制到装箱对象obj中。最后进行拆箱操作,将装箱对象obj赋值给整型变量y。