數據類型
遵禮謂之劬,守法謂之固,此荒國之風也 ——《荀悅·申鑒》
待教室平靜下來,冒號再度開腔:“在談論動態語言之前,最好先澄清一下它與動態類型語言之間的區別。”
歎號訝然道:“它們不是一回事嗎?一直以為動態語言是動態類型語言的簡稱呢。”
“有親戚之名,卻無血緣之親。名稱上相似,加之動態語言絕大多數確是動態類型語言,造成混淆實屬在所難免,但二者之間並無必然聯系——動態語言不一定是動態類型語言,動態類型語言也不一定是動態語言。”冒號飛跑的舌頭幾乎絆蒜,同時把眾人的腦子攪成了一鍋粥。
見勢不妙,冒號改用迂回戰術:“我們不妨再談開些,大家對數據類型是如何理解的?”
逗號隨口道:“數據類型不就是數據的種類嗎?”
眾人暗笑:說了跟沒說差不多。
冒號說道:“數據類型包含兩個要素:一個是允許取值的集合,一個是允許參與的運算。例如int類型在Java中既定義了介於− 231和231 − 1之間的整數集合,也定義了該集合上的整數所能進行的運算。現在的問題是:數據類型的意義何在?”
句號回答:“限定一個變量的數據類型,就意味著限制了該變量的取值范圍和所參與的運算,這從一定程度上保證了代碼的安全性。”
冒號追問:“還有嗎?”
句號略作思考後說:“用戶自定義的數據類型,如C中的結構和Java中的類或接口,賦予數據以邏輯內涵,提高了代碼的抽象性。”
“精辟!”冒號贊道,“數據類型既有針對機器的物理意義,又有針對人的邏輯意義。前者用於進行底層的內存分配和數值運算等,後者用於表達高層的邏輯概念。既然類型如此重要,類型檢查就必不可少了。所謂動態類型語言(Dynamic Typing Language),正是指類型檢查發生在運行期間(run-time)的語言。”
“那靜態類型語言(Static Typing Language)自然是類型檢查發生在編譯期間(compile-time)的語言咯。”引號接話道。
冒號回應:“一般的說法是這樣,不過我更願意將‘編譯期間’四個字改為‘運行之前’,否則容易讓人誤解為靜態類型語言一定是編譯型語言(Compiled Language)。”
問號問道:“是否可以這麼說:靜態類型語言需要變量申明,而動態類型語言則不需要?”
“這話只對了一半。”冒號評論,“動態類型語言固然不需要顯式的變量申明(Explicit Declaration),一些靜態類型語言有時也不需要。典型的如ML、Haskell之類的函數式語言,編譯器可以通過上下文來進行類型推斷(type inference)。”
歎號感慨:“動態類型語言不必申明變量,甚至一個變量在不同地方可以代表不同類型,多省事多方便啊!”
冒號微微颔首:“動態類型語言的確有簡明快捷的優勢,並且天然具有泛型(generic)特征,代碼更加靈活。比如,動態類型有一種被稱作鴨子類型(Duck Typing)的形式。”
逗號感到有趣:“鴨子類型?很滑稽的名字。”
“這種類型的思想是:如果一個對象既會走鴨步又會呷呷叫,何妨將其視作鴨子呢?”冒號說著投影出一段Ruby代碼——
class Duck #會叫會游的鴨 def shout puts '呷呷呷' end def swim puts '鴨泳' end end class Frog #會叫會游的蛙 def shout puts '呱呱呱' end def swim puts '蛙泳' end end def shoutAndSwim(duck) #讓一只會叫會游的家伙邊叫邊游 duck.shout duck.swim end shoutAndSwim(Duck.new) #讓一只鴨邊叫邊游 shoutAndSwim(Frog.new) #讓一只蛙邊叫邊游
冒號繼續講解:“在Smalltalk、Python和Ruby等動態類型的OO語言中,只要一個類型具有shout和swim的方法,它就可以為shoutAndSwim所接受。在Java這種靜態類型語言中是不可能的,除非鴨和蛙在同一繼承樹上,或者二者均顯式實現了一個包含shout和swim的公用接口。”
句號敏銳地指出:“C++是靜態類型語言,但它的模板也可實現類似功能,並不需要引入繼承關系。”
“非常正確!但請接著看下去。”冒號又放出一段投影——
class Cock #會叫不會游的雞 def shout puts '喔喔喔' end end class Fish #會游不會叫的魚 def swim puts '自由泳' end end def shoutOrSwim(duck, flag) #讓一只會叫或會游的家伙叫或游 flag ? duck.shout : duck.swim end shoutOrSwim(Cock.new, true) #讓一只雞叫 shoutOrSwim(Fish.new, false) #讓一只魚游
“這裡雞沒有swim的方法,魚沒有shout的方法。若采用C++的模板,shoutOrSwim是無法通過編譯的。但在支持Duck 類型的語言中,只要在運行期間不讓雞swim、讓魚shout——除非你突發奇想——一切平安無事。”冒號作了個OK的手勢。
“動態類型語言真是越看越可愛。”歎號簡直垂涎欲滴了。
“Duck類型為軟件重用開啟了新的窗口,但凡事都是一分為二的。由於Duck類型的接口組合是隱性的,其使用者需要比普通Interface更小心以避免誤用;其維護者也需要更小心以避免破壞客戶代碼;此外它也可能造成濫用——將會叫會游的東西放進池塘似乎不算壞主意,但如果一艘輪船趁機也開了進來,恐怕就不那麼美妙了。”
眾皆莞爾。
“再來看看靜態類型語言的好處:由於在運行之前進行了類型檢查,一方面代碼的可靠性增強,另一方面編譯器有可能藉此優化機器代碼以提高運行效率,同時相比前者節省了運行期的類型檢查時間。此外,變量類型的聲明表明了編程者的意圖,有輔助文檔的功效。”冒號解釋著,“兩種類型的體制可以用兩種法律原則來類比:靜態類型檢查類似‘疑罪從有’的有罪推定制——在被證明合法之前是非法的,動態類型檢查類似‘疑罪從無’的無罪推定制——在被證明非法之前是合法的。至於如何取捨,套用一句話:‘Static Typing Where Possible, Dynamic Typing When Needed’。”
問號提出新問題:“動態類型語言與弱類型語言有何不同?”
冒號喟言:“它們也常常被混為一談,但類型的動靜與強弱完全是正交的兩個概念。前者以類型的綁定(binding)時間來劃分,後者以類型的剛性強度來劃分。通常弱類型語言(Weakly-typed Language)允許一種類型的值隱性轉化為另一種類型,而強類型語言(Strongly-typed Language)則不允許。舉個例子,1+"2"在VB中等於3——第二個字符串轉化為整數;在Javascript中等於"12"——第一個整數轉化為字符串;在C中則等於一個不定的整數值——第二個字符串作為地址來運算。這樣似乎很有趣很方便,但程序容易藏污納垢,滋生臭蟲(bug)。”
引號想起:“好像還有一種所謂的類型安全語言?”
逗號緊緊抱著頭,仿佛害怕裂開。
“類型按安全性來劃分,可分為類型安全語言(Type-safe Language)和類型不安全語言(Type-unsafe Language)。一般認為強類型語言是類型安全的,弱類型語言是類型不安全的。”冒號應道,“至此,我們已論及數據類型的三種劃分方式。值得一提的是,這些劃分並非泾渭分明的,更多的是定性而非定量的描述,甚至沒有公認統一的定義。但了解它們,對我們日後的學習是有所裨益的。”