2009年11月2日 星期一

Signed不Signed還是問題所在 之 C# Remix 版

每個男人都想當慣C,可是在2009年的今天也有不少男人不想當慣C,想當慣西夏(C#)也說不定,跟C和C++比起來,C#是個純物件導向的程式語言,在Everything is object的設計前提上,這些Signed 不Signed 的問題當然使用者不是很想去沾惹,總是希望Framework能把事情處理的很好,這樣王子和公主就能過這幸福快樂的日子了。在這個那麼美好的世界當中,會不會有那些令人惱怒的轉換規則呢?在前篇中提到的C 語言中Signed Type 跟 Unsigned Type 容易造成的誤會和運算轉換的規則,在現行的純物件導向語言當中,轉換的規則、Signed Type 跟 Unsigned Type 之間的交互作用,又全然是另外一個故事了,本篇以C#為例來探討一下所謂的新世紀Signed 問題。

當然這整件事情的緣起,是在某朋友的部落閣當中,看到了一個程式。這個程式這這樣寫的。

unsigned int i=1;
int j=-1;
if(i>j) printf("i>j");
else printf(“j>i");

慣C的人看到一定馬上就看出問題所在,大聲說出:「閃開,讓專業的來」,或是「放開那女孩」,然後新警察就會出來說,依造前篇的規則,這個轉換會根據C99 Spec中的6.3.1.8的Rule 3(a),得到 "j>i"這個慣C的人看起來很自我感覺良好,可是大家感覺卻不太好的結果。有沒有念過國小的人都知道1是比-1來得大,不過"數學是數學","數字要在程式中表現出來"就是會有這樣的結果,數值的表示本來就是有極限,所以才需要訂規則消除這種模糊的狀況,這個應該是程式工程師應該小心的,這是他們的責任。不過把這個看起來詭異的程式拿去C語言的後繼者,也是2000之後才出現的新世紀語言C#去做驗證,卻得到了看起來正常,卻沒有那麼正常的答案"i>j"

看起來正常是法理上是"i>j" 應該,"j>i"會是悲哀,可是如果仔細去想的話,這樣的結果又會令人覺得奇怪,畢竟在C#中什麼都是物件,那個運算子'=='其實會對應到某物件的成員函式,那現在 i 跟 j 的型態並不一樣,那到底是哪個比較函式會被叫起來呢?不過對程式語言稍有理解的人其實會了解,這樣的情境下你的編譯器勢必會幫你動一些手腳,如果你不懂你的編譯器幫你做了些什麼,那可能會是個災難。一個看起來合理的想法可是卻是錯誤的想法就是編譯器會幫你把unsigned int i (C# 中 uint i) 會被轉成 Signed Type,讓這個轉換看起來會得到這樣的結果,可是C#是個號稱自己是個強型態的語言,除了隱性轉換之外(所謂的隱性轉換就是設計者覺得合理的轉換(笑)),使用者是必須在程式碼顯性的說出:「我就是要這個轉換。」然而unsigned int i 轉 int 是會丟失資料的,這樣一個轉換的確不該是一個隱性轉換。會設計成這樣就是不希望使用者管這問題,如果是顯性轉換的話,其實使用者是在意了。

另外一個合理的實作是把每個基本形態的比較運算子成員函式做不同型態的比較,這樣的話unsigned int 就能正確的跟int 做相比了,不過這樣的排列組合會挺多的,所以並不是一個很好的解法。另外一個合理的轉換方式就是把unsigned int i 跟 int j 都轉成64bit int 再來進行比較,這樣就可以得到合理的結果了,事實上C#的Language Specification中是使用這種解法的,所以C#會得到"i>j"也非常正常。其實就如同其他的語言,不同型態的運算難免不了要做 promotion,只是這樣的作法就見仁見智了。最後,這部份的轉換是定義在C# Spec中的 7.2.6 中,其中的第二節Binary numeric promotions就是在這個範例程式碼當中,C#編譯器所套用的轉換規則。