冒號和他的學生們
——程序員提高班紀事
- 數據類型
遵禮謂之劬,守法謂之固,此荒國之風也 ——《荀悅·申鑒》
待教室平靜下來,冒號再度開腔:“在談論動態語言之前,最好先澄清一下它與動態類型語言之間的區別。”
嘆號訝然道:“它們不是一回事嗎?一直以為動態語言是動態類型語言的簡稱呢。”
“有親戚之名,卻無血緣之親。名稱上相似,加之動態語言絕大多數確是動態類型語言,造成混淆實屬在所難免,但二者之間并無必然聯系——動態語言不一定是動態類型語言,動態類型語言也不一定是動態語言。”冒號飛跑的舌頭幾乎絆蒜,同時把眾人的腦子攪成了一鍋粥。
見勢不妙,冒號改用迂回戰術:“我們不妨再談開些,大家對數據類型是如何理解的?”
逗號隨口道:“數據類型不就是數據的種類嗎?”
眾人暗笑:說了跟沒說差不多。
冒號說道:“數據類型包含兩個要素:一個是允許取值的集合,一個是允許參與的運算。例如int類型在Java中既定義了介于− 231 和231 − 1之間的整數集合,也定義了該集合上的整數所能進行的運算?,F在的問題是:數據類型的意義何在?”
句號回答:“限定一個變量的數據類型,就意味著限制了該變量的取值范圍和所參與的運算,這從一定程度上保證了代碼的安全性。”
冒號追問:“還有嗎?”
句號略作思考后說:“用戶自定義的數據類型,如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)。一般認為強類型語言是類型安全的,弱類型語言是類型不安全的。”冒號應道,“至此,我們已論及數據類型的三種劃分方式。值得一提的是,這些劃分并非涇渭分明的,更多的是定性而非定量的描述,甚至沒有公認統一的定義。但了解它們,對我們日后的學習是有所裨益的。”