![C# 8.0本质论](https://wfqqreader-1252317822.image.myqcloud.com/cover/306/43475306/b_43475306.jpg)
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 显式转型的例子
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.20.jpg?sign=1739351392-gkNJoUCSIWNSsoflpMGmKoChpg2B17Nl-0-4af3199351748748fa66e05bf14f1ba0)
程序员使用转型操作符告诉编译器:“相信我,我知道自己正在干什么。我知道值能适应目标类型。”只有程序员像这样做出明确选择,编译器才允许转换。但这也可能只是程序员“一厢情愿”。执行显式转换时,如数据未能成功转换,“运行时”还是会抛出异常。所以,要由程序员负责确保数据成功转换,或提供错误处理代码来处理转换不成功的情况。
高级主题:checked和unchecked转换
C#提供了特殊关键字来标识代码块,指出假如目标数据类型太小以至于容不下所赋的数据,会发生什么情况。默认情况下,容不下的数据在赋值时会悄悄地溢出。代码清单2.21展示了一个例子。
代码清单2.21 整数值溢出
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.21.jpg?sign=1739351392-Tjxoa3fxoOpQzjeEPYGfh6Ir8JuDMHLb-0-d744c65c73042da501406ffca54f61b2)
输出2.14展示了结果。
输出2.14
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.14.jpg?sign=1739351392-7RqJbVRzEHp4E9JKtU3MP02conrt9fx8-0-1346c6e052081514ecc02ad36a530d66)
代码清单2.21向控制台写入值-2147483648。但将上述代码放到一个checked块中,或在编译时使用checked选项,就会使“运行时”引发System.OverflowException异常。代码清单2.22给出了checked块的语法。
代码清单2.22 checked块示例
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.22.jpg?sign=1739351392-KT0EgmVDKTuMqMRm88fjblCcxMvwBS61-0-0d366ee478cde083d4c8d1acfeb495f3)
输出2.15展示了结果。
输出2.15
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.15.jpg?sign=1739351392-syGvitqUH0flFDQZvtJYfugFHySf03g6-0-2f47683b5c5765bda83f1db30b318e0d)
checked块的代码在运行时发生赋值溢出将抛出异常。
C#编译器提供了一个命令行选项将默认行为从unchecked改为checked。此外,C#还支持unchecked块来强制不进行溢出检查,块中溢出的赋值不会抛出异常,如代码清单2.23所示。
代码清单2.23 unchecked块示例
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.23.jpg?sign=1739351392-GYDRb51MiVEyXsIx1dNwBin7cJA7Rty8-0-d90657e1f40081cf65263fed8b007ebf)
输出2.16展示了结果。
输出2.16
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.16.jpg?sign=1739351392-ivp1tVNlLibCmK6TgoYNmJhXsibBQEr8-0-f5ff9ccfe11ac3e8e98df87724dc78b5)
即使开启了编译器的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 隐式转型无须使用转型操作符
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.24.jpg?sign=1739351392-GCxyYngNyHhNvFEX0TuyJQjaWd2NnLsD-0-a739e08a2d5feee77d1bc8e79143ab2d)
如果愿意,在允许隐式转型的时候也可强制添加转型操作符,如代码清单2.25所示。
代码清单2.25 隐式转型也使用转型操作符
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.25.jpg?sign=1739351392-av3CnFwUHzzkQoTnyygdeI6BzJ2atVzQ-0-c75cb681a80bc85658e831c7ab047623)
2.3.3 不使用转型操作符的类型转换
由于未定义从字符串到数值类型的转换,因此需要使用像Parse()这样的方法。每个数值数据类型都包含一个Parse()方法,允许将字符串转换成对应的数值类型。如代码清单2.26所示。
代码清单2.26 使用float.Parse()将string转换为数值类型
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.26.jpg?sign=1739351392-LzKAREVfuceFtXxdk0UjM9ahhYWmSLBM-0-72191700abe130c7f092ec01790fb701)
还可利用特殊类型System.Convert将一种类型转换成另一种。如代码清单2.27所示。
代码清单2.27 使用System.Convert进行类型转换
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.27.jpg?sign=1739351392-3vAru6UkPRESNihGrrWmprPefVNf8V7D-0-8efeb46865acb3b2f3dc6a5de4e4d668)
但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
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.28.jpg?sign=1739351392-RpJOvVVLGmD25Yo4usADUm8CRJKUTwOO-0-10dbcdcf1d711bf8d907a1dba129c7b0)
输出2.17
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.17.jpg?sign=1739351392-IzKzsPkMRS1EM2hoAmwN8oN2FjMKmvaO-0-0aec86a79cddbbba22e538d14a82c26a)
大多数类型的ToString()方法只是返回数据类型的名称,而不是数据的字符串表示。只有在类型显式实现了ToString()的前提下才会返回字符串表示。最后要注意,完全可以编写自定义的转换方法,“运行时”的许多类都存在这样的方法。
高级主题:TryParse()
从C# 2.0(.NET 2.0)起,所有基元数值类型都包含静态TryParse()方法。该方法与Parse()非常相似,只是转换失败不是抛出异常,而是返回false,如代码清单2.29所示。
代码清单2.29 用TryParse()代替抛出异常
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.29.jpg?sign=1739351392-QDRGZzOHfwfJ1a34u5b8kN74JcfKNpR0-0-e8724d5a937f53c04d84713af7277015)
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.29x.jpg?sign=1739351392-ipolgSWXBm1boaVYN7ecdRRWdhkIaXVd-0-b5330bedcb826684e19fe7ed19ecfbfc)
输出2.18展示了结果。
输出2.18
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.18.jpg?sign=1739351392-RhOYPFcfOjm65RbiBDHnXSBpfQZkf7tX-0-ec8f9cb9b93bdb99869b4d0aaed29da1)
上述代码从输入字符串解析到的值通过out参数(本例是number)返回。
TryParse()除了可以解析数值类型之外,也可以解析枚举类型。
注意从C# 7.0起不用先声明只准备作为out参数使用的变量。代码清单2.30展示了修改后的代码。
代码清单2.30 TryParse()的out参数声明在C# 7.0中可以内联了
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.30.jpg?sign=1739351392-wTvyDNNaT6Dm6fTIS54MBNirdIYVA0H1-0-8346d3dfff41ae7b12269e1e0467a6ce)
注意先写out再写数据类型。这样定义的number变量在if语句内部和外部均可使用,而不管TryParse()向if语句返回true还是false。
Parse()和TryParse()的关键区别在于,如果转换失败,TryParse()不会抛出异常。string到数值类型的转换是否成功,往往取决于输入文本的用户。用户完全可能输入无法成功解析的数据。使用TryParse()而不是Parse(),就可以避免在这种情况下抛出异常(由于预见到用户会输入无效数据,所以要想办法避免抛出异常)。