C# 10核心技术指南
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.4 数值类型

表2-1中列出了C#中所有的预定义数值类型。

表2-1:C#中的预定义数值类型

在整数类型中,int和long是最基本的类型,C#和运行时对它们都有良好的支持。其他的整数类型通常用于实现互操作性或优化存储空间使用效率等情况。nint和unint是C# 9引入的原生大小的整数类型,它们适用于执行指针算法,我们将在4.18.8节进行介绍。

在实数类型中,float和double称为浮点类型[2],并通常用于科学和图形计算。decimal类型通常用于金融计算这种十进制下的高精度算术运算。

从.NET 5开始,运行时引入了一种16位浮点类型,称为Half。该类型主要针对与显卡处理器的互操作,大多数CPU都没有对这种类型提供原生的支持。Half并非CLR的基元类型,C#对该类型也不存在特殊的语言支持。

2.4.1 数值字面量

整数类型字面量可以使用十进制或者十六进制表示,十六进制辅以0x前缀,例如:

我们可以在数值字面量的任意位置加入下划线以方便阅读:

也可以用0b前缀使用二进制表示数值:

实数字面量可以用小数或指数表示,例如:

2.4.1.1 数值字面量类型推断

默认情况下,编译器将数值字面量推断为double类型或者整数类型:

· 如果这个字面量包含小数点或者指数符号(E),那么它是double。

· 否则,这个字面量的类型就是下列能满足这个字面量的第一个类型:int、uint、long和ulong。

例如:

2.4.1.2 数值后缀

数值后缀显式定义了字面量的类型。后缀可以是下列小写或大写字母:

一般U和L后缀是很少需要的,因为uint、long和ulong总是可以推断出来或者从int类型隐式转换而来:

从技术上讲,后缀D是多余的,因为所有带小数点的字面量都会被推断为double类型。因此可以直接在数值字面量后加上小数点:

后缀F和M是最有用的,并应该在指定float或decimal字面量时使用。下面的语句不能在没有后缀F时进行编译,因为4.5会被认定为double,而double是无法隐式转换为float的:

同样的规则也适用于decimal字面量:

我们将在2.4.2节详细介绍数值转换的语义。

2.4.2 数值转换

2.4.2.1 整数类型到整数类型的转换

整数类型转换在目标类型能够表示源类型的所有可能值时是隐式转换,否则需要显式转换,例如:

2.4.2.2 浮点类型到浮点类型的转换

double能表示所有可能的float值,因此float能隐式转换为double。反之则必须是显式转换。

2.4.2.3 浮点类型到整数类型的转换

所有整数类型可以隐式转换为浮点类型:

反之,则必须是显式转换:

将浮点数转换为整数时,小数点后的数值将被截去而不会舍入。静态类System.Convert提供了在不同值类型之间转换的舍入方法(参见第6章)。

将大的整数类型隐式转换为浮点类型会保留数值部分,但是有时会丢失精度。这是因为浮点类型虽然拥有比整数类型更大的数值,但是有时其精度却比整数类型要小。以下代码用一个更大的数重复上述示例展示了这种精度丢失的情况:

2.4.2.4 decimal类型转换

所有的整数类型都能隐式转换为decimal类型,这是因为decimal可以表示所有可能的C#整数类型值。其他所有的数值类型转换为decimal或从decimal类型进行转换都必须是显式转换,因为这些转换要么数值可能超越边界,要么可能发生精度损失。

2.4.3 算术运算符

算术运算符(+、-、*、/、%)可应用于除8位和16位的整数类型之外的所有数值类型:

2.4.4 自增和自减运算符

自增和自减运算符(++、--)分别给数值类型加1或者减1,具体要将其放在变量之前还是之后则取决于需要得到变量在自增/自减之前的值还是之后的值,例如:

2.4.5 特殊整数类型运算

整数类型指int、uint、long、ulong、short、ushort、byte和sbyte。

2.4.5.1 整数除法

整数类型的除法运算总是会舍去余数(向0舍入),用一个值为0的变量做除数将产生运行时错误(DivideByZeroException):

用字面量或常量0做除数将产生编译时错误。

2.4.5.2 整数溢出

