前面介绍了如何声明变量和常量,下面要详细讨论C#中可用的数据类型。与其他语言相比,C#对其可用的类型及其定义进行了过分的修饰。
在开始介绍C#中的数据类型之前,理解C#把数据类型分为两种是非常重要的:
● 值类型
● 引用类型
下面几节将详细介绍值类型和引用类型的语法。从概念上看,其区别是值类型直接存储其值,而引用类型存储对值的引用。与其他语言相比,C#中的值类型基本上等价于VB或C++中的简单类型(整型、浮点型,但没有指针或引用)。引用类型与VB中的引用类型相同,与C++中通过指针访问的类型类似。
这两种类型存储在内存的不同地方:值类型存储在堆栈中,而引用类型存储在托管堆上。注意区分某个类型是值类型还是引用类型,因为这种存储位置的不同会有不同的影响。例如,int是值类型,这表示下面的语句会在内存的两个地方存储值20:
// i and j are both of type int
i = 20;
j = i;
但考虑下面的代码。这段代码假定已经定义了一个类Vector,Vector是一个引用类型,它有一个int类型的成员变量Value:
Vector x, y
x = new Vector ();
x.Value = 30; // Value is a field defined in Vector class
y = x;
Console.WriteLine(y.Value);
y.Value = 50;
Console.WriteLine(x.Value);
要理解的重要一点是在执行这段代码后,只有一个Vector对象。x和y都指向包含该对象的内存位置。因为x和y是引用类型的变量,声明这两个变量只是保留了一个引用——而不会实例化给定类型的对象。这与在C++中声明指针和VB中的对象引用是相同的——在C++和VB中,都不会创建对象。要创建对象,就必须使用new关键字,如上所示。因为x和y引用同一个对象,所以对x的修改会影响y,反之亦然。因此上面的代码会显示30和50。
注意:
C++开发人员应注意,这个语法类似于引用,而不是指针。我们使用.(句点)符号,而不是->来访问对象成员。在语法上,C#引用看起来更类似于C++引用变量。但是,抛开表面的语法,实际上它类似于C++指针。
如果变量是一个引用,就可以把其值设置为null,表示它不引用任何对象:
这类似于Java中把引用设置为null,C++中把指针设置为NULL,或VB中把对象引用设置为Nothing。如果将引用设置为null,显然就不可能对它调用任何非静态的成员函数或字段,这么做会在运行时抛出一个异常。
在像C++这样的语言中,开发人员可以选择是直接访问某个给定的值,还是通过指针来访问。VB的限制更多:COM对象是引用类型,简单类型总是值类型。C#在这方面类似于VB:变量是值还是引用仅取决于其数据类型,所以,int总是值类型。不能把int变量声明为引用(在第5章介绍装箱时,可以在类型为object的引用中封装值类型)。
在C#中,基本数据类型如bool和long都是值类型。如果声明一个bool变量,并给它赋予另一个bool变量的值,在内存中就会有两个bool值。如果以后修改第一个bool变量的值,第二个bool变量的值也不会改变。这些类型是通过值来复制的。
相反,大多数更复杂的C#数据类型,包括我们自己声明的类都是引用类型。它们分配在堆中,其生存期可以跨多个函数调用,可以通过一个或几个别名来访问。CLR执行一种精细的算法,来跟踪哪些引用变量仍是可以访问的,哪些引用变量已经不能访问了。CLR会定期进行清理,删除不能访问的对象,把它们占用的内存返回给操作系统。这是通过垃圾收集器实现的。
把基本类型(如int和bool)规定为值类型,而把包含许多字段的较大类型(通常在有类的情况下)规定为引用类型,C#设计这种方式的原因是可以得到最佳性能。如果要把自己的类型定义为值类型,就应把它声明为一个结构。
如第1章所述,C#认可的基本预定义类型并没有内置于语言中,而是内置于.NET Framework中。例如,在C#中声明一个int类型的数据时,声明的实际上是.NET结构System.Int32的一个实例。这听起来似乎很深奥,但其意义深远:这表示在语法上,可以把所有的基本数据类型看作是支持某些方法的类。例如,要把int i转换为string,可以编写下面的代码:
string s = i.ToString();
应强调的是,在这种便利语法的背后,类型实际上仍存储为基本类型。基本类型在概念上用.NET结构表示,所以肯定没有性能损失。
下面看看C#中定义的类型。我们将列出每个类型,以及它们的定义和对应.NET类型(CTS 类型)的名称。C#有15个预定义类型,其中13个是值类型,2个是引用类型(string和object)。
内置的值类型表示基本数据类型,例如整型和浮点类型、字符类型和bool类型。
C#支持8个预定义整数类型,如表2-1所示。
表 2-1
|
名 称 |
CTS 类 型 |
说 明 |
范 围 |
|
sbyte |
System.SByte |
8位有符号的整数 |
–128 到 127 (–27到27–1) |
|
short |
System.Int16 |
16位有符号的整数 |
–32 768 到 32 767 (–215到215–1) |
|
int |
System.Int32 |
32位有符号的整数 |
–2 147 483 648 到 2 147 483 647(–231到231–1) |
|
long |
System.Int64 |
64位有符号的整数 |
–9 223 372 036 854 775 808到9 223 372 036 854 775 807(–263到263–1) |
|
byte |
System.Byte |
8位无符号的整数 |
0到255(0到28–1) |
|
ushort |
System.Uint16 |
16位无符号的整数 |
0到65535(0到216–1) |
|
uint |
System.Uint32 |
32位无符号的整数 |
0到4 294 967 295(0到232–1) |
|
ulong |
System.Uint64 |
64位无符号的整数 |
0到18 446 744 073 709 551 615(0到264–1) |
Windows的将来版本将支持64位处理器,可以把更大的数据块移入移出内存,获得更快的处理速度。因此,C#支持8至64位的有符号和无符号的整数。
当然,VB开发人员会发现有许多类型名称是新的。C++和Java开发人员应注意:一些C#类型名称与C++和Java类型一致,但类型有不同的定义。例如,在C#中,int总是32位带符号的整数,而在C++中,int是带符号的整数,但其位数取决于平台(在Windows上是32位)。在C#中,所有的数据类型都以与平台无关的方式定义,以备将来C#和.NET迁移到其他平台上。
byte是0~255(包括255)的标准8位类型。注意,在强调类型的安全性时,C#认为byte类型和char类型完全不同,它们之间的编程转换必须显式写出。还要注意,与整数中的其他类型不同,byte类型在默认状态下是无符号的,其有符号的版本有一个特殊的名称sbyte。
在.NET中,short不再很短,现在它有16位,Int类型更长,有32位。 long类型最长,有64位。所有整数类型的变量都能赋予10进制或16进制的值,后者需要0x前缀:
long x = 0x12ab;
如果对一个整数是int、uint、long或是ulong没有任何显式的声明,则该变量默认为int类型。为了把键入的值指定为其他整数类型,可以在数字后面加上如下字符:
uint ui = 1234U;
long l = 1234L;
ulong ul = 1234UL;
也可以使用小写字母u和l,但后者会与整数1混淆。
C#提供了许多整型数据类型,也支持浮点类型,如表2-2所示。C和C++程序员很熟悉 它们。
表 2-2
|
名称 |
CTS类型 |
说 明 |
位 数 |
范围 (大致) |
|
float |
System.Single |
32位单精度浮点数 |
7 |
±1.5 × 10-45 到 ±3.4 × 1038 |
|
double |
System.Double |
64位双精度浮点数 |
15/16 |
±5.0 × 10-324到 ±1.7 × 10308 |
float数据类型用于较小的浮点数,因为它要求的精度较低。double数据类型比float数据类型大,提供的精度也大一倍(15位)。
如果在代码中没有对某个非整数值(如12.3)硬编码,则编译器一般假定该变量是double。如果想指定值为float,可以在其后加上字符F(或f):
float f = 12.3F;
另外,decimal类型表示精度更高的浮点数,如表2-3所示。
表 2-3
|
名 称 |
CTS类型 |
说 明 |
位 数 |
范围(大致) |
|
decimal |
System. |
128位高精度十进制数表示法 |
28 |
±1.0×10-28到±7.9 × 1028 |
CTS和C#一个重要的优点是提供了一种专用类型表示财务计算,这就是decimal类型,使用decimal类型提供的28位的方式取决于用户。换言之,可以用较大的精确度(带有美分)来表示较小的美元值,也可以在小数部分用更多的舍入来表示较大的美元值。但应注意,decimal类型不是基本类型,所以在计算时使用该类型会有性能损失。
要把数字指定为decimal类型,而不是double、 float或整型,可以在数字的后面加上字符M(或m),如下所示。
C#的 bool 类型用于包含bool值true或false,如表2-4所示。
表 2-4
|
名 称 |
CTS 类 型 |
值 |
|
bool |
System.Boolean |
true或false |
bool值和整数值不能相互转换。如果变量(或函数的返回类型)声明为bool类型,就只能使用值true或false。如果试图使用0表示false,非0值表示true,就会出错。
为了保存单个字符的值,C#支持char数据类型,如表2-5所示。
表 2-5
|
名 称 |
CTS 类 型 |
值 |
|
char |
System.Char |
表示一个16位的(Unicode)字符 |
虽然这个数据类型在表面上类似于C和C++中的char类型,但它们有重大区别。C++的char表示一个8位字符,而C#的char包含16位。其部分原因是不允许在char类型与8位byte类型之间进行隐式转换。
尽管8位足够编码英语中的每个字符和数字0~9了,但它们不够编码更大的符号系统中的每个字符(例如中文)。为了面向全世界,计算机行业正在从8位字符集转向16位的Unicode模式,ASCII编码是Unicode的一个子集。
char类型的字面量是用单引号括起来的,例如'A'。如果把字符放在双引号中,编译器会把它看作是字符串,从而产生错误。
除了把char表示为字符字面量之外,还可以用4位16进制的Unicode值(例如'\u0041'),带有数据类型转换的整数值(例如(char)65),或16进制数('\x0041')表示它们。它们还可以用转义序列表示,如表2-6所示。
表 2-6
|
转 义 序 列 |
字 符 |
|
\' |
单引号 |
|
\" |
双引号 |
|
\\ |
反斜杠 |
|
\0 |
空 |
|
\a |
警告 |
|
\b |
退格 |
|
\f |
换页 |
|
\n |
换行 |
|
\r |
回车 |
|
\t |
水平制表符 |
|
\v |
垂直制表符 |
C++开发人员应注意,因为C#本身有一个string类型,所以不需要把字符串表示为char类型的数组。
C#支持两个预定义的引用类型,如表2-7所示。
表 2-7
|
名 称 |
CTS 类 |
说 明 |
|
object |
System.Object |
根类型,CTS中的其他类型都是从它派生而来的(包括值类型) |
|
string |
System.String |
Unicode字符串 |
许多编程语言和类结构都提供了根类型,层次结构中的其他对象都从它派生而来。C#和.NET也不例外。在C#中,object类型就是最终的父类型,所有内在和用户定义的类型都从它派生而来。这是C#的一个重要特性,它把C#与VB和C++区分开来,但其行为与Java非常类似。所有的类型都隐含地最终派生于System.Object类,这样,object类型就可以用于两个目的:
● 可以使用object引用绑定任何子类型的对象。例如,第5章将说明如何使用object类型把堆栈中的一个值对象装箱,再移动到堆中。对象引用也可以用于反射,此时必须有代码来处理类型未知的对象。这类似于C++中的void指针或VB中的Variant数据类型。
● object类型执行许多基本的一般用途的方法,包括Equals()、GetHashCode()、GetType()和ToString()。用户定义的类需要使用一种面向对象技术—— 重写(见第4章),提供其中一些方法的替代执行代码。例如,重写ToString()时,要给类提供一个方法,提供类本身的字符串表示。如果类中没有提供这些方法的实现代码,编译器就会使用object类型中的实现代码,它们在类中的执行不一定正确。
后面的章节将详细讨论object类型。
有C和C++开发经验的人员可能在使用C风格的字符串时不太顺利。C或C++字符串不过是一个字符数组,因此客户机程序员必须做许多工作,才能把一个字符串复制到另一个字符串上,或者连接两个字符串。实际上,对于一般的C++程序员来说,执行包装了这些操作细节的字符串类是一个非常头痛的耗时过程。VB程序员的工作就比较简单,只需使用string类型即可。而Java程序员就更幸运了,其String类在许多方面都类似于C#字符串。
C#有string关键字,在翻译为.NET类时,它就是System.String。有了它,像字符串连接和字符串复制这样的操作就很简单了:
string str1 = "Hello ";
string str2 = "World";
string str3 = str1 + str2; // string concatenation
尽管这是一个值类型的赋值,但string是一个引用类型。String对象保留在堆上,而不是堆栈上。因此,当把一个字符串变量赋给另一个字符串时,会得到对内存中同一个字符串的两个引用。但是,string与引用类型在常见的操作上有一些区别。例如,修改其中一个字符串,就会创建一个全新的string对象,而另一个字符串没有改变。考虑下面的代码:
using System;
class StringExample
{
public static int Main()
{
string s1 = "a string";
string s2 = s1;
Console.WriteLine("s1 is " + s1);
Console.WriteLine("s2 is " + s2);
s1 = "another string";
Console.WriteLine("s1 is now " + s1);
Console.WriteLine("s2 is now " + s2);
return 0;
}
}
其输出结果为:
s1 is a string
s2 is a string
s1 is now another string
s2 is now a string
换言之,改变s1的值对s2没有影响,这与我们期待的引用类型正好相反。当用值"a string"初始化s1时,就在堆上分配了一个string对象。在初始化s2时,引用也指向这个对象,所以s2的值也是"a string"。但是现在要改变s1的值,而不是替换原来的值时,堆上就会为新值分配一个新对象。s2变量仍指向原来的对象,所以它的值没有改变。这实际上是运算符重载的结果,运算符重载详见第5章。基本上,string类实现为其语义遵循一般的、直观的字符串规则。
字符串字面量放在双引号中("...");如果试图把字符串放在单引号中,编译器就会把它当作char,从而引发错误。C#字符串和char一样,可以包含Unicode、16进制数转义序列。因为这些转义序列以一个反斜杠开头,所以不能在字符串中使用这个非转义的反斜杠字符。而需要用两个反斜杠字符(\\)来表示它:
string filepath = "C:\\ProCSharp\\First.cs";
即使用户相信自己可以在任何情况下都记住要这么做,但键入两个反斜杠字符会令人迷惑。幸好,C#提供了另一种替代方式。可以在字符串字面量的前面加上字符@,在这个字符后的所有字符都看作是其原来的含义——它们不会解释为转义字符:
string filepath = @"C:\ProCSharp\First.cs";
甚至允许在字符串字面量中包含换行符:
string jabberwocky = @"'Twas brillig and the slithy toves
Did gyre and gimble in the wabe.";
那么jabberwocky的值就是:
'Twas brillig and the slithy toves
Did gyre and gimble in the wabe.