![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=1738948486-aAlNJbtmR7zncHHQqYInW9cyrKi6tgBW-0-3763a00f1244262f96214a7a206cc4a8)
程序员使用转型操作符告诉编译器:“相信我,我知道自己正在干什么。我知道值能适应目标类型。”只有程序员像这样做出明确选择,编译器才允许转换。但这也可能只是程序员“一厢情愿”。执行显式转换时,如数据未能成功转换,“运行时”还是会抛出异常。所以,要由程序员负责确保数据成功转换,或提供错误处理代码来处理转换不成功的情况。
高级主题:checked和unchecked转换
C#提供了特殊关键字来标识代码块,指出假如目标数据类型太小以至于容不下所赋的数据,会发生什么情况。默认情况下,容不下的数据在赋值时会悄悄地溢出。代码清单2.21展示了一个例子。
代码清单2.21 整数值溢出
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.21.jpg?sign=1738948486-tRvcwWEdIMpHLp76kHLaIcidqoRtz0Rb-0-82df3cb7f14eb5f50b61370c95c4aa7d)
输出2.14展示了结果。
输出2.14
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.14.jpg?sign=1738948486-igUXh8XVJeQT4WuN3MSJPVjmsx23PmUF-0-4d628b628defeeb5a3b925e58fed6a4b)
代码清单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=1738948486-IKphSc2O0wJGrQXZAgSHSnBCh5JuZE0b-0-550e9eac4e7d90522ea2f572e7be7404)
输出2.15展示了结果。
输出2.15
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.15.jpg?sign=1738948486-zPcsmU53ofPyqTooEkinh5VAsLIWNFmz-0-ca02bc30494b6a1a8f23d30419e9582c)
checked块的代码在运行时发生赋值溢出将抛出异常。
C#编译器提供了一个命令行选项将默认行为从unchecked改为checked。此外,C#还支持unchecked块来强制不进行溢出检查,块中溢出的赋值不会抛出异常,如代码清单2.23所示。
代码清单2.23 unchecked块示例
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.23.jpg?sign=1738948486-Ep1ewIXKGzzxmZwA1tT0Al3DdpXJv1gG-0-6a7e2cf02754382ea1e47d06460a1592)
输出2.16展示了结果。
输出2.16
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.16.jpg?sign=1738948486-8mkVxeaUTKPIN1WDBicdLg9tJd8JnQCa-0-8d6015e21026a6f75e13b675f1d1edc9)
即使开启了编译器的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=1738948486-oy5IJ6ktXnE9AscGPG8NrYCcHmdCWfzr-0-2f59da39a47048e6553e42d5bc0071fa)
如果愿意,在允许隐式转型的时候也可强制添加转型操作符,如代码清单2.25所示。
代码清单2.25 隐式转型也使用转型操作符
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.25.jpg?sign=1738948486-jy1kE4rvSPP60GrrAkJEK6IewDyVIKTN-0-0cc6ad89f82722975ab3faedf6f1b572)
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=1738948486-39bUoKtmVoFbjjPWP43n0Syu3N8vN1m2-0-44d45110b6bd9a707e6bdf701cb9c6b1)
还可利用特殊类型System.Convert将一种类型转换成另一种。如代码清单2.27所示。
代码清单2.27 使用System.Convert进行类型转换
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.27.jpg?sign=1738948486-NS7gGeFE8Kt0rwvcyfxbHia9nMOieyRS-0-85a55f8369ab0c515afcd0d9dad240aa)
但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=1738948486-ioAbyeJmlOgptDbRXY6svKIgn69IbB37-0-2f63f9bba52619a8ebcf073ab4606c5d)
输出2.17
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.17.jpg?sign=1738948486-evoo5hBuE207ECZApB7WbUha0jBZQG12-0-e7288ab247f0a923943028ec759ae3ea)
大多数类型的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=1738948486-T2AAINytCuBFHsD2ZwC1U32h63X63L9H-0-b490007cf6d1bd127bbd5a0a528e0874)
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.29x.jpg?sign=1738948486-ArHBG4SwZPvNj2uM2SpsJNmYtQrgHFim-0-36e72e1ba281c11648d1673e3900022d)
输出2.18展示了结果。
输出2.18
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.18.jpg?sign=1738948486-miVWSkrNmaw3eATAlgXuGVvFOAg7TFWP-0-a6d0219c1064be08b8e1254727aac88f)
上述代码从输入字符串解析到的值通过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=1738948486-jPWWxjtpPrOMegL4TzUqEyBqMcucXma0-0-d45c3431118547869fd17d6b1a578e52)
注意先写out再写数据类型。这样定义的number变量在if语句内部和外部均可使用,而不管TryParse()向if语句返回true还是false。
Parse()和TryParse()的关键区别在于,如果转换失败,TryParse()不会抛出异常。string到数值类型的转换是否成功,往往取决于输入文本的用户。用户完全可能输入无法成功解析的数据。使用TryParse()而不是Parse(),就可以避免在这种情况下抛出异常(由于预见到用户会输入无效数据,所以要想办法避免抛出异常)。