8.3 类型的安全性
第1章提到中间语言(IL)可以对其代码强制实现强类型安全性。强类型化支持.NET提供的许多服务,包括安全性和语言的交互性。因为C#语言会编译为IL,所以C#也是强类型的。此外,这说明数据类型并不总是可无缝互换。本节将介绍基本类型之间的转换。
注意:C#也支持不同引用类型之间的轮换,在与其他类型相互转换时还允许定义所创建的数据类型的行为方式。本章稍后将详细讨论这两个主题。
另一方面,泛型可以避免对一些常见的情形进行类型转换,详见第6章和第11章。
8.3.1 类型转换
我们常常需要把数据从一种类型转换为另一种类型。考虑下面的代码:
byte value1 = 10; byte value2 = 23; byte total; total = value1 + value2; WriteLine(total);
在试图编译这些代码行时,会得到一条错误消息:
Cannot implicitly convert type 'int' to 'byte'
问题是,我们把两个byte型数据加在一起时,应返回int型结果,而不是另一个byte数据。这是因为byte包含的数据只能为8位,所以把两个byte型数据加在一起,很容易得到不能存储在单个byte型数据中的值。如果要把结果存储在一个byte变量中,就必须把它转换回byte类型。C#支持两种转换方式:隐式转换和显式转换。
1.隐式转换
只要能保证值不会发生任何变化,类型转换就可以自动(隐式)进行。这就是前面代码失败的原因:试图从int转换为byte,而可能丢失了3个字节的数据。编译器不允许这么做,除非我们明确告诉它这就是我们希望的结果!如果在long类型变量而非byte类型变量中存储结果,就不会有问题了:
byte value1 = 10; byte value2 = 23; long total; // this will compile fine total = value1 + value2; WriteLine(total);
程序可以顺利编译,而没有任何错误,这是因为long类型变量包含的数据字节比byte类型多,所以没有丢失数据的危险。在这些情况下,编译器会很顺利地转换,我们也不需要显式提出要求。
表8-4列出了C#支持的隐式类型转换。
表8-4
注意,只能从较小的整数类型隐式地转换为较大的整数类型,而不能从较大的整数类型隐式地转换为较小的整数类型。也可以在整数和浮点数之间转换;然而,其规则略有不同。尽管可以在相同大小的类型之间转换,如int/uint转换为float, long/ulong转换为double,但是也可以从long/ulong转换回float。这样做可能会丢失4个字节的数据,但这仅表示得到的float值比使用double得到的值精度低;编译器认为这是一种可以接受的错误,因为值的数量级不会受到影响。还可以将无符号的变量分配给有符号的变量,只要无符号变量值的大小在有符号变量的范围之内即可。
在隐式地转换值类型时,对于可空类型需要考虑其他因素:
● 可空类型隐式地转换为其他可空类型,应遵循表8-4中非可空类型的转换规则。即int?隐式地转换为long? 、float? 、double?和decimal?。
● 非可空类型隐式地转换为可空类型也遵循表8-5中的转换规则,即int隐式地转换为long? 、float? 、double?和decimal?。
● 可空类型不能隐式地转换为非可空类型,此时必须进行显式转换,如下一节所述。这是因为可空类型的值可以是null,但非可空类型不能表示这个值。
2.显式转换
有许多场合不能隐式地转换类型,否则编译器会报告错误。下面是不能进行隐式转换的一些场合:
● int转换为short——会丢失数据。
● int转换为uint——会丢失数据。
● uint转换为int——会丢失数据。
● float转换为int——会丢失小数点后面的所有数据。
● 任何数字类型转换为char——会丢失数据。
● decimal转换为任何数字类型——因为decimal类型的内部结构不同于整数和浮点数。
● int?转换为int——可空类型的值可以是null。
但是,可以使用类型强制转换(cast)显式地执行这些转换。在把一种类型强制转换为另一种类型时,有意地迫使编译器进行转换。类型强制转换的一般语法如下:
long val = 30000; int i = (int)val; // A valid cast. The maximum int is 2147483647
这表示,把强制转换的目标类型名放在要转换值之前的圆括号中。对于熟悉C的程序员,这是类型强制转换的典型语法。对于熟悉C++类型强制转换关键字(如static_cast)的程序员,这些关键字在C#中不存在,必须使用C风格的旧语法。
这种类型强制转换是一种比较危险的操作,即使在从long转换为int这样简单的类型强制转换过程中,如果原来long的值比int的最大值还大,就会出现问题:
long val = 3000000000; int i = (int)val; // An invalid cast. The maximum int is 2147483647
在本例中,不会报告错误,但也得不到期望的结果。如果运行上面的代码,并将输出结果存储在i中,则其值为:
-1294967296
最好假定显式类型强制转换不会给出希望的结果。如前所述,C#提供了一个checked运算符,使用它可以测试操作是否会导致算术溢出。使用checked运算符可以检查类型强制转换是否安全,如果不安全,就要迫使运行库抛出一个溢出异常:
long val = 3000000000; int i = checked((int)val);
记住,所有的显式类型强制转换都可能不安全,在应用程序中应包含代码来处理可能失败的类型强制转换。第14章将使用try和catch语句引入结构化异常处理。
使用类型强制转换可以把大多数基本数据类型从一种类型转换为另一种类型。例如,下面的代码给price加上0.5,再把结果强制转换为int:
double price = 25.30; int approximatePrice = (int)(price + 0.5);
这会把价格四舍五入为最接近的美元数。但在这个转换过程中,小数点后面的所有数据都会丢失。因此,如果要使用这个修改过的价格进行更多的计算,最好不要使用这种转换;如果要输出全部计算或部分计算的近似值,且不希望由于小数点后面的多位数据而麻烦用户,这种转换就很合适。
下面的例子说明了把无符号整数转换为char时会发生的情况:
ushort c = 43; char symbol = (char)c; WriteLine(symbol);
输出结果是ASCII码为43的字符,即“+”符号。可以尝试数字类型(包括char)之间的任何转换,这种转换是可行的,例如,把decimal转换为char,或把char转换为decimal。
值类型之间的转换并不仅限于孤立的变量。还可以把类型为double的数组元素转换为类型为int的结构成员变量:
struct ItemDetails { public string Description; public int ApproxPrice; } //.. double[] Prices = { 25.30, 26.20, 27.40, 30.00 }; ItemDetails id; id.Description = "Hello there."; id.ApproxPrice = (int)(Prices[0] + 0.5);
要把一个可空类型转换为非可空类型,或转换为另一个可空类型,并且其中可能会丢失数据,就必须使用显式的类型强制转换。甚至在底层基本类型相同的元素之间进行转换时,也要使用显式的类型强制转换,例如,int?转换为int,或float?转换为float。这是因为可空类型的值可以是null,而非可空类型不能表示这个值。只要可以在两种等价的非可空类型之间进行显式的类型强制转换,对应可空类型之间显式的类型强制转换就可以进行。但如果从可空类型强制转换为非可空类型,且变量的值是null,就会抛出InvalidOperationException异常。例如:
int? a = null; int b = (int)a; // Will throw exception
谨慎地使用显式的类型强制转换,就可以把简单值类型的任何实例转换为几乎任何其他类型。但在进行显式的类型转换时有一些限制,就值类型来说,只能在数字、char类型和enum类型之间转换。不能直接把布尔型强制转换为其他类型,也不能把其他类型转换为布尔型。
如果需要在数字和字符串之间转换,就可以使用.NET类库中提供的一些方法。Object类实现了一个ToString()方法,该方法在所有的.NET预定义类型中都进行了重写,并返回对象的字符串表示:
int i = 10; string s = i.ToString();
同样,如果需要分析一个字符串,以检索一个数字或布尔值,就可以使用所有预定义值类型都支持的Parse()方法:
string s = "100"; int i = int.Parse(s); WriteLine(i + 50); // Add 50 to prove it is really an int
注意,如果不能转换字符串(例如,要把字符串Hello转换为一个整数), Parse()方法就会通过抛出一个异常注册一个错误。第14章将介绍异常。
8.3.2 装箱和拆箱
第2章介绍了所有类型,包括简单的预定义类型(如int和char)和复杂类型(如从object类型中派生的类和结构)。这意味着可以像处理对象那样处理字面值:
string s = 10.ToString();
但是,C#数据类型可以分为在栈上分配内存的值类型和在托管堆上分配内存的引用类型。如果int不过是栈上一个4字节的值,该如何在它上面调用方法?
C#的实现方式是通过一个有点魔术性的方式,即装箱(boxing)。装箱和拆箱(unboxing)可以把值类型转换为引用类型,并把引用类型转换回值类型。这些操作包含在8.6节中,因为它们是基本的操作,即把值强制转换为object类型。装箱用于描述把一个值类型转换为引用类型。运行库会为堆上的对象创建一个临时的引用类型“箱子”。
该转换可以隐式地进行,如上面的例子所述。还可以显式地进行转换:
int myIntNumber = 20; object myObject = myIntNumber;
拆箱用于描述相反的过程,其中以前装箱的值类型强制转换回值类型。这里使用术语“强制转换”,是因为这种转换是显式进行的。其语法类似于前面的显式类型转换:
int myIntNumber = 20; object myObject = myIntNumber; // Box the int int mySecondNumber = (int)myObject; // Unbox it back into an int
只能对以前装箱的变量进行拆箱。当myObject不是装箱的int类型时,如果执行最后一行代码,就会在运行期间抛出一个运行时异常。
这里有一个警告:在拆箱时必须非常小心,确保得到的值变量有足够的空间存储拆箱的值中的所有字节。例如,C#的int类型只有32位,所以把long值(64位)拆箱为int时,会导致抛出一个InvalidCastException异常:
long myLongNumber = 333333423; object myObject = (object)myLongNumber; int myIntNumber = (int)myObject;