代码改变世界

5.1.1 checked和unchecked基元类型操作

2011-12-30 10:52  iRead  阅读(434)  评论(1编辑  收藏  举报

  对基元类型执行的许多算术运算都可能造成溢出:

  Byte b = 100;

  b = (Byte) (b+200) ; //b现在包含44(或者十六进制值2C)

重要提示:执行上述算术运算时,第一步要求所有操作数都扩大为32位值(或者64位值,如果任何操作数需要超过32位来表示的话)。所以,b和200(这两个值都不超过32位)首先转换成32位值,然后加到一起。结果是一个32位值(十进制300,或十六进制12C)。该值在存回变量b之前,必须转型为一个Byte。C#不会隐式执行这个转型操作,这正是第二行代码需要强制转换为Byte的原因。

  在大多数变成情形中,这种情悄悄发生的溢出是我们不希望的。如果没有检测到这种溢出,会导致应用程序行为失常。但在极少数编程情形中,比如计算一个哈希值或者校验和,这种溢出不仅是可以接受的,还是我们希望的。

  不同语言以不同方式处理溢出。C和C++不将溢出视为错误,并允许值回滚(wrap);应用程序将“若无其事”地运行。相反,Microsoft Visual Basic总是将溢出视为错误,并会在检测到溢出时抛出一个异常。

  CLR提供了一些特殊的IL指令,允许编译器选择它认为最恰当的行为。CLR有一个add指令,作用是将两个值加到一起,但不执行溢出检查。CLR还有一个add.ovf指令,作用也是将两个值加到一起,但会在发生溢出时抛出一个System.OverflowException异常。除了用于加法运算的这两个IL指令,CLR还为减、乘和数据转换提供了类似的IL指令,分别是sub/sub.ovf,mul/mul.ovf和conv/conv.ovf。

  C#允许程序员自己决定如何处理溢出。溢出检查默认是关闭的。也就是说,编译器在生成IL代码时,会自动使用加、减、乘以及转换指令的不含溢出检查的版本。这样的结果是代码能够更快地运行—但是,开发人员必须保证不会发生溢出,或者他们的代码能预见到这些溢出。

  让C#编译器控制溢出的一个办法是使用/checked+编译器开关。这个开关指示编译器在生成代码时,使用加、减、乘、除和转换指令的溢出检查版本。这样生成的代码在执行时会稍慢一些,因为CLR会检查这些运算,判断是否会发生溢出。如果发生溢出,CLR会抛出一个OverflowException异常。

  除了全局性地打开或关闭溢出检查,程序员还可在代码的特定区域控制溢出检查。C#通过提供checked和unchecked操作符来实现这种灵活性。下面是一个使用了unchecked操作符的例子:

  UInt32 invalid = unchecked ((UInt32)(-1));                //OK

  下例则使用了checked操作符:

  Byte b = 100;

  b = checked((Byte)(b+200));              //抛出OverflowException异常

  在这个例子中,b和200首先转换成32位值,然后加到一起,结果是300.然后,因为显式转型的存在,300被转换成一个Byte,这造成一个OverflowException异常。如果Byte是在checked操作符外部转型的,则不会发生异常:

  b = (Byte) checked(b+200);      //b包含44;不会抛出OverflowException异常

  除了checked和unchecked操作符,C#还支持checked和unchecked语句,它们造成一个块中的所有表达式进行或不进行溢出检查:

  checked{                                       //开始一个checked块

    Byte b = 100;

    b = (Byte) (b+200);            //该表达式会进行溢出检查

  }                                                       //结束一个checked块

  事实上,如果使用了一个checked语句块,就可以将+=操作符用于Byte,从而稍微简化以下代码:

  checked{                              //开始一个checked块

    Byte b = 100;            

    b += 200;                    //该表达式会进行溢出检查

  }                                             //结束一个checked块

重要提示:由于checked操作符和checked语句唯一的作用就是决定生成哪一个版本的加、减、乘和数据转换IL指令,所以在一个checked操作符或者语句中调用一个方法,不会对该方法造成任何影响,如下例所示:

  checked{

    //假定SomeMethod试图把400加载到一个Byte中

    SomeMethod(400);

    //SomeMethod可能会、也可能不会抛出一个OverflowException异常

    //如果SomeMethod使用checked指令来编译,就会抛出异常

    //但这和当前的checked语句无关

  }

  根据我的经验,许多计算都会产生令人吃惊的结果。这一般是由于无效的用户输入造成的,但也可能是由于系统的某个部分返回了程序员没有预料到的值。所以,我向程序员做出以下建议:

  • 尽量使用有符号数值类型(比如Int32和Int64),而不要使用无符号数值类型(比如UInt32和UInt64)。这允许编译器检测更多的上溢/下溢错误。除此之外,类库的多个部分(比如Array和String的Length属性)被硬编码为返回有符号的值。这样一来,在代码中四处移动这些值时,需要进行的强制类型转换就少了。较少的强制类型转换使代码更简洁,更容易维护。除此之外,无符号的数值类型是不相容于CLS的(不符合CLS的要求)。
  • 写代码时,如果代码可能发生你不希望的溢出(可能是因为无效的输入数据而发生的,比如需要使用由最终用户或客户机提供的数据来处理一个请求),就把这些代码放到一个checked块中。同时还应捕捉OverflowException,从容地从错误中恢复。
  • 写代码时,将允许发生溢出的代码显式放到一个unchecked块中,比如在计算一个校验和(checksum)的时候。
  • 对于没有使用checked和unchecked的任何代码,都假定你希望在发生溢出时抛出一个异常,比如在输入是已知的前提下计算一些东西(比如质数),此时的溢出应被记为bug。

  现在,开发应用程序时,请打开编译器的/checked+开关来进行调试性生成。这样一来,系统就会对没有显式标记checked或unchecked的代码进行溢出检查,所以应用程序运行起来会慢一些。此时一旦发生异常,就可以轻松检测到它,而且能及时地修正代码中的bug。但是,为了正式发布而生成应用程序时,应使用编译器的/checked-开关,确保代码能够更快地运行,不会生成溢出异常。要在Microsoft Visual Studio中更改checked设置,请打开项目的属性对话框,选择“生成”选项卡,单击“高级”,再勾选“检查运算上溢/下溢”,如图5-1所示。

图5-1 在Visual Studio的“高级生成设置”对话框中指定编译器是否检查溢出

  如果应用程序能容忍始终执行checked运算所造成的些许性能损失,那么建议即使是为了发布而生成应用程序,也用/checked命名行开关来编译,这样可防止应用程序在包含以损坏的数据(甚至可能有安全漏洞)的前提下继续运行。例如,通过乘法运算来计算一个数组的索引时,相较于因为数学运算的“回滚”而访问一个不正确的数组元素,更好的做法是抛出一个OverflowException异常。

重要提示:

  System.Decimal类型是一个非常特殊的类型。虽然许多编程语言(包括C#和Visual Basic)都将Decimal视为一个基元类型,但CLR则不然。这意味着CLR没有相应的IL指令来决定如何处理一个Decimal值。在.NET Framework SDK文档中查看Decimal类型可以看出,它提供了一系列public static方法,包括AddS,Substract,Multiply,Divide等。除此之外,Decimal类型还为+,-,*,/等提供了操作符重载方法。

  编译使用了Decimal值的程序时,编译器会生成代码来调用Decimal成员,并通过这些成员来执行实际的运算。这意味着Decimal值的处理速度慢于CLR基元类型的值的处理速度。另外,由于没有相应的IL指令来处理Decimal值,所以checked和unchecked操作符、语句以及编译器开关都失去了效用。如果对Decimal值执行的运算是不安全的,肯定会抛出一个OverflowException异常。

  类似地,System.Numerics.BigInteger类型也在内部使用一个UInt32数组来表示一个任意大的整数,它的值没有上限和下限。因此,对BigInteger执行的运算永远不会造成OverflowException异常。然而,如果值太大,而且没有足够多的内存来改变数组的大小,对BigInteger的运算可能会抛出一个OutOfMemoryException异常。