![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=1739056351-ruGEh8bWNE5BiMIdykj4t6a898yat0As-0-56d3cfd9bedfa13ba63360858ff83bc5)
程序员使用转型操作符告诉编译器:“相信我,我知道自己正在干什么。我知道值能适应目标类型。”只有程序员像这样做出明确选择,编译器才允许转换。但这也可能只是程序员“一厢情愿”。执行显式转换时,如数据未能成功转换,“运行时”还是会抛出异常。所以,要由程序员负责确保数据成功转换,或提供错误处理代码来处理转换不成功的情况。
高级主题:checked和unchecked转换
C#提供了特殊关键字来标识代码块,指出假如目标数据类型太小以至于容不下所赋的数据,会发生什么情况。默认情况下,容不下的数据在赋值时会悄悄地溢出。代码清单2.21展示了一个例子。
代码清单2.21 整数值溢出
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.21.jpg?sign=1739056351-SGAhkKdbKbriiXmC9bpYOUzCxgtjjFvc-0-67d8bafaea4628a847923743101f4d40)
输出2.14展示了结果。
输出2.14
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.14.jpg?sign=1739056351-6f18ps6KtGqaHfxRlcJsq1onpDjFeGea-0-d1caed726a0544088a4fa311437c03c7)
代码清单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=1739056351-7rarlRVPLW6eCW3rqup3o98pjEFkIJ4D-0-6af6658a5432cc7bee6db4b2c9452881)
输出2.15展示了结果。
输出2.15
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.15.jpg?sign=1739056351-RuRJR88GYnfNfXjKKDfIyBoygd4xC0m7-0-0d90dea435a1108b2047da1b641cf79e)
checked块的代码在运行时发生赋值溢出将抛出异常。
C#编译器提供了一个命令行选项将默认行为从unchecked改为checked。此外,C#还支持unchecked块来强制不进行溢出检查,块中溢出的赋值不会抛出异常,如代码清单2.23所示。
代码清单2.23 unchecked块示例
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.23.jpg?sign=1739056351-ItTLgY59JrjWi801SZPEzMhd7b8b2Ddg-0-7b6c679c7e0a41b8ca0c02efede61e4e)
输出2.16展示了结果。
输出2.16
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.16.jpg?sign=1739056351-XdTxEy85oAqiSNXkPWKXht5hWv3Ai0BP-0-7cafa945d9d732581ff04ce870685770)
即使开启了编译器的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=1739056351-zjYS68abNoinnANAOZW6CgI5inY6448c-0-1b532b7efe79cf5bbf8f4aef0c93f5ea)
如果愿意,在允许隐式转型的时候也可强制添加转型操作符,如代码清单2.25所示。
代码清单2.25 隐式转型也使用转型操作符
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.25.jpg?sign=1739056351-2jEx2yRDvshIOS8vhXPHvnfreEqp09V2-0-7e3151312db93cca77dabf58d7620921)
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=1739056351-6ZzcOlifL8qyqLjaUZnctngoFTYVPzq5-0-bd70b8eea10e6dbcc072ec0a4058012a)
还可利用特殊类型System.Convert将一种类型转换成另一种。如代码清单2.27所示。
代码清单2.27 使用System.Convert进行类型转换
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.27.jpg?sign=1739056351-VCM5ycj17rkk1f15eyYrIANb9FXwMTTV-0-e48240fc56d5e160241b6e784f318eea)
但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=1739056351-lMI5vm5JtodtBV1mQVP7Fjq3ne8tWRI3-0-83b72cf4c5b35dd67f1616db9e2c3063)
输出2.17
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.17.jpg?sign=1739056351-NMHgrZZClhUQOUfChf31heDoUa6KltM8-0-80881ef398cf08a5442b960fc6b1892c)
大多数类型的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=1739056351-QTW21ixyJWi9taBvrPhP4Z8hsnet9xfj-0-3c3bbbed8ea78c35b9856941f50c9e08)
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.29x.jpg?sign=1739056351-JVow64llMjwKpOc8qszOdnZm0Vb8Xkwp-0-5eb8b4ee4a83d7d348f68abbd63758b8)
输出2.18展示了结果。
输出2.18
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.18.jpg?sign=1739056351-G8oTaViKxmn2mwVXE6laMb0Mr9ySCPVe-0-aabd6586328fe177c08b6fc2d8891ff4)
上述代码从输入字符串解析到的值通过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=1739056351-qkjb4fvR1hXsoUrNZUG5muFTx1RARrRr-0-7fabc95ec612efaac908d2aca50f21f0)
注意先写out再写数据类型。这样定义的number变量在if语句内部和外部均可使用,而不管TryParse()向if语句返回true还是false。
Parse()和TryParse()的关键区别在于,如果转换失败,TryParse()不会抛出异常。string到数值类型的转换是否成功,往往取决于输入文本的用户。用户完全可能输入无法成功解析的数据。使用TryParse()而不是Parse(),就可以避免在这种情况下抛出异常(由于预见到用户会输入无效数据,所以要想办法避免抛出异常)。