在运行时执行整数类型的算术运算可能会造成溢出。默认情况下,溢出会默默地发生而不会抛出任何异常,且其溢出行为是“周而复始”的。就像是运算发生在更大的整数类型上,而超出部分的进位就被丢弃了。例如,减小最小的整数值将产生最大的整数值:

2.4.5.3 整数运算溢出检查运算符

checked运算符的作用是,在运行时当整数类型表达式或语句超过相应类型的算术限制时不再默默地溢出,而是抛出OverflowException。checked运算符可在有++、--、+、-(一元运算符和二元运算符)、*、/和整数类型间显式转换运算符的表达式中起作用。溢出检查会带来微小的性能损失。

checked运算符对double和float类型没有作用(它们会溢出为特殊的“无限”值,这会在后面介绍),对decimal类型也没有作用(这种类型总是会进行溢出检查)。

checked运算符既可以包裹表达式也能够包裹语句块,例如:

在编译时打开checked开关(在Visual Studio中,可以在“Advanced Build Settings”中设置)将使程序在默认情况下对所有表达式都进行算术溢出检查。如果你只想禁用指定表达式或语句的溢出检查,可以用unchecked运算符。例如,下面的代码即使在编译时打开了checked开关也不会抛出异常:

2.4.5.4 常量表达式的溢出检查

无论是否打开了checked工程选项,编译时的表达式计算总会检查溢出,除非使用unchecked运算符。

2.4.5.5 位运算符

C#支持以下的位运算符:

.NET 6将其他位运算操作添加到了System.Numerics命名空间下的BitOperations的类中(请参见6.10节)。

2.4.6 8位和16位整数类型

8位和16位整数类型是指byte、sbyte、short和ushort。这些类型自己并不具备算术运算符,所以C#隐式地将它们转换为所需的更大一些的类型。当试图把运算结果赋给一个小的整数类型时,会产生编译时错误:

在以上情况下,x和y会隐式转换成int以便进行加法运算。因此运算结果也是int,它不能隐式转换回short(因为这可能会造成数据丢失)。我们必须使用显式转换才能通过编译:

2.4.7 特殊的float和double值

不同于整数类型,浮点类型还包含一些特殊的值,这些值在特定运算中需要特殊对待。这些特殊的值是NaN(Not a Number,非数字)、+∞、-∞和-0。float和double类型包含表示NaN、+∞和-∞值的常量,其他的常量还有MaxValue、MinValue以及Epsilon,例如:

double和float类型的特殊值的常量表如下:

非零值除以零的结果是无穷大:

零除以零,或无穷大减去无穷大的结果是NaN:

使用比较运算符(==)时,一个NaN的值永远也不等于其他的值,甚至不等于其他的NaN值:

必须使用float.IsNaN或double.IsNaN方法来判断一个值是否为NaN:

但当使用object.Equals方法时,两个NaN却是相等的:

NaN在表示特殊值时很有用。在Windows Presentation Foundation(WPF)中,double.NaN表示值为“Automatic”(自动),另一种表示这种值的方法是使用可空值类型(nullable,参见第4章)。还可以使用一个包含数值类型和一个额外字段的自定义结构体(参见第3章)来表示。

float和double遵循IEEE 754格式类型规范。几乎所有的处理器都原生支持此规范。如果需要此类型行为的详细信息,可参考IEEE官方网站(http://www.ieee.org/)。

2.4.8 double和decimal的对比

double类型常用于科学计算(例如,计算空间坐标)。decimal类型常用于金融计算和计算那些“人为”的而非真实世界的度量值。以下是这两种类型的不同之处:

(续)

2.4.9 实数的舍入误差

float和double在内部都是基于2来表示数值的。因此只有基于2表示的数值才能够精确表示。事实上,这意味着大多数有小数部分的字面量(它们都基于10)将无法精确表示。例如:

这就是为什么float和double不适合金融运算。相反,decimal基于10,它能够精确表示基于10的数值(也包括它的因数、基于2和基于5的数值)。因为实数的字面量都是基于10的,所以decimal能够精确表示像0.1这样的数。然而,double和decimal都不能精确表示那些基于10的循环小数:

这将会导致积累性的舍入误差:

这也将影响相等和比较操作: