银河

SKYIV STUDIO

  博客园 :: 首页 :: 博问 :: 闪存 :: :: :: 订阅 订阅 :: 管理 ::

谜题33: 循环者遇到了狼人
    请提供一个对i声明,将下面的循环转变为无限循环。
    while (i != 0 && i == -i)
    {
    }

解惑33: 循环者遇到了狼人
    这仍然是一个循环。在布尔表达式(i != 0 && i == -i)中,一元减号操作符作用于i,意味着它的类型必须是数字的:一元减号操作符作用于一个非数字预定义类型操作数是非法的。因此,我们要寻找一个非0的数字类型数值,它等于自己的负值。NaN不能满足这个属性,因为它不等于任何数值,因此,i必须表示一个实际的数字。确定没有任何数字满足这样的属性吗?
    嗯,没有任何实数具有这种属性,但是没有任何一种C#数字类型能够对实数进行完美建模。浮点数值是用一个符号位、一个被通俗地称为尾数(mantissa)的有效数字以及一个指数来表示的。除了0之外,没有任何浮点数等于其符号位取反之后的值,因此,i的类型必然是整数的。
    有符号的整数类型使用2的补码算术运算:为了取得一个数值的负值,要对其每一位取反,然后加1,从而得到结果。2的补码算术运算的一个很大优势是,0具有唯一的表示形式。如果要对int数值0取负值,将得到0xffffffff+1,它仍然是0。但是,这也有一个相应的缺点。总共存在偶数个int数值——准确地说有232个,其中一个用来表示0,剩下奇数个int数值来表示正整数和负整数,这意味着正的和负的int数值的数量必然不相等。换句话说,这暗示着至少有一个int数值,其负值不能正确地表示为int数值。
    事实上,恰恰就有一个这样的int数值,它就是int.MinValue,即-231。它的十六进制表示是0x80000000。其符号位为1,其余所有的位都是0。如果我们对这个值取负值,将得到0x7fffffff+1,也就是0x80000000,即int.MinValue!换句话说,int.MinValue是它自己的负值,long.MinValue也是一样[C#语言规范 7.6.2]。对这两个值取负值将产生溢出,但是C#在整数计算(unchecked上下文)中忽略了溢出。其结果已经阐述清楚了,即使它们并不总是你所期望的。
    下面的声明将使得布尔表达式(i != 0 && i == -i)的计算结果为true,从而使循环无限循环下去:
    int i = int.MinValue;
    下面这个也可以:
    long i = long.MinValue;
    如果你对取模运算很熟悉,那么有必要指出,也可以用代数方法解决这个谜题。C#的int算术运算是实际的算术运算对232取模,因此本谜题需要一个对这种线性全等的非零解决方案:
    i ≡ -i(mod 232)
    在恒等式的两边加i,可以得到:
    2i ≡ 0(mod 232)
    对这种全等的非零解决方案就是i = 231。尽管这个值不能表示成int,但是它和-231是全等的,即与int.MinValue全等。
    总之,C#使用2的补码的算术运算,是不对称的。对于每一种有符号的整数类型(int、long、sbyte和short),负的数值总是比正的数值多一个,这个多出来的值总是这种类型所能表示的最小数值。对int.MinValue取负值不会改变它的值,long.MinValue也是如此。对short.MinValue取负值并将所产生的int数值转型回short,返回的同样是最初的值(short.MinValue)。对sbyte.MinValue来说,也会产生相似的结果。更一般地讲,千万要当心溢出:就像狼人一样,它是个杀手。
    对语言设计者的教训与谜题26中的教训一样。考虑对某种不会悄悄发生溢出的整数算术运算形式提供语言级的支持。
    (注:在C#的checked上下文中将进行溢出检查[C#语言规范 7.5.12])


    C#解惑总目录

posted on 2006-09-02 18:53  银河  阅读(1126)  评论(1编辑  收藏  举报