謎題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#解惑總目錄