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

2.3 数据类型转换

考虑到各种.NET framework实现预定义了大量类型,加上代码也能定义无限数量的类型,所以类型之间的相互转换至关重要。会造成转换的最常见操作就是转型强制类型转换(casting)。

考虑将long值转换成int的情形。long类型能容纳的最大值是9 223 372 036 854 775 808,int则是2 147 483 647。所以转换时可能丢失数据——long值可能大于int能容纳的最大值。有可能造成数据丢失(因为数据尺寸或精度改变)或抛出异常(因为转换失败)的任何转换都需要执行显式转型。相反,不会丢失数据,而且不会抛出异常(无论操作数的类型是什么)的任何转换都可以进行隐式转型

2.3.1 显式转型

C#允许用转型操作符执行转型。通过在圆括号中指定希望变量转换成的类型,表明你已确认在发生显式转型时可能丢失精度和数据,或者可能造成异常。代码清单2.20将一个long转换成int,而且显式告诉系统尝试这个操作。

代码清单2.20 显式转型的例子

程序员使用转型操作符告诉编译器:“相信我,我知道自己正在干什么。我知道值能适应目标类型。”只有程序员像这样做出明确选择,编译器才允许转换。但这也可能只是程序员“一厢情愿”。执行显式转换时,如数据未能成功转换,“运行时”还是会抛出异常。所以,要由程序员负责确保数据成功转换,或提供错误处理代码来处理转换不成功的情况。

高级主题:checked和unchecked转换

C#提供了特殊关键字来标识代码块,指出假如目标数据类型太小以至于容不下所赋的数据,会发生什么情况。默认情况下,容不下的数据在赋值时会悄悄地溢出。代码清单2.21展示了一个例子。

代码清单2.21 整数值溢出

输出2.14展示了结果。

输出2.14

代码清单2.21向控制台写入值-2147483648。但将上述代码放到一个checked块中,或在编译时使用checked选项,就会使“运行时”引发System.OverflowException异常。代码清单2.22给出了checked块的语法。

代码清单2.22 checked块示例

输出2.15展示了结果。

输出2.15

checked块的代码在运行时发生赋值溢出将抛出异常。

C#编译器提供了一个命令行选项将默认行为从unchecked改为checked。此外,C#还支持unchecked块来强制不进行溢出检查,块中溢出的赋值不会抛出异常,如代码清单2.23所示。

代码清单2.23 unchecked块示例

输出2.16展示了结果。

输出2.16

即使开启了编译器的checked选项,上述代码中的unchecked关键字也会阻止“运行时”抛出异常。

读者可能奇怪,在不检查溢出的前提下,在int.MaxValue上加1的结果为什么是-2147483648。这是二进制的回绕(wrap around)语义造成的。int.MaxValue的二进制形式是01111111111111111111111111111111,第一位(0)代表这是正值。递增该值触发回绕,下个值是10000000000000000000000000000000,即最小的整数(int.MinValue),第一位(1)代表这是负值。在int.MinValue上加1变成10000000000000000000000000000001(-2147483647)并如此继续。

转型操作符不是万能药,它不能将一种类型任意转换为其他类型。编译器仍会检查转型操作的有效性。例如,long不能转换成bool。因为没有定义这种转换,所以编译器不允许。

语言对比:数值转换成布尔值

一些人可能觉得奇怪,C#居然不存在从数值类型到布尔类型的有效转型,因为这在其他许多语言中都是很普遍的。C#不支持这样的转换,是为了避免可能发生的歧义,比如-1到底对应true还是false?更重要的是,如下一章要讲到的那样,这还有助于避免用户在本应使用相等操作符的时候使用赋值操作符。例如,可避免在本该写成if(x==42){...}的时候写成if(x=42){...}。

2.3.2 隐式转型

有些情况下,比如从int类型转换成long类型时,不会发生精度的丢失,而且值不会发生根本性的改变,所以代码只需指定赋值操作符,转换将隐式地发生。换言之,编译器判断这样的转换能正常完成。代码清单2.24直接使用赋值操作符实现从int到long的转换。

代码清单2.24 隐式转型无须使用转型操作符

如果愿意,在允许隐式转型的时候也可强制添加转型操作符,如代码清单2.25所示。

代码清单2.25 隐式转型也使用转型操作符

2.3.3 不使用转型操作符的类型转换

由于未定义从字符串到数值类型的转换,因此需要使用像Parse()这样的方法。每个数值数据类型都包含一个Parse()方法,允许将字符串转换成对应的数值类型。如代码清单2.26所示。

代码清单2.26 使用float.Parse()将string转换为数值类型

还可利用特殊类型System.Convert将一种类型转换成另一种。如代码清单2.27所示。

代码清单2.27 使用System.Convert进行类型转换

但System.Convert只支持少量类型,且不可扩展,允许从bool、char、sbyte、short、int、long、ushort、uint、ulong、float、double、decimal、DateTime和string转换到这些类型中的任何一种。

此外,所有类型都支持ToString()方法,可用它提供类型的字符串表示。代码清单2.28演示了如何使用该方法,输出2.17展示了结果。

代码清单2.28 使用ToString()转换成一个string

输出2.17

大多数类型的ToString()方法只是返回数据类型的名称,而不是数据的字符串表示。只有在类型显式实现了ToString()的前提下才会返回字符串表示。最后要注意,完全可以编写自定义的转换方法,“运行时”的许多类都存在这样的方法。

高级主题:TryParse()

从C# 2.0(.NET 2.0)起,所有基元数值类型都包含静态TryParse()方法。该方法与Parse()非常相似,只是转换失败不是抛出异常,而是返回false,如代码清单2.29所示。

代码清单2.29 用TryParse()代替抛出异常

输出2.18展示了结果。

输出2.18

上述代码从输入字符串解析到的值通过out参数(本例是number)返回。

TryParse()除了可以解析数值类型之外,也可以解析枚举类型。

注意从C# 7.0起不用先声明只准备作为out参数使用的变量。代码清单2.30展示了修改后的代码。

代码清单2.30 TryParse()的out参数声明在C# 7.0中可以内联了

注意先写out再写数据类型。这样定义的number变量在if语句内部和外部均可使用,而不管TryParse()向if语句返回true还是false。

Parse()和TryParse()的关键区别在于,如果转换失败,TryParse()不会抛出异常。string到数值类型的转换是否成功,往往取决于输入文本的用户。用户完全可能输入无法成功解析的数据。使用TryParse()而不是Parse(),就可以避免在这种情况下抛出异常(由于预见到用户会输入无效数据,所以要想办法避免抛出异常)。