
下面的程式使用了複合的異或指派操作符,它所展示的技術是一種程式設計習俗。那麼它會列印出什麼呢?
就像其名稱所暗示的,這個程式應該交換變量 x 和 y 的值。如果你運作它,就會發現很悲慘,它失敗了,列印的是:
x = 0; y = 1984。
交換兩個變量的最顯而易見的方式是使用一個臨時變量:
int tmp = x;
x = y;
y = tmp;
很久以前,當中央處理器隻有少數寄存器時,人們發現可以通過利用異或操作符(^)的屬性(x ^ y ^ x) == y 來避免使用臨時變量:
x = x ^ y;
y = y ^ x;
x = y ^ x;
這個慣用法曾經在 C 程式設計語言中被使用過,并進一步被建構到了 C++中,但是它并不保證在二者中都可以正确運作。但是有一點是肯定的,那就是它在 Java 中肯定是不能正确運作的。
Java 語言規範描述到:操作符的操作數是從左向右求值的。為了求表達式 x ^= expr 的值,x 的值是在計算 expr 之前被提取的,并且這兩個值的異或結果被賦給變量 x。在 CleverSwap 程式中,變量 x 的值被提取了兩次——每次在表達式中出現時都提取一次——但是兩次提取都發生在所有的指派操作之前。
下面的代碼段詳細地描述了将互換慣用法分解開之後的行為,并且解釋了為什麼産生的是我們所看到的輸出:
// Java 中 x^= y^= x^= y 的實際行為
int tmp1 = x ; // x 在表達式中第一次出現
int tmp2 = y ; // y 的第一次出現
int tmp3 = x ^ y ; // 計算 x ^ y
x = tmp3 ; // 最後一個指派:存儲 x ^ y 到 x
y = tmp2 ^ tmp3 ; // 第二個指派:存儲最初的 x 值到 y 中
x = tmp1 ^ y ; // 第一個指派:存儲 0 到 x 中
在 C 和 C++中,并沒有指定表達式的計算順序。當編譯表達式 x ^= expr 時,許多 C 和 C++編譯器都是在計算 expr 之後才提取 x 的值的,這就使得上述的慣用法可以正常運轉。
盡管它可以正常運轉,但是它仍然違背了 C/C++有關不能在兩個連續的序列點之間重複修改變量的規則。是以,這個慣用法的行為在 C 和 C++中也沒有明确定義。
為了看重其價值,我們還是可以寫出不用臨時變量就可以互換兩個變量内容的 Java 表達式的。但是它同樣是醜陋而無用的:
// 殺雞用牛刀的做法,千萬不要這麼做!
y = (x^= (y^= x))^ y ;
這個教訓很簡單:在單個的表達式中不要對相同的變量指派兩次。表達式如果包含對相同變量的多次指派,就會引起混亂,并且很少能夠執行你希望的操作。即使對多個變量進行指派也很容易出錯。更一般地講,要避免所謂聰明的程式設計技巧。
它們都是易于産生 bug 的,很難以維護,并且運作速度經常是比它們所替代掉的簡單直覺的代碼要慢。
語言設計者可能會考慮禁止在一個表達式中對相同的變量多次指派,但是在一般的情況下,強制執行這條禁令會因為别名機制的存在而顯得很不靈活。例如,請考慮表達式 x = a[i]++ - a[j]++,它是否遞增了相同的變量兩次呢?這取決于在表達式被計算時 i 和 j 的值,并且編譯器通常是無法确定這一點。