C# 8.0本质论
上QQ阅读APP看书,第一时间看更新

2.1 基本数值类型

C#的基本数值类型都有关键字与之关联,包括整数类型、浮点类型以及decimal类型。decimal是特殊的浮点类型,能存储大数字而无表示错误。

2.1.1 整数类型

C#中有八种整数类型,可选择最恰当的一种来存储数据以避免浪费资源。表2.1总结了每种整数类型。

表2.1 整数类型

表2.1(以及表2.2和表2.3)专门有一列给出了每种类型的完整名称,本章稍后会讲述后缀问题。C#所有基元类型都有短名称和完整名称。完整名称对应BCL(基类库)中的类型名称。该名称在所有语言中都相同,对程序集中的类型进行了唯一性标识。由于基元数据类型是其他类型的基础,所以C#为基元数据类型的完整名称提供了短名称(或称为缩写)。其实从编译器的角度看,两种名称完全一样,最终都生成相同的代码。事实上,检查最终生成的CIL代码,根本看不出源代码具体使用的名称。

C#支持完整BCL名称和关键字,导致开发者不确定在什么时候应该用什么。不要时而用这个,时而用那个,最好坚持用一种。C#开发者一般用C#关键字。例如,用int而不是System.Int32,用string而不是System.String(甚至不要用String这种简化形式)。

设计规范

·要在指定数据类型时使用C#关键字而不是BCL名称(例如,使用string而不是String)。

·要一致而不要变来变去。

坚持一致可能和其他设计规范冲突。例如,虽然规范说要用C#关键字取代BCL名称,但有时需维护公司遗留下来的风格相反的文件(或文件库)。这时只能维持原风格,而不是强行引入新风格,造成和原来的约定不一致。但话又说回来,如原有“风格”实际是不好的编码实践,有可能造成bug,严重妨碍维护,还是应尽量全盘纠正问题。

语言对比:C++—— short数据类型

C/C++的short数据类型是short int的缩写。而C#的short是一种实际存在的数据类型。

2.1.2 浮点类型

浮点数精度可变。除非用分数表示时,分母恰好是2的整数次幂,否则用二进制浮点类型无法准确表示该数。将浮点变量设为0.1,很容易表示成0.099 999 999 999 999 999或者0.100 000 000 000 000 000 1(或者其他非常接近0.1的数)。另外,像阿伏伽德罗常数这样非常大的数字(6.02×1023),即使误差为108,结果仍然非常接近6.02×1023,因为原始数字实在是太大了。根据定义,浮点数的精度与它所代表的数字的大小成正比。准确地说,浮点数精度由有效数位的个数决定,而不是由一个固定值(比如±0.01)决定。从.NET Core 3.0开始,double型浮点数可以表示的有效数字最多为17位,float型为最多9位(前提是该数字不是由字符串转换获得的,具体解释请参考后面的高级主题“关于浮点类型的补充说明”)。

C#支持表2.2所示的两个浮点数类型。为了方便理解,二进制数被转换成十进制数。

表2.2 浮点类型

如表2.2所示,二进制数位被转换成15个十进制数位,余数构成第16个十进制数位。具体地说,1.7×10307~1×10308的数只有15个有效数位。但1×10308~1.7×10308的数有16个。decimal类型的有效数位范围与此相似。

高级主题:关于浮点类型的补充说明

浮点数类型(float或者double)采用二进制浮点数来存储数值,这种方法会在小数部分产生舍入误差(rounding error)问题。当使用十进制数来表示实数时,无法用有限个数字来表示1/3所代表的数;类似地,当使用二进制小数来表示数值时,也无法用有限个比特位来表示11/10所代表的数(因为其对应的二进制小数为1.000 110 011 001 100 110 1……)。因此,在实际运算当中,用十进制和二进制所表示的实数都存在舍入误差问题。作为对比,C#的decimal类型在其所允许的精度范围之内不会有误差。decimal类型将在下一节详细介绍[1]

decimal类型数据的公式描述为:±N*10k,其中:

·N为尾数,是一个96位的正整数;

·k为指数,其取值范围为-28<=k<=0。

浮点数类型数据的公式描述为:±N*2k,其中:

·N在float型浮点数中为24位正整数,而在double型浮点数中为53位正整数;

·k在float型浮点数中的取值范围为-149<=k<=+104,而在double型浮点数中的取值范围为-1074<=k<=+970[2]

2.1.3 decimal类型

C#还提供了128位精度的十进制浮点类型(参见表2.3)。它适合大而精确的计算,尤其是金融计算。

表2.3 decimal类型

和浮点数不同,decimal类型保证范围内的所有十进制数都是精确的。所以,对于decimal类型来说,0.1就是0.1,而不是近似值。不过,虽然decimal类型具有比浮点类型更高的精度,但它的范围较小。所以,从浮点类型转换为decimal类型可能发生溢出错误。此外,decimal的计算速度稍慢(虽然差别不大以至于完全可以忽略)。

2.1.4 字面值

字面值(literal value)表示源代码中的固定值。例如,假定希望用System.Console.WriteLine()输出整数值42和double值1.618 034(黄金分割比例),可以使用如代码清单2.1所示的代码。

代码清单2.1 指定字面值

输出2.1展示了代码清单2.1的结果。

输出2.1

初学者主题:硬编码值的时候要慎重

直接将值放到源代码中称为硬编码(hardcoding),因为以后若是更改了值,就必须重新编译代码。因为可能会为维护带来不便,所以开发者在硬编码值的时候必须慎重。

例如,可以考虑从一个外部来源获取值,比如从一个配置文件中。这样以后需要修改值的时候,就不需要重新编译代码了。

