謎題27: 變幻莫測的i值
你的任務仍舊是要指出這個程式將列印什麼。
class Shifty
{
static void Main()
{
int i = 0;
while (-1 << i != 0)
i++;
System.Console.WriteLine(i);
}
}
解惑27: 變幻莫測的i值
常量-1是所有32位都被置位的int數值(0xffffffff)。左移操作符將0移入到由移位所空出的右邊最低位,因此運算式(-1 << i)將最右邊的i位設定為0,並保持其餘的32-i位為1。很明顯,這個迴圈將完成32次迭代,因為(-1 << i)對任何小於32的i來說都不等於0。你可能期望在i等於32時終止條件測試返回false,從而使程式列印32,但是它列印的並不是32。實際上,它不會列印任何東西,而是進入了一個無限迴圈。
問題在於(-1 << 32)等於-1而不是0,因為移位操作符只使用其右運算元的低5位作為移位長度。或者是低6位,如果其左運算元是一個long類型數值[C#語言規範 7.8]。這條規則作用於全部的兩個移位操作符:<<和>>。移位長度總是介於0到31之間,如果左運算元是long類型,則介於0到63之間。這個長度是對32取餘的,如果左運算元是long類型的,則對64取餘。如果試圖對一個int數值移位32位,或者是對一個long數值移位64位,都只能返回這個數值本身。沒有任何移位長度可以讓一個int數值丟棄其所有的32位,或者是讓一個long數值丟棄其所有的64位。
幸運的是,有一個非常容易的方法能夠修正該問題。我們不是讓-1重複地移位不同的移位長度,而是將前一次移位操作的結果儲存起來,並且讓它在每一次迭代時都向左再移1位。下面這個版本的程式就可以列印我們所期望的32:
class Shifty
{
static void Main()
{
int distance = 0;
for (int val = -1; val != 0; val <<= 1)
distance++;
System.Console.WriteLine(distance);
}
}
這個修正過的程式說明了一條普遍的原則:如果可能的話,移位長度應該是常量。如果移位長度緊盯著你不放,那麼你讓其值超過31,或者如果左運算元是long類型的,讓其值超過63的可能性就會大大降低。當然,並不總是可以使用常量的移位長度。當必須使用一個非常量的移位長度時,請確保你的程式可以應付這種容易產生問題的情況,或者根本不會碰到這種情況。
前面提到的移位操作符的行為還有另外一個令人震驚的結果。很多程式員都希望具有負移位長度的右移操作符可以起到左移操作符的作用,反之亦然。但是情況並非如此。右移操作符總是起到右移的作用,而左移操作符也總是起到左移的作用。負的移位長度通過只保留低5位而去除其他位的方式被轉換成了正的移位長度——如果左運算元是long類型的,則保留低6位。因此,如果要將一個int數值左移,其移位長度為-1,那麼移位的效果是它被左移了31位。
總之,移位長度是對32取餘的,或者如果左運算元是long類型的,則對64取餘。因此,使用任何移位操作符和移位長度,都不可能將一個數值的所有位全部移走。同時,我們也不可能用右移操作符來執行左移操作,反之亦然。如果可能的話,請使用常量的移位長度,如果移位長度不能設為常量,那麼就要千萬小心。
語言設計者可能應該考慮將移位長度限制在從0到以位為單位的類型長度的範圍內,並且修改移位長度為類型長度時的語義,讓其返回0。儘管這可以避免在本謎題中所展示的混亂情況,但是它可能會帶來負面的執行結果,因為C#的移位操作符的語義正是許多處理器上的移位指令語義。
C#解惑總目錄