5.1 类型转换
本书前面说过,无论是什么类型,所有数据都是一系列的位,即一系列0和1。变量的含义是通过解释这些数据的方式来确定的。最简单的示例是char类型,这种类型用一个数字表示Unicode字符集中的一个字符。实际上,这个数字与ushort的存储方式完全相同——它们都存储0和65535之间的数字。
但一般情况下,不同类型的变量使用不同的模式来表示数据。这意味着,即使可以把一系列的位从一种类型的变量移动到另一种类型的变量中(也许它们占用的存储空间相同,也许目标类型有足够的存储空间包含所有的源数据位),结果也可能与期望的不同。
因此,需要对数据进行类型转换,而不是将数据位从一个变量一对一映射到另一个变量。类型转换采用以下两种形式:
● 隐式转换:从类型A到类型B的转换可在所有情况下进行,执行转换的规则非常简单,可以让编译器执行转换。
● 显式转换:从类型A到类型B的转换只能在某些情况下进行,转换规则比较复杂,应进行某种类型的额外处理。
5.1.1 隐式转换
隐式转换不需要做任何工作,也不需要另外编写代码。考虑下面的代码:
var1 = var2;
如果var2的类型可以隐式地转换为var1的类型,这条赋值语句就涉及隐式转换。这两个变量的类型也可能相同,此时就不需要隐式转换。例如,ushort和char的值是可以互换的,因为它们都可以存储0和65535之间的数字,在这两种类型之间可以进行隐式转换,如下面的代码所示:
ushort destinationVar; char sourceVar = 'a'; destinationVar = sourceVar; WriteLine($"sourceVar val: {sourceVar}"); WriteLine($"destinationVar val: {destinationVar}");
这里存储在sourceVar中的值放在destinationVar中。在用两个WriteLine()命令输出变量时,得到如下结果:
sourceVar val: a destinationVar val: 97
即使两个变量存储的信息相同,使用不同的类型解释它们时,方式也是不同的。
简单类型有许多隐式转换;bool和string没有隐式转换,但数值类型有一些隐式转换。表5-1列出了编译器可以隐式执行的数值转换(记住,char存储的是数值,所以char被当作数值类型)。
表5-1 隐式数值转换
不要担心——不需要记住这个表格,因为很容易看出编译器可以执行哪些隐式转换。第3章中的表3-1、表3-2和表3-3列出了每种简单数字类型的取值范围。这些类型的隐式转换规则是:任何类型A,只要其取值范围完全包含在类型B的取值范围内,就可以隐式转换为类型B。
其原因是很简单的。如果要把一个值放在变量中,而该值超出了变量的取值范围,就会出问题。例如,short类型的变量可以存储0-32767的数字,而byte可以存储的最大值是255,所以如果要把short值转换为byte值,就会出问题。如果short包含的值在256和32767之间,相应数值就不能放在byte中。
但是,如果short类型变量中的值小于255,就应能转换这个值吗?答案是可以。具体地说,虽然可以,但必须使用显式转换。执行显式转换有点类似于“我已经知道你对我这么做提出了警告,但我将对其后果负责”。
5.1.2 显式转换
顾名思义,在明确要求编译器把数值从一种数据类型转换为另一种数据类型时,就是在执行显式转换。因此,这需要另外编写代码,代码的格式因转换方法而异。在学习显式转换代码前,首先分析如果不添加任何显式转换代码,会发生什么情况。
例如,下面对上一节的代码进行修改,试着把short值转换为byte类型:
byte destinationVar; short sourceVar = 7; destinationVar = sourceVar; WriteLine($"sourceVar val: {sourceVar}"); WriteLine($"destinationVar val: {destinationVar}");
如果编译这段代码,就会产生如下错误:
Cannot implicitly convert type 'short' to 'byte'. An explicit conversion exists (are you missing a cast? )
为成功编译这段代码,需要添加代码,进行显式转换。最简单的方式是把short变量强制转换为byte类型(由上述错误字符串提出)。强制转换就是强迫数据从一种类型转换为另一种类型,其语法比较简单:
(<destinationType>)<sourceVar>
这将把<sourceVar>中的值转换为<destinationType>类型。
注意:这只在某些情况下是可行的。彼此之间几乎没有什么关系的类型或根本没有关系的类型不能进行强制转换。
因此可以使用这个语法修改示例,把short变量强制转换为byte类型:
byte destinationVar;
short sourceVar = 7;
destinationVar = (byte)sourceVar;
WriteLine($"sourceVar val: {sourceVar}");
WriteLine($"destinationVar val: {destinationVar}");
得到如下结果:
sourceVar val: 7 destinationVar val: 7
在试图把一个值转换为不兼容的变量类型时,会发生什么呢?以整数为例,不能把一个大整数放到一个太小的数值类型中。按如下所示修改代码就能证明这一点:
byte destinationVar;
short sourceVar = 281;
destinationVar = (byte)sourceVar;
WriteLine($"sourceVar val: {sourceVar}");
WriteLine($"destinationVar val: {destinationVar}");
结果如下:
sourceVar val: 281 destinationVar val: 25
发生了什么?看看这两个数字的二进制表示,以及可以存储在byte中的最大值255:
281 = 100011001 25 = 000011001 255 = 011111111
可以看出,源数据的最左边一位丢失了。这会引发一个问题:如何确定数据是何时丢失的?显然,当需要显式地把一种数据类型转换为另一种数据类型时,最好能够了解是否有数据丢失了。如果不知道这些,就会发生严重问题。例如,财务应用程序或确定火箭飞往月球的轨道的应用程序。
一种方式是检查源变量的值,将它与目标变量的取值范围进行比较。还有另一个技术,就是迫使系统特别注意运行期间的转换。在将一个值放在一个变量中时,如果该值过大,不能放在该类型的变量中,就会导致溢出,这就需要检查。
对于为表达式设置所谓的溢出检查上下文,需要用到两个关键字—— checked和unchecked。按下述方式使用这两个关键字:
checked(<expression>) unchecked(<expression>)
下面对上一个示例进行溢出检查:
byte destinationVar; short sourceVar = 281; destinationVar = checked((byte)sourceVar); WriteLine($"sourceVar val: {sourceVar}"); WriteLine($"destinationVar val: {destinationVar}");
执行这段代码时,程序会崩溃,并显示如图5-1所示的错误信息(在OverflowCheck项目中编译这段代码)。
图5-1
但在这段代码中,如果用unchecked替代checked,就会得到与以前同样的结果,不会出现错误。这与前面的默认做法是一样的。
也可以配置应用程序,让这种类型的表达式都和包含checked关键字一样,除非表达式明确使用unchecked关键字(换言之,可以改变溢出检查的默认设置)。为此,应修改项目的属性:右击Solution Explorer窗口中的项目,选择Properties选项。单击窗口左边的Build,打开Build设置。
要修改的属性是一个Advanced设置,所以单击Advanced按钮。在打开的对话框中,选中Check for arithmetic overflow/underflow选项,如图5-2所示。默认情况下禁用这个设置,激活它可以提供上述checked行为。
图5-2
5.1.3 使用Convert命令进行显式转换
前面章节中的许多“试一试”示例中使用的显式类型转换,与本章前面的示例有一些区别。前面使用ToDouble()等命令把字符串值转换为数值,显然,这种方式并不适用于所有字符串。
例如,如果使用ToDouble()把Number字符串转换为double值,在执行代码时,将看到如图5-3所示的对话框。
图5-3
可以看出,执行失败。为成功执行此类转换,所提供的字符串必须是数值的有效表达方式,该数还必须是不会溢出的数。数值的有效表达方式是:首先是一个可选符号(加号或减号),然后是0位或多位数字,一个可选的句点后跟一位或多位数字,接着是一个可选的e或E,后跟一个可选符号和一位或多位数字,除了还可能有空格(在这个序列之前或之后),不能有其他字符。利用这些可选的额外数据,可将-1.2451e-24这样复杂的字符串识别为数值。
对于这些转换要注意的一个问题是,它们总是要进行溢出检查,checked和unchecked关键字以及项目属性设置不起作用。
下面的示例包括本节介绍的许多转换类型。它声明和初始化许多不同类型的变量,再在它们之间进行隐式和显式转换。
试一试:类型转换实践:Ch05Ex01\Program.cs
(1)在C:\BegVCSharp\Chapter05目录中创建一个新的控制台应用程序Ch05Ex01。
(2)把下述代码添加到Program.cs中:
static void Main(string[] args) { short shortResult, shortVal = 4; int integerVal = 67; long longResult; float floatVal = 10.5F; double doubleResult, doubleVal = 99.999; string stringResult, stringVal = "17"; bool boolVal = true; WriteLine("Variable Conversion Examples\n"); doubleResult = floatVal * shortVal; WriteLine($"Implicit, -> double: {floatVal} * {shortVal} -> { doubleResult }"); shortResult = (short)floatVal; WriteLine($"Explicit, -> short: {floatVal} -> {shortResult}"); stringResult = Convert.ToString(boolVal) + Convert.ToString(doubleVal); WriteLine($"Explicit, -> string: \"{boolVal}\" + \"{doubleVal}\" -> " + $"{stringResult}"); longResult = integerVal + ToInt64(stringVal); WriteLine($"Mixed, -> long: {integerVal} + {stringVal} -> {longResult}"); ReadKey(); }
(3)执行代码,结果如图5-4所示。
图5-4
示例说明
这个示例包含前面介绍的所有转换类型,既有像前面简短代码示例中的简单赋值,也有在表达式中进行的转换。必须考虑这两种情况,因为每个非一元运算符的处理都可能要进行类型转换,而不仅是赋值运算符。例如:
shortVal * floatVal
其中把一个short值与一个float值相乘。在这样的指令中,没有指定显式转换,所以如有可能,就会进行隐式转换。在这个示例中,唯一有意义的隐式转换是把short值转换为float值(因为把float值转换为short值需要进行显式转换),所以这里将使用隐式转换。
也可以覆盖这种行为,如下所示:
shortVal * (short)floatVal
注意:有趣的是,两个short值相乘的结果并不会返回一个short值。因为这个操作的结果很可能大于32767(这是short类型可以存储的最大值),所以这个操作的结果实际上是int值。
这个转换过程初看起来比较复杂,但只要按照运算符的优先级把表达式分解为不同的部分,就可以弄明白这个过程。