默认情况下,输入带小数点的字面值,编译器会自动把它解释成double类型。相反,整数值(没有小数点)通常默认为32位int,前提是该值不是太大,以至于无法用int来存储。如果值太大,编译器会把它解释成long。此外,C#编译器允许向非int的数值类型赋值,前提是字面值对于目标数据类型来说合法。例如,short s=42和byte b=77都是允许的。但这一点仅对字面值成立。不使用额外的语法,b=s就是非法的,具体参见2.4节。

前面说过C#有许多数值类型。在代码清单2.2中,一个字面值被直接放在C#代码中。由于带小数点的值默认为double类型,所以如输出2.2所示,结果是1.61803398874989(最后一个数字5丢失了),这符合我们预期的double值的精度。

代码清单2.2 指定double字面值

输出2.2

要显示具有完整精度的数字,必须将字面值显式声明为decimal类型,这是通过追加一个M(或者m)来实现的,如代码清单2.3和输出2.3所示。

代码清单2.3 指定decimal字面值

输出2.3

代码清单2.3的输出符合预期:1.618033988749895。注意d表示double,之所以用m表示decimal,是因为这种数据类型经常用于货币(monetary)计算。

还可以使用F和D作为后缀,将字面值分别显式声明为float或者double。对于整数数据类型,相应后缀是U、L、LU和UL。整数字面值的类型是像下面这样确定的:

·无后缀的数值字面值按以下顺序解析成能存储该值的第一个数据类型:int,uint,long,ulong。

·带后缀U的数值字面值按以下顺序解析成能存储该值的第一个数据类型:uint,ulong。

·带后缀L的数值字面值按以下顺序解析成能存储该值的第一个数据类型:long,ulong。

·如后缀是UL或LU,就解析成ulong类型。

注意字面值的后缀不区分大小写。但一般推荐大写,避免出现小写字母l和数字1不好区分的情况。

设计规范

·要使用大写的字面值后缀(例如1.618033988749895M)。

有时数字很大,很难辨认。为解决可读性问题,C# 7.0新增了对数字分隔符的支持。如代码清单2.4所示,可在书写数值字面值的时候用下划线(_)分隔。

代码清单2.4 使用数字分隔符

本例将数字转换成千分位,但只是为了好看,C#不要求这样。可在数字第一位和最后一位之间的任何位置添加分隔符。事实上,还可以连写多个下划线。

有时可考虑使用指数记数法,避免在小数点前后写许多个0。指数记数法要求使用e或E中缀,在中缀字母后面添加正整数或者负整数,并在字面值最后添加恰当的数据类型后缀。例如,可将阿伏伽德罗常数作为float输出,如代码清单2.5和输出2.4所示。

代码清单2.5 指数记数法

输出2.4

初学者主题:十六进制记数法

一般使用十进制记数法,即每个数位可用10个符号(0~9)表示。还可使用十六进制记数法,即每个数位可用16个符号表示:0~9,A~F(允许小写)。所以,0x000A对应十进制值10,而0x002A对应十进制值42(2×16+10)。不过,实际的数是一样的。十六进制和十进制的相互转换不会改变数本身,改变的只是数的表示形式。

每个十六进制数位都用4个二进制位表示,所以一个字节可表示两个十六进制数位。

前面讨论数值字面值的时候只使用了十进制值。C#还允许指定十六进制值。为值附加0x前缀,再添加希望使用的十六进制数字,如代码清单2.6所示。

代码清单2.6 十六进制字面值

输出2.5展示了结果。注意,代码输出的仍然是42,而不是0x002A。

输出2.5

从C# 7.0起可将数字表示成二进制值,如代码清单2.7所示。

代码清单2.7 二进制字面值

语法和十六进制语法相似,只是使用0b前缀(允许大写B)。参考第4章的初学者主题“位和字节”了解二进制记数法以及二进制和十进制之间的转换。

注意从C# 7.2起,数字分隔符可以放到代表十六进制的x或者代表二进制的b后面(称为前导数字分隔符)。

高级主题:将数字格式化成十六进制

要显示数值的十六进制形式,必须使用x或X数值格式说明符。大小写决定了十六进制字母的大小写。代码清单2.8展示了一个例子。

代码清单2.8 十六进制格式说明符的例子

输出2.6展示了结果。

输出2.6

注意数值字面值(42)可随便使用十进制或十六进制形式,结果一样。另外,格式说明符前要添加冒号。

高级主题:round-trip格式化

执行System.Console.WriteLine(1.618033988749895);语句默认显示1.61803398874989,最后一个数位被丢弃。为了更准确地标识double值的字符串形式,可以使用格式字符串和round-trip格式说明符R(或者r)进行转换。例如,string.Format("{0:R}", 1.618033988749895)会返回结果1.6180339887498949。

将round-trip格式说明符返回的字符串转换回数值肯定能获得原始值。所以在代码清单2.9中,如果没有使用round-trip格式,两个数就不相等了。

代码清单2.9 使用R格式说明符进行格式化

输出2.7显示了结果。

输出2.7

第一次为text赋值没有使用R格式说明符,所以double.Parse(text)的返回值与原始数值不同。相反,在使用了R格式说明符之后,double.Parse(text)返回的就是原始值。

如果还不熟悉C语言风格的==语法,可以理解为result==number在result等于number的前提下会返回true,result!=number则相反。下一章将讨论赋值和相等性操作符。

[1] 如果读者不理解为何无法用有限个比特位来表示11/10,请自行搜索十进制小数转换二进制小数的计算方法。——译者注

[2] C#与大部分通用编程语言一样,采用IEEE754格式来存储浮点数。简单地说,该格式由三部分组成,其中包含一个24或53位的二进制小数作为尾数(即前述中的N),以及一个8或11位的二进制整数作为指数(即前述中的k),它指定了N中的小数点应当移动的位数,最后还有1位标志整个浮点数的正负。——译者注