第五課 語言小談(2)
5.2數據類型——規則與變通
操縱於規矩之中,神明於規矩之外 ——《俞震·古今醫案按》
關鍵詞:數據類型,靜態類型,動態類型,Duck類型,強類型,弱類型,類型安全
摘要:關於數據類型的討論
!預覽
·Duck類型的哲學是:是什麼不重要,重要的是能干什麼
·將一個會叫會游的家伙放進池塘看起來不算壞主意,但如果一艘輪船趁機也轟隆隆地開了進來,事情恐怕就不那麼美妙了
·靜態類型檢查類似“疑罪從有”的有罪推定制,動態類型檢查類似“疑罪從無”的無罪推定制
·盡可能守規則,必要時求變通
·規則如褲帶,過於寬松和過於束縛都不好
?提問
·動態語言與動態類型語言是一回事嗎?
·數據類型有哪兩個要素?其意義何在?
·什麼是動態類型和靜態類型?它們的區別是什麼?各有什麼優缺點?
·什麼是鴨子類型(duck typing)?它有什麼優缺點?
·什麼是強類型與弱類型?什麼是類型安全的?
:講解
待教室平靜下來,冒號再度開腔:“在談論動態語言之前,最好先澄清一下它與動態類型語言之間的區別。”
歎號訝然道:“它們不是一回事嗎?一直以為動態語言是動態類型語言的簡稱呢。”
“有親戚之名,卻無血緣之親。名稱上相似,加之動態語言絕大多數確是動態類型語言,造成混淆實屬在所難免,但二者之間並無必然聯系——動態語言不一定是動態類型語言[1],動態類型語言也不一定是動態語言[2]。”冒號飛跑的舌頭幾乎絆蒜,同時把眾人的腦子攪成了一鍋粥。
見勢不妙,冒號改用迂回戰術:“我們不妨再談開些,大家對數據類型是如何理解的?”
逗號隨口道:“數據類型不就是數據的種類嗎?”
眾人暗笑:說了跟沒說差不多。
冒號說道:“數據類型包含兩個要素:一個是允許取值的集合,一個是允許參與的運算。例如int類型在Java中既定義了介於− 231和231 − 1之間的整數集合,也定義了該集合上的整數所能進行的運算。現在的問題是:數據類型的意義何在?”
句號回答:“限定一個變量的數據類型,就意味著限制了該變量的取值范圍和所參與的運算,這從一定程度上保證了代碼的安全性。”
冒號追問:“還有嗎?”
句號略作思考後說:“用戶自定義的數據類型,如C中的結構和Java中的類或接口,賦予數據以邏輯內涵,提高了代碼的抽象性。”
“精辟!”冒號贊道,“數據類型既有針對機器的物理意義,又有針對人的邏輯意義。前者用於進行底層的內存分配和數值運算等,後者用於表達高層的邏輯概念。既然類型如此重要,類型檢查就必不可少了[3]。所謂動態類型語言(dynamic typing language),正是指類型檢查發生在運行期間(run-time)的語言。”
“那靜態類型語言(static typing language)自然是類型檢查發生在編譯期間(compile-time)的語言咯。”引號接話道。
冒號回應:“一般的說法是這樣,但我更願意將‘編譯期間’四個字改為‘運行之前’,否則容易讓人誤解為靜態類型語言一定是編譯型語言(compiled language)。”
問號問道:“是否可以這麼說:靜態類型語言需要變量聲明,而動態類型語言則不需要?”
“這話只對了一半。”冒號評論,“動態類型語言固然不需要顯式的變量聲明(explicit declaration),一些靜態類型語言有時也不需要。典型的如ML、Haskell之類的函數式語言,編譯器可以通過上下文來進行類型推斷(type inference)。”
“如何進行類型推斷?”問號有點丈二和尚摸不著頭腦。
冒號打了個比方:“假設‘+’號只限於同類型的數據運算,那麼從表達式a + 1中可以推出a是整型變量,從b + 1.0中推出b是浮點型變量,從c + “1”中推出c是字符串型變量。這些變量不必事先聲明,但一旦類型被推斷確定後,便不再更改。由於這些推斷都是在程序運行之前進行的,因此仍屬於靜態類型。它既有動態類型的簡潔性,又不失聲明式靜態類型的安全性,可謂裁長補短啊。”
歎號有些羨慕地說:“還是動態類型語言好,不僅不必聲明變量,而且一個變量在不同地方還可以代表不同類型,多省事多方便啊!”
冒號微微颔首:“雖然這種機制也有為人诟病之處,但不可否認,動態類型語言的確有它的優勢:簡明、快捷、靈活,並且天然具有泛型(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等動態類型的OOP語言中,只要一個類型具有shout和swim的方法,它就可以為shoutAndSwim所接受。這在C++、Java、C#等靜態類型語言中是不可能的[4],除非鴨和蛙在同一繼承樹上,或者二者均顯式實現了一個包含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’。不妨理解為:盡可能守規則,必要時求變通。”
句號俏皮地說:“規則如褲帶,過於寬松和過於束縛都不好。”
問號提出新問題:“動態類型語言與弱類型語言有何不同?”
冒號喟言:“它們也常常被混為一談,但類型的動靜與強弱完全是正交的兩個概念。靜態類型語言中,有強類型的Java,也有弱類型的C;動態類型語言中,有強類型的Smalltalk,也有弱類型的JavaScript。前者以類型的綁定(binding)時間來劃分,後者以類型的約束強度來劃分。通常弱類型語言(weakly-typing language)允許一種類型的值隱性轉化為另一種類型[5]。舉個例子,1+"2"在VB中等於3——第二個字符串轉化為整數;在JavaScript中等於"12"——第一個整數轉化為字符串;在C中則等於一個不定的整數值——第二個字符串作為地址來運算。這樣似乎很有趣很方便,但程序容易藏污納垢,滋生臭蟲(bug)。與此相對地,強類型語言(strongly-typed language)著意貫徹類型控制,為保障數據的完整性和代碼的安全有效性,一般不允許隱性類型轉換[6]。如果一定需要類型轉換,必須是顯性轉換,一般通過我們熟知的鑄型(cast)來完成。”
引號想起:“好像還有一種所謂的類型安全語言?”
逗號緊緊抱著頭,仿佛害怕裂開。
“類型按安全性來劃分,可分為類型安全語言(type-safe language)和類型不安全語言(type-unsafe language)。類型檢查的目的就是為了避免類型錯誤(type error)[7],即杜絕因類型問題而產生的錯誤或不良代碼。如果一個類型系統能完全做到這一點,它就被稱為類型安全的。雖然尚存爭議,但一般認為強類型語言對類型控制更嚴格,因而是類型安全的,弱類型語言是類型不安全的。類型安全固然對保障程序的合理性和可靠性十分重要,但若過於嚴苛,程序也就失去了活力,正所謂‘水至清則無魚’啊。” 冒號有條不紊地解說著,“至此,我們已論及數據類型的三種劃分方式。需要說明的是,這些劃分並非泾渭分明的[8],更多的是定性而非定量的描述,甚至沒有公認統一的定義。但了解它們,對我們理解編程語言和編程原則是大有裨益的。”
,插語
[1] Scala是動態語言,卻是靜態類型的。
[2] Visual Basic(不包括VB.NET) 支持動態類型,卻是靜態語言。
[3] 極少數語言沒有類型檢查(untyped或typeless),如大多數匯編語言、Forth語言等。
[4] C#4.0將支持duck typing。
[5] 隱式轉換也稱為強制轉換(coercion)。有人將顯式轉換的鑄型(cast)譯為強制轉換,並不准確。
[6] 但許多強類型語言對於寬轉換(widening conversion)還是允許隱性的,如必要時int可自動轉換為float。
[7] 典型的類型錯誤是:一個函數本來期待的參數類型是A,實際傳入的變量a卻不是A類型。
[8] 比如,靜態類型的OOP語言如C++、Java支持downcasting,能在運行期間進一步細化數據類型,從某種意義上也具有動態類型的特征。
。總結
盡管動態語言大多數是動態類型語言,但二者並不是一回事。
數據類型包含兩個要素:允許取值的集合和允許參與的運算。
數據類型既有針對機器的物理意義,又有針對人的邏輯意義,提高了代碼的安全性和抽象性。
動態類型的類型檢查發生在運行期間,靜態類型的類型檢查發生在編譯期間(運行之前)。
動態類型的變量不需要顯式聲明,靜態類型的變量需要通過顯式聲明或類型推斷。
鴨子類型是動態類型的一種風格,允許非繼承性多態,即一個對象的類型可以由其接口集合來確定,不需要通過顯式繼承。它有利於代碼重用,但也可能造成誤用和濫用。
動態類型語言的優點:代碼簡明靈活、易於重用,適合泛型編程和快速原型開發。
靜態類型語言的優點:運行之前的類型檢查增強了代碼的可靠性,使編譯器有可能進行優化處理從而提高運行效率,節省了運行期的類型檢查所占用的時間和空間,同時類型聲明有輔助文檔的功效。
靜態類型檢查實行“疑罪從有”的有罪推定制,動態類型檢查實行“疑罪從無”的無罪推定制。取捨的原則是:Static Typing Where Possible, Dynamic Typing When Needed。即盡可能守規則,必要時求變通。
類型的動靜以類型的綁定時間來劃分,類型的強弱以類型的約束強度來劃分,它們之間沒有必然聯系。弱類型語言允許類型的隱性轉化,被認為是類型不安全的;而強類型語言則一般不允許這種轉化,被認為是類型安全的。