代码改变世界

5.1 编程语言的基元类型

2011-12-23 10:23  iRead  阅读(313)  评论(0编辑  收藏  举报

  某些数据类型如此常用,以至于许多编译器允许代码以简化的语法来操作它们。例如,可以使用以下语法来分配一个整数:

  System.Int32 a = new System.Int32();

  但你肯定不愿意使用这种语法来声明并初始化一个整数,它实在是太繁琐了。幸运的是,包括C#在内的许多编译器都允许换用如下所示的语法:

  int a = 0;

  这种语法不仅增强了代码的可读性,而且生成的IL代码与使用System.Int32时生成的IL代码是完全一致的。编译器直接支持的数据类型成为基元类型(primitive type。基元类型直接映射到Framework类库(FCL)中存在的类型。比如在C#中,int直接映射到System.Int32类型。因此,以下4行代码都能正确编译,并生成完全相同的IL:

  int a = 0;                                                 //最方便的语法

  System.Int32 a = 0;                             //方便的语法

  int a = new int();                                   //不方便的语法

  System32.Int32 a = new System.Int32(); //最不方便的语法

  表5-1列出的FCL类型在C#中都有对应的基元类型。对于相容于公共语言规范(CLS)的类型,其他语言将提供类似的基元类型。然而,并不要求语言为非CLS相容的类型提供任何支持。

表5-1 C#基元类型与对应的FCL类型

C#基元类型 FCL类型 CLS相容 说明
sbyte System.Sbyte 有符号8位值
byte System.Byte 无符号8位值
short System.Int16 有符号16位值
ushort System.UInt16 无符号16位值
int System.Int32 有符号32位值
uint System.UInt32 无符号32位值
long System.Int64 有符号64位值
ulong System.UInt64 无符号64位值
char System.Char 16位Unicode字符(char不像在非托管C++中那样代表一个8位值)
float System.Single IEEE32位浮点值
double System.Double IEEE64位浮点值
bool                    System.Boolean 一个true/false值
decimal System.Decimal 一个128位高精度浮点值,常用于不容许舍入误差的金融计算。在128位中,有1位代表值的符号(正负号),有96位代表值本身,并有8位代表一个比例因子。比例因子用作96位整数的除数并指定整数的哪一部分为小数。比例因子隐式地定位数字10的幂,范围从0到28.其余位没有使用
string System.String 一个字符数组
object System.Object 所有类型的基类型
dynamic System.Object 对于CLR,dynamic和object完全一致。然而,C#编译器允许使用一个简单的语法,让dynamic变量参与动态调用。欲知详情,请参见本章最后的5.5节”dynamic基元类型”

  从另一个角度思考,可以想象C#编译器自动假定在所有源代码文件中添加了以下using指令(参考第4章):

  using sbyte = System.Sbyte;

  using byte = System.Byte;

  using short = System.Int16;

  using ushort = System.UInt16;

  using int = System.Int32;

  using uint = System.UInt32;

  …

  C#语言规范称:”从风格上说,最好使用关键字,而不是使用完整的系统类型名称。”但我并不同意语言规范的说法:我情愿使用FCL类型名称,并完全避免使用基元类型名称。事实上,我希望编译器根本不要提供基元类型名称,强制开发人员使用FCL类型名称。下面是我的理由:

  • 许多开发人员都困惑到底应该使用string还是String。由于C#的string(一个关键字)直接映射到System.String(一个FCL类型),所以两者没有区别,都可以使用。类似地,还有一些开发人员说应用程序在32位操作系统上运行时,int代表32位整数;在64位操作系统上运行时,int代表64位整数。这个说法是完全错误的。在C#中,int始终映射到System.Int32,所以不管在什么操作系统上运行,代表的都是32位整数。如果程序员习惯了在代码中使用Int32,像这样的误解也就不会产生了。
  • 在C#中,long映射到System.Int64,但在其他编程语言中,long可能映射到Int16或Int32。例如,C++/CLI就将long视为一个Int32。习惯于用一种语言写程序的人在看用另一种语言写的源代码时,很容易错误地理解代码的意图。事实上,大多数语言甚至不将long看作一个关键字,根本不会编译使用了它的代码。
  • FCL的许多方法都将类型名称作为方法名的一部分。例如,BinaryReader类型提供的方法包括ReadBoolean, ReadInt32, ReadSingle等;而System.Convert类型提供的方法包括ToBoolean,ToInt32,ToSingle等。以下代码虽然语法没有问题,但包含float的那一行显得颇不自然,无法一下子判断该行的正确性:

      BinaryReader br = new BinaryReader(…);

      Float val = br.ReadSingle();  //正确,但感觉不自然

      Single val = br.ReadSingle();  //正确,而且让人一目了然

  • 平时只用C#的许多程序员逐渐忘了还可以用其他语言写面向CLR的代码。因此,”C#主义”逐渐入侵类库代码。例如,Microsoft的FCL几乎是完全用C#写的,FCL团队的开发人员现在向库中引入像Array的GetLongLength这样的方法。该方法返回一个Int64值。这种值在C#中是long,但在其他语言中不是(C++/CLI)。另一个例子是System.Linq.Enumerable的LongCount方法。

  考虑到所有这些原因,本书将坚持使用FCL类型名称。

  在许多编程语言中,以下代码都能正确编译并运行:

  Int32 i = 5;                 //一个32位值

  Int64 l = i;                   //隐式转型为一个64位值

  然而,根据第4章对类型转换的讨论,你或许会认为上述代码无法编译通过。毕竟,System.Int32和System.Int64是不同的类型,相互不存在派生关系。但实际上,你会欣喜地发现C#编译器正确编译了上述代码,运行起来也没有任何问题。这是为什么呢?原因是C#编译器非常熟悉基元类型,并会在编译代码时应用它自己的特殊规则。换言之,编译器能识别常见的编程模式,并生成必要的IL,使写好的代码能够像预期的那样工作。具体地说,C#编译器支持与类型转换、文本常量以及操作符有关的模式。在后面的几个例子中,我们将对它们进行演示。

  首先,编译器能执行基元类型之间的隐式或显示转型,例如:

  Int32 i=5;                                               //从Int32隐式转型为Int32

  Int64 l = i;                                               //从Int32隐式转型为Int64

  Single s = i;                                            //从Int32隐式转型为Single

  Byte b =(Byte)i;                                    //从Int32显示转型为Byte

  Int16 v=(Int16) s;                                 //从Single显示转型为Int16

  只有在转换“安全”的时候,C#才允许隐式转型。所谓“安全”,是指不会发生数据丢失的情况,比如从Int32转换为Int64。然而,如果一次转换有可能是不安全的,C#就要求进行显式转型。对于数值类型,“不安全”意味着在转换之后,有可能丢失精度或者数量级。例如,从Int32转换为Byte要求显示转型,因为大的Int32数字可能丢失精度;从Single转换为Int16也要求显式转型,因为Single能表示比Int16更大数量级的数字。

  注意,不同的编译器可能生成不同的代码来处理这些转型操作。例如,将值为6.8的一个Single转型为一个Int32时,有的编译器可能生成代码,对其进行截断处理(向下取整),最终将6放到Int32中;其他编译器则可能将结果向上取整为7。顺便说一句,C#总是对结果进行截断处理,而不进行向上取整。要了解C#对基元类型进行转型时的具体规则,请参见C#语言规范的”Conversions”一节。

  除了转型,基本类型还能写成文本常量(literal)。文本常量可被看成是类型本身的一个实例,所以可以像下面为一个实例(123和456)调用实例方法:

  Console.WriteLine(123.ToString() + 456.ToString());        //”123456”

  此外,如果一个表达式由文本常量构成,编译器能在编译时完成表达式的求值,从而增强应用程序的性能:

  Boolean found = false;                        //生成的代码将found设为0

  Int32 x = 100+20+3;                            //生成的代码将x设为123

  String s = “a “+”bc”;                            //生成的代码将s设为”a bc”

  最后,编译器知道如何以及以什么顺序解析代码中出现的操作符(比如+,-,*,/,%,&,^,|,==,!=,>,<,>=,<=,<<,>>,~,!,++,--等):

  Int32 y = 100;                                        //赋值操作符

  Int32 y = x+23;                                               //加和赋值操作符

  Boolean lessThanFifty = (y<50);       //小于和赋值操作符