今早看到了《你真的瞭解a ^= (b ^= (a ^= b))嗎?》一文,文章中提到了一個有趣的運算式:a ^= (b ^= (a ^= b)); 很簡單的一個程式:
1 int a = 2;
2 int b = 9;
3 a ^= (b ^= (a ^= b));
4 Console.WriteLine("{0}, {1}", a.ToString(), b.ToString());
最後的運算結果不是預期的9,2,而是0,2。同時,還有1個運算式:
1 int i=0;
2 int j=(i++)+(i++);
3 j=?
j的值是多少?2嗎?不是,正確的結果是1。
為啥會有這樣的結果,難道是編譯器的bug嗎?其實不然,編寫編譯器的人可不是數學白癡,是我們對電腦的理解有誤。
C#中,運算式的計算遵循一個規律:從左至右依次計算。
例如,Z= X operation Y,電腦依次計算X,Y的值,然後OperationX和Y,最後賦值給Z。
同理,Z = X operation1 ( X operation2 Y),電腦則把(X operation2 Y)當做一個變數Temp。此時Z = X operation1 Temp。然後電腦再計算Temp運算式,並壓棧,計算完Temp運算式後,將結果出棧。這是個遞迴過程。注意:這個過程中,X與Temp是兩個並列的運算式,如果Temp中有對X進行再操作,並不會影響X的結果。因此運算式規律是從左至右依次計算,X在左,已經計算完畢。如果是Z = Temp operation1 X,那麼Temp中對X的操作將會影響到X本身。
OK,理論分析完畢,讓我們來實踐一下為什麼上面2個運算式的結果令人詫異。
對於第一個運算式:a ^= (b ^= (a ^= b))。先將運算子分解:a = a ^ (b = b ^ (a = a ^ b)); 這樣看起來更清晰。
1. 電腦分解運算式為 a = a ^ Temp1。計算左邊的a,a本身是一個變數,因此a用2來代替。接下來計算右邊的Temp1運算式,發現其中比較複雜,無法直接用值來代替,因此開始對Temp1堆棧計算。
2. Temp1 是 b = b ^ (a = a ^ b)。計算左邊,b是變數,可以直接賦值為9。右邊是運算式,用Temp2代替。
3. Temp2 是 a = a ^ b。計算左邊,a是變數,替換為2; 右邊,b也是變數,替換為9。並計算出Temp2=2 ^ 9 = 11。
4. 迴歸計算Temp 1 = 9 ^ Temp2 = 9 ^ 11 = 2,也就是b = 2。
5. 迴歸計算a = 2 ^ Temp1 = 2 ^ 2 = 0。也就是a = 0。
OK,這樣0, 2結果就出現了。
對於第二個運算式: j = (i++) + (i++);。
1. 先計算左邊的運算式,i++,因為是尾碼,所以(i++)運算式返回i,此時i = 0, 意味著返回0。
2. 執行++運算子,0++就是1,因此i = 1;
3. 計算右邊的運算式(i++),也是尾碼,因此返回i ,此時 i = 1,意味著返回1;
4. 執行++運算子,1++就是2,因此i = 2;
5 賦值給j. j = 0 + 1 = 1。
綜上,其實不是什麼編譯器的bug,而是電腦的做法永遠是死腦筋的。
還有,實際項目中千萬不要出現這樣的運算式,寧可多寫幾行,也不要堆在一行,我們一定要遵循《代碼規範》。這也是為了提高代碼的可閱讀性和可維護性。
最後,給大家出個題目:
int i = 0; int j = (i--) + ((i++) + (++i)); 計算結果i 和 j 分別是多少?