2.1 .NET框架与C#的数据类型
常量、变量、表达式和方法(C和C++语言中称为函数)等都是C#程序中的基本语法成分。每个常量、变量、表达式求值的结果以及方法的返回值等都按各自的特点归属于不同的数据类型,并按不同的数据类型采用不同的方式存储并执行不同种类的运算。C#提供了一组标准的内置数值类型,用来表示整数、浮点值、布尔表达式、文本字符、十进制值和其他数据类型。
值得注意的是,C#语言所使用的基本数据类型并非内置于C#语言中,而是内置于.NET框架中。例如,在C#中定义一个int类型的变量时,实际上定义的是名为“Int32”的结构的一个实例,而Int32是.NET框架的System命名空间中预先定义好的一个结构。
2.1.1 .NET框架的数据类型
.NET框架提供了可加快和优化开发过程并访问系统功能的类、接口和值类型。为了便于不同语言(如C#、VB等)编写的程序之间进行交互操作,大多数.NET框架类型都符合CLS(公共语言规范),只要某种语言的编译器符合CLS,就可以在这种语言的程序中使用这些类型。
.NET框架中所有数据类型(见图2-1)可以划分为两个基本类别:值类型和引用类型。其中基元类型(编译器直接支持的数据类型,如int32)、枚举类型和结构类型(两种用户自定义类型)为值类型,而类、字符串、标准模块、接口、数组和委托为引用类型。
注意
根类型System.Object例外。它既非引用类型亦非值类型,而且不能实例化。因此,Object类型的变量既可赋值为值类型也可为引用类型。
图2-1 .NET中的数据类型
1. 值类型
如果数据类型在自身所占用的内存空间中直接存储数据,则该数据类型就是“值类型”。值类型包括以下几种。
• 所有数字(Byte、Int32、Double等)型。
• 布尔(Boolean)型、字符(Char)型和日期(Date)型。
• 结构(内含多个数据成员和方法成员的用户自定义类型),即使结构类型中的某些成员为引用类型,结构本身仍为值类型。
• 枚举型(可在多个表示整数的名称中任取一个的用户自定义类型),枚举类型总是基于SByte、Short、Int16、Inte32、Long、Byte、UShort、UInteger或Ulong等各种值类型定义的,故枚举型数据本身也是值类型。
值类型实例通常分配在堆栈(只能在一端存入和取出的一组内存单元)上,变量本身就包含它的实例数据。例如,一个整型变量就是一个值类型的实例,在它所占用的一组内存单元中,存储的就是它自身的值。
注意
值类型的实例也可能内联在结构中。
2. 引用类型
引用类型存储的是指向实际存储数据的另一个内存空间的指针(一组内存单元的首地址)。引用类型可以是自描述类型、指针类型或接口类型。而自描述类型进一步细分为数组和类类型。引用类型包括以下几种。
• 字符串(String)型。
• 数组(多个同类型数据的序列),即便是某种值类型数据所构成的数组,其本身仍为引用类型。
• 类类型(如定义窗体的可视化类Form)
• 委托。
引用类型实例分配在托管堆上,变量保存了实例数据的内存引用。换句话说,引用类型存储的是其值所在的位于堆上的内存地址的引用。
托管堆是.NET框架的CLR(公共语言运行时)中自动内存管理的基础。初始化一个新进程(正在执行的程序)时,CLR会为该进程保留一个连续的地址空间,称为托管堆。托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。最初,该指针设置为指向托管堆的基址。
2.1.2 System命名空间及其基类型
.NET框架的数据类型使用点语法命名方案,将相关数据类型划分为不同的命名空间组,以便搜索和引用这些数据类型。全名的第一部分(最右边的点之前的内容)是命名空间名。全名的最后一部分是类型名。例如,
System.Collections.ArrayList
表示ArrayList类型,该类型属于System.Collections命名空间。其中的数据类型可用于操作对象集合。
注意
这种命名方式使扩展.NET框架的库开发人员可以轻松地创建具有层次结构的数据类型组,以统一且带有提示性的方式对其命名,而且可以使用全名(命名空间和类型名称)来明确地标识数据类型,从而防止类型名称发生冲突。库开发人员在创建其命名空间的名称时可使用约定“公司名称。技术名称”,例如,Microsoft.Word命名空间就是一个例子。
通过这样的命名模式将相关类型分组为命名空间是构建和记录类库的一种实用的方式。一个命名空间可以划分到多个程序组中,单个程序组也可以包含来自多个命名空间的类型。程序组为CLR(公共语言运行时)中的版本控制、部署、安全性、加载和可见性提供形式上的结构。
System命名空间是.NET框架中基本类型的根命名空间。其中包括那些描述了可用于所有应用程序的基本数据类型的类:Object(层次型类继承结构的根类)、Byte、Char、Array、Int32、String等。这些类型中的许多类型与编程语言所使用的基元数据类型相对应。因而,编写代码时,如果需要使用某种数据类型,则既可使用.NET框架中的指定类型,也可使用某种程序设计语言中的相应关键字。
.NET框架提供的基本数据类型如表2-1所示。
表2-1 .NET框架提供的基本类型
除基本数据类型外,System命名空间还包含100多个类,如处理异常的类、处理核心运行时概念的类(应用程序域、垃圾回收器等)。System命名空间中还包含多个二级或更低级的命名空间,如System.Data、System.Windows.Forms等。
2.1.3 C#的数据类型
C#数据是.NET数据在C#语言上的一个实现。因此,C#同样有两种数据类型:值类型和引用类型。值类型的变量直接包含它们的数据,而引用类型的变量存储对它们的数据的引用。后者称为对象。
每个值类型的变量都有它自己的数据副本(ref和out参数变量除外),故一个变量的操作不会影响另一个变量。而引用类型的两个变量有可能引用同一个对象,故一个变量的操作可能会影响另一个变量所引用的对象,这是需要特别注意的。
1. C#的数据类型
C#的值类型可以细分为简单类型、枚举类型和结构类型,引用类型可细分为类类型、接口类型、数组类型和委托类型,如表2-2所列举。
表2-2 C#的数据类型
2. C#内置类型与.NET类型的联系
C#中内置的值类型(除object与string之外,均为简单类型)实际上是System命名空间中相应预定义类型的别名,如表2-3所列。
表2-3 C#内置类型与.NET类型对照
C#类型的关键字及其别名可以互换。例如,下列两个语句是等效的,都可以定义其值为123的整数变量。
int x = 123; System.Int32 x = 123;
可以通过系统方法GetType来查询某个C#类型的实际类型(运行时类型)。例如,下面的语句显示x变量的实际类型为System.Int32。
Console.WriteLine(x.GetType());
2.2 内置类型及其常量和变量
C#提供了一组标准的内置数值类型,如整型、浮点型、布尔型、字符型和字符串型等,用来表示不同种类的常量、变量以及函数(如数学函数sin(x))和表达式(如算术表达式2*x+1)求值的结果。
变量和常量是程序加工的基本数据对象。常量是具体的数据,在程序执行过程中值不会变。而变量是表示数据的符号,一个变量对应计算机中的一组存储单元,可在程序执行过程中按需要重新赋值。常量的用法比较简单,通过本身的书写格式即可判断其类型;但变量在使用之前必须先说明其类型,否则程序无法为它分配存储。这条原则不仅仅适用于变量,也适用于程序中的其他成分。例如,在.NET框架的System命名空间中,提供了一个自然对数函数求值的方法(C或C++中称为函数),这个方法的定义是
public static double Log(double d)
其中规定了方法中的自变量d和返回值(求值结果)都属于双精度型。
2.2.1 数值型常量
常量分为两种:字面常量和符号常量。
1. 字面常量
字面常量就是实际的常量,如下所述。
• 布尔常量:有false(假)和true(真)两种。
• 字符常量:指的是由单引号括起来的单个字符,如'a'、'B'、'9'。
• 字符串常量:如"y="、"C#"、"ZhangJinQi"。
• 整型常量:如10、9、100。
• 浮点型常量:如0.9m、98.5f、100.0。
2. 符号常量
符号常量使用const修饰符进行定义。只有C#的内置类型(System.Object除外)可以定义为符号常量,而用户自定义的数据类型(类、结构和数组)不能出现在const定义中。
注意
用readonly修饰符创建在运行时初始化一次就不可再更改的类、结构或数组。
常量必须在定义时初始化。例如,语句
public const int months=10;
定义了months为公有的符号常量,其值始终为12,不可更改。
实际上,当编译器遇到C#源代码中的常量修饰符时,直接把文本值替换到它生成的中间语言(IL)代码中。由于运行时并无与常量关联的像变量那样的地址,因此const字段不能通过引用传递且不能在表达式中作为左值出现。
可以同时定义多个相同类型的常量,例如,语句
const int months = 12, weeks = 52, days = 365;
同时定义了3个常量。
3. 常量的数据类型
书写数值型常数时,C#一般将小数解释为浮点型而非decimal型。书写一个十进制数值常数时,C#默认地按以下方法判断一个数值常数属于哪种C#数值类。
• 如果一个数值常数不带小数点(如56789),则判其为整型。
• 对于整型数值常数,按int→uint→long→ulong的顺序确定其类型。
• 对于带小数点的数值常数(如3.14),确定为double类型。
如果不希望C#使用上述默认的方式来确定一个十进制数值常数的类型,可以通过给数值常数加后缀的方式来指定数值常数的类型。数值常数后缀有以下几种。
• u(或大写的U)后缀:加在整型常数后面,说明它是uint类型或者ulong类型。由实际值确定到底是两种中的哪一种(优先匹配uint类型)。
• l(或大写的L)后缀:加在整型常数后面,说明是long类型或者ulong类型。由实际值确定到底是两种中的哪一种(优先匹配long类型)。
• ul后缀:加在整型常数后面,说明它是ulong类型。
• f(或大写的F)后缀:加在任何一种数值常数后面,说明常数是float类型。
• d(或大写的D)后缀:加在任何一种数值常数后面,说明它是double类型。
• m(或大写的M)后缀:加在任何一种数值常数后面,说明它是decimal类型。
2.2.2 数值类型及其变量
C#中的简单类型可进一步细分为以下几种。
• 8种整型,分别是8位、16位、32位和64位整数值的有符号和无符号形式。
• 两种浮点型,分别使用32位单精度和64位双精度格式表示。
• 小数(decimal)型,是128位的数据类型,适用于财务计算和货币计算。
• 布尔(bool)型,取布尔值true或false。
• 字符(char)型,16位Unicode编码表示的字符。
注意
string类型是16位Unicode字符的序列,属于引用类型。
C#简单类型中的数值类型如表2-4所列。
表2-4 C#的数值类型
1. 变量的概念及命名规则
变量的值在程序运行过程中随时可以变化。在计算机中,一个变量实际上代表了内存中的一组存储单元,因此,变量的名字就相当于这个存储空间的名字。对于一个C#的值类型变量来说,这些存储单元中的内容就是这个变量的值。
C#中变量的命名需要遵守以下规则。
• 由字母、数字或下划线“_”组成。
• 以字母或下划线“_”开头(不能以数字开头)。
• 不能使用C#中的关键字,如int、string、bool、main、class等。
• 区分大小写,如变量a和变量A是两个变量。
作为一个比较好的学习者,就必须遵守一些变量命名规范。
给变量命名时,要尽力做到“见名知意”。当变量名中包含多个英文单词组时,可使用骆驼命名法:第一个单词首字母小写,其他单词首字母大写,如:myName、myAge等。
2. 变量的定义和赋值
定义变量的一般形式为
数据类型 变量名表;
例如,以下语句分别定义了无符号整型变量number、小数型变量price和money。
uint number; decimal price, money;
为变量赋值的一般形式为
变量名 = 表达式;
其中,符号“=”为赋值运算符,意为将右边表达式求值的结果赋值给左边的变量。
可以在定义变量的同时为其赋值(称为赋初值)。例如,下面两个语句都可以定义decimal型变量pay,并为其赋初值为9000。
decimal pay = 9000.00m; System.Decimal pay = 9000.00m;
这里需要注意以下两点。
• 前一语句使用了C#中的类型名decimal(相应.NET框架中类型名的别名),后一语句使用了.NET框架中的类型名System.Decimal。
• C#默认9000.00为Double型常数,为给decimal型变量赋值,必须将该数显示地标记为9000.00m。
3. 隐式类型
在定义局部变量(仅在一个类、一个循环语句等小范围内有效的变量)时,可使用var关键字赋予其“推断”类型而非显式类型。var指示编译器根据初次赋值的语句右侧的表达式来推断变量的类型。推断类型可以是内置类型、匿名类型、用户定义类型或.NET框架类库中定义的类型。
注意
从Visual C# 3.0开始,在方法范围中声明的变量可以具有隐式类型var。
隐式类型就好像已经定义了该类型一样,但其类型是由编译器确定的。例如,下面两个语句在功能上是等效的,都可用于定义i变量并为其赋值为10。
var i = 10; // 隐式声明 int i = 10; //显示声明
2.2.3 字符和字符串
C#的字符类型数据采用Unicode字符集,一个字符的长度为16位(二进制位),可用于表示世界上大部分语言文字符号以及其他常用符号。
1. 字符变量
字符类型变量可在全体Unicode字符集中取值。凡是在单引号中的一个字符(包括汉字),就是一个字符常数,例如,
'*'、'5'、'a'、'A'、'字'
字符类型的标识符是char(或System.Char),例如,语句
char c1='2',c2='A',c3 = '数';
定义了字符型变量c1、c2和c3,并分别赋值为'2'、'A'和'数'。语句
Console.WriteLine("{0}、{1}", c3.GetType(), c3.GetTypeCode());
的输出结果为
System.Char、Char
前者为变量c3的类型名,后者为其C#中的别名。
2. 转义符
对于控制字符,如“换行”、单引号、双引号、反斜杠符等,可以使用由一个反斜杠符和一个符号组成的转义字符来表示,例如
'\n'、'\r'、'\t'、'\''、'\\''
分别表示“换行”“回车”“横向跳格”“单引号”和“反斜杠”。
3. 字符串的概念
字符串是由零个或多个字符组成的有限序列。一般可记为
s= ''a1 a2 ••• an ''(n>=0)
它是程序设计语言中表示文本的数据类型,通常是整体作为操作对象的。例如,在字符串中查找某个子串、求取一个子串、在串的某个位置上插入一个子串以及删除一个子串等。两个字符串相等的充要条件是:长度相等,并且各个对应位置上的字符都相等。
无论创建什么类型的应用程序,都需要使用字符串。无论数据如何存储,终端用户总要与可读的文本打交道,因此,字符串是几乎所有程序设计语言都支持的最常用的数据类型。某些语言中,字符串属于基本数据类型,还有些语言中属于复合数据类型。多数高级语言都使用某种方式引用起来的字符串表示字符串数据类型的实例。这种元字符串既可以称为“字符串”,也可以称为“文本”。
注意
在C及较早的C++语言中,没有提供字符串类型,一般是用字符数组来存放字符串的,也可以用字符指针指向字符串。
4. C#中的字符串
一个字符串是双引号定界的字符序列(如"Hello C#!")。String类是专门用于对字符串进行操作的,例如:
string sName = "张金"; string sClass = "电气51班"; string stu=sName+","+sClass+"学生"; //字符串连接运算 char c=stu3[0]; //取出stu中的第一个字符,即“张”字。
5. C#中的字符串常数
C#支持以下两种形式的字符串常数。
(1)常规字符串常数:位于双引号间的一串字符就是一个常规字符串常数。例如
"this is a String"
除了普通的字符,一个字符串常数也能包含一个或多个转义符。
(2)逐字字符串常数:以@开头,后跟一对双引号,字符位于双引号中。例如
@"C#程序设计"
逐字字符串常数与常规字符串常数的区别在于:逐字字符串常数的双引号中的每个字符都代表它最原始的意义,逐字字符串常数中不包含转义符。也就是说,逐字字符串常数的双引号中的内容在操作时是不变的,并且可以跨越多行。但有一个例外,如果其中包含双引号("),就必须在一行中使用两个双引号(" ")。
2.2.4 数据类型转换
在编写实现数据的输入、输出或者赋值等多种操作的代码时,往往要将某个数据从一种数据类型转换为另一种数据类型。例如,在将一个整数赋值给浮点型变量时,需要先将整数转换为浮点数,然后再赋值给浮点型变量。又如,C#将用户键入的浮点数当作字符串对待,为了将这种浮点数模样的字符串赋值给浮点型变量,也需要先将其转换为浮点数,然后再赋值给浮点型变量。
.NET框架提供了多种功能来支持数据类型转换。下面介绍几种常用的转换方法。
注意
数据类型转换时,创建一个等同于旧类型值的新类型值,但不必保留原始对象的恒等值(或精确值)。
1. 数值类型之间的相互转换
比较short和int两种类型:虽然都是整型,但前者比后者短,所占用的存储空间自然小。再比较long和float两种类型:前者属于整型,其存储方式比属于浮点型的后者简单。假定我们将存储空间小或者存储方式简单称为“低”,反之称为“高”,那么,可将数值型数据按从低到高排序为:
Byte→short→int→long→float→double
(1)在从左到右(从短到长或从简单到复杂)进行数据类型转换时,可以直接进行转换(隐式转换),不必作任何说明。
例2-1 数据类型的隐式转换。
//例2-1_ 数据类型的隐式转换 using System; namespace 隐式转换 { class Program { static void Main(string[] args) { //隐式转换(字节多←字节少|复杂格式←简单格式) int a = 9; long al = a; // 长整型←整型 float af = al; //浮点型←整型 double ad = af; // 双精度型←单精度型 Console.WriteLine("a={0};al={1} ;af={2} ;ad={2}", a, al, af, ad); } } }
本程序的运行结果如图2-2所示。
图2-2 例2-1程序的运行结果
在执行语句
long al = a;
时,int型变量a的值自动行转换成long型,再赋给long型变量al。执行语句
float af = al;
时,long型变量al的值自动行转换成float型,再赋给float型变量af。
(2)如果想按相反的顺序赋值,则会出现错误提示信息。例如,键入以下两个语句:
long al = 9; int a = al;
之后,错误列表窗口中立即显示:
无法将类型"long"隐式转换为"int"……
(3)如果一定要进行从长到短或从简单到复杂的转换,就应该使用
(类型名) 变量名
的形式强制类型转换。例如,下面两个语句可以顺利执行
long al = 9; int a = (int)al;
在执行后一个语句时,C#先将long变量al的值强制转换成int型,再赋给int型变量a。
值得注意的是,在将字节数较多的类型强制转换为字节数较少的类型,或者将字节数相同的无符号数强制转换成有符号数时,都有可能会因被转换类型的值超出目标类型的取值范围而产生溢出错误。例如,在将byte型的129强制转换为sbyte型时,就会溢出。
2. 字符的ASCII码和Unicode码
有时候,可能有如下需要。
(1)得到一个英文字符的ASCII码,或者一个汉字字符的Unicode码。
(2)查询某个编码对应的是哪个字符。
对于这种编码和字符互相转换的操作,不同的语言有不同的处理方式,如下所述。
(1)在VB中,Asc()函数用于将一个字符转换成相应的ASCII码,Chr()函数用于将ASCII码转换成相应的字符。
(2)在C语言中,如果将英文字符型数据强制转换成合适的数值型数据,就可以得到相应的ASCII码;反之,如果将一个合适的数值型数据强制转换成字符型数据,就可以得到相应的字符。
C#中字符的范围扩大了,不仅可以使用单字节字符,也可以使用像中文字符这样的双字节字符,而在字符和编码之间的转换,则仍延用了C语言的做法——强制类型转换。
例2-2 字(英文字符、汉字)与编码(ASCII码、Unicode编码)的互相转换。
//例2-2_ 字符与编码的转换 using System; namespace 字符与编码 { class Program { static void Main(string[] args) { //英文字符<->ASCII 码|汉字<->Unicode码 char c1 = 'a'; short i1 = 65; char c2 = ' 好'; short i2 = 23456; Console.WriteLine("{0}的ASCII码:{1}", c1, (short)c1); Console.WriteLine("{0}的ASCII字符:{1}", i1, (char)i1); Console.WriteLine("“{0} ”字的Unicode码:{1}", c2, (short)c2); Console.WriteLine("Unicode码{0} 的汉字:{1}", i2, (char)i2); } } }
本程序的运行结果如图2-3所示。
图2-3 例2-2程序的运行结果
3. 数值字符串和数值之间的转换
在C#中,字符串是一对双引号定界的字符序列,如果这个序列中的字符都是数字,则为数值字符串。例如,"56789"就是一个数值字符串。在输入数值的时候,需要把这样的字符串转换成数值;而在另一些时候,可能需要相反的转换。
将数值转换成字符串非常简单,因为每个类都有一个ToString()方法。所有数值型的ToString()方法都能将数据转换为数值字符串。反之,将数值型字符串转换成数值时,可以使用short、int、float等数值类型的Parse()函数,该函数用来将字符串转换为相应数值。
例2-3 数值与数值型字符串的互相转换。
//例2-3_ 数值与字符串的转换 using System; namespace temp { class Program { static void Main(string[] args) { //数字←Parse( 字符串)|字符串←ToString(数字) string s1 = "12345"; int i = int.Parse(s1); Console.WriteLine("int型变量 i={0}", i); string s2 = "567.985"; double d = double.Parse(s2); Console.WriteLine("doubleint 型变量 d={0}", d); float f = 56.987F; string s3 = f.ToString(); Console.WriteLine("string型变量 s3={0}", s3); } } }
本程序的运行结果如图2-4所示。
图2-4 例2-3程序的运行结果
Convert是System命名空间中的一个专门用于数据类型转换的类,基本上可以转换所有常用的数据类型。例如,下面第1个语句将数值56.987转换为整数57并赋给int型变量a,第2个语句将数值56.987转换为数值字符串并赋给string型变量s。
int a = Convert.ToInt16(56.987); string s = Convert.ToString(56.987);
2.2.5 常用数学函数
C#通过System命名空间中的Math类提供了一系列实现常用数学函数的静态方法,如表2-5所列,调用这些数学函数的一般形式为
Math.函数名(参数表)
例如,下面的语句计算0.56的正弦函数值并将其赋给y变量。
double y = Math.Sin(0.56);
常用的数学方法见表2-5。
表2-5 实现数学函数的常用方法