型別檢查所進行的檢驗處理以及實行型別的約束,可發生在編譯時期(靜態檢查)或執行時期(動態檢查)。靜態型別檢查是在編譯器所進行語義分析中進行的。如果一個語言強制實行型別規則(即通常只允許以不遺失資訊為前提的自動型別轉換)就稱此處理為強型別,反之稱為弱型別。
如果一個程式語言的型別檢查,可在不測試執行時期運算式的等價性的情況下進行,該語言即為靜態型別的。一個靜態型別的程式語言,是在執行時期和編譯時期之間的處理階段下重視這些區別的。如果程式的獨立模組,可進行各自的型別檢查(獨立編譯),而無須所有會在執行時出現的模組的那些資訊,該語言即具有一個編譯時期階段。如果一個程式語言支援執行時期(動態)調度已標記的資料,該語言即為動態型別的。如果一個程式語言破壞了階段的區別,因而型別檢查需要測試執行時期的運算式的等價性,該語言即為依存型別的。[1]
在動態型別中,經常在執行時期進行型別標記的檢查,因為變數所約束的值,可經由執行路徑獲得不同的標記。在靜態型別程式語言中,型別標記使用辨識聯合型別表示。
動態型別經常出現於腳本語言和RAD語言中。動態型別在解释型语言中極為普遍,編譯語言則偏好無須執行時期標記的靜態型別。對於型別和隱式型別語言較完整的列表參見型別和隱式型別語言。
術語推斷型別(鸭子类型,duck typing)指的是動態型別在語言中的應用方式,它會「推斷」一個數值的型別。
看看型別標記檢查是如何運作的,考慮下列假碼範例:
var x; //(1)
x := 5; //(2)
x := "hi"; //(3)
在這個範例中,(1)宣告x;(2)將整數值5代給x;(3)將字串值"hi"代給x。在主要的靜態系統中,這個代碼片斷將會違反規則,因為(2)和(3)對 x所約束的型別相矛盾。
相較之下,一個純粹的動態型別系統允許上述程式的執行,因為型別標記附到數值上(不是變數)。在處理錯誤語句或運算式的時候,以動態型別實作的語言會捕捉程式的錯誤,而不是誤用錯誤型別的數值。換句話說,動態型別捕捉在程式執行時的錯誤。
典型的動態型別實作,會以型別標記維持程式所有數值的「標記」,並在運算任何數值之前檢查標記。例如:
var x := 5; //(1)
var y := "hi"; //(2)
var z := x + y; //(3)
在這個程式片斷中,(1)將數值5約束給x;(2)將數值"hi"約束給y;以及(3)嘗試將x加到y。在動態型別語言中,約束給x的值會是一對(整數, 5),且約束給y的值會是一對(字串, "hi")。當這個程式嘗試執行第3行時,語言對型別標記整數和字串進行檢查,如果這兩個型別的+(加法)運算尚未定義,就會發出一個錯誤。
某些靜態語言有一個「後門」,在這些程式語言中,能夠編寫一些不被靜態型別所檢查的代碼。例如,Java和C-風格的語言有「轉型」可用。在靜態型別的程式語言中,不必然意味著缺乏動態型別機制。例如Java使用靜態型別,但某些運算需要支援執行時期的型別測試,這就是動態型別的一種形式。更多靜態和動態型別的討論,請參閱程式語言。
實踐中的靜態和動態型別檢查
编辑
對靜態型別和動態型別兩者之間的權衡也是必要的。
靜態型別在編譯時期時,就能可靠地發現型別錯誤。因此通常能增進最終程式的可靠性。然而,有多少的型別錯誤發生,以及有多少比例的錯誤能被靜態型別所捕捉,目前對此仍有爭論。靜態型別的擁護者認為,當程式通過型別檢查時,它才有更高的可靠性。雖然動態型別的擁護者指出,實際流通的軟體證明,兩者在可靠性上並沒有多大差別。可以認為靜態型別的價值,在於增進型別系統的強化。強型別語言(如ML和Haskell)的擁護者提出,幾乎所有的bug都可以看作是型別錯誤,如果編寫者以足夠恰當的方式,或者由編譯器推斷來宣告一個型別。[2]
靜態型別通常可以編譯出速度較快的代碼。當編譯器清楚知道所要使用的資料型別,就可以產生最佳化過後的機器碼。更進一步,靜態型別語言中的編譯器,可以更輕易地發現較佳捷徑。某些動態語言(如Common Lisp)允許任意型別的宣告,以便於最佳化。以上理由使靜態型別更為普及。參閱最佳化。
相較之下,動態型別允許編譯器和解譯器更快速的運作。因為原始碼在動態型別語言中,變更為減少進行檢查,並減少解析代碼。這也可減少編輯-編譯-測試-除錯的週期。
靜態型別語言缺少型別推斷(如Java),而需要編寫者宣告所要使用的方法或函式的型別。編譯器將不允許編寫者忽略,這可為程式起附加性說明文件的作用。但靜態型別語言也可以無須型別宣告,所以與其說是靜態型別的代價,倒不如說是型別宣告的報酬。
靜態型別允許建構函式庫,它們的使用者不太可能意外的誤用。這可作為傳達函式庫開發者意圖的額外機制。
動態型別允許建構一些靜態型別系統所做不出來的東西。例如,eval函式,它使得執行任意資料作為代碼成為可能(不過其代碼的型別仍是靜態的)。此外,動態型別容納過渡代碼和原型設計,如允許使用字串代替資料結構。靜態型別語言最近的增強(如Haskell 一般化代數資料型別)允許eval函式以型別安全的方式撰寫。
動態型別使元程式設計更為強大,且更易於使用。例如C++模板的寫法,比起等價的Ruby或Python寫法要來的麻煩。更高度的執行時期構成物,如元類別(metaclass)和內觀(Introspection),對靜態型別語言而言通常更為困難。
強型別的基本定義即為,禁止錯誤型別的參數繼續運算。C語言的型別轉換即為缺乏強型別的證例;如果編寫者用C語言對一個值轉換型別,不僅令編譯器允許這個代碼,而且在執行時期中也同樣允許。這使得C代碼可更為緊密和快速,不過也使除錯變的更為困難。
部分學者使用術語記憶體安全語言(或簡稱為安全語言)形容禁止未定義運算發生的語言。例如,某個記憶體安全語言將會檢查陣列邊界。
弱型別意指一個語言可以隱式的轉換型別(或直接轉型)。看看先前的例子:
var x := 5;
var y := "37";
x + y;
在弱型別語言中編寫上述代碼,並不清楚將會得到哪一種結果。某些語言如Visual Basic,將會產生可以運作的代碼,它將會給出的結果是42:系統將字串"37"轉換成數字37,以符合運算上的直覺;其它的語言,像JavaScript將會產生的結果是"537":系統將數字5轉換成字串"5"並把兩者串接起來。在Visual Basic和JavaScript中,最終的型別是以那兩個運算元為考量的規則所決定。在部分語言中,如AppleScript,某個值最終的型別,只以最左邊的運算元的型別所決定。
設計精巧的語言也允許語言顯現出弱型別(藉由类型推断之類的技術)的特性以方便使用,並且保留了強型別語言所提供的型別檢查和保護。例子包括VB.Net、C#以及Java。
運算子多載所帶來的簡化,像是不以算術運算中的加法來使用「+」,可以減少一些由動態型別所造成的混亂。例如,部分語言使用「.」或「&」來串連字串。
型別系統的安全性
编辑
程式語言的型別系統的第三種分類方法,就是型別運算和轉換的安全性。如果它不允許導致不正確的情況的運算或轉換,電腦科學就認為該語言是「型別安全」的。
再次看看這個假碼例子:
var x := 5;
var y := "37";
var z := x + y;
在一個如Visual Basic的語言中,例子中的變數z得到的值為42。不管編寫者有沒有這個意圖,該語言定義了明確的結果,且程式不會就此崩潰,或將不明定義的值賦給z。就這方面而言,這樣的語言就是型別安全的。
現在來看C的相同例子:
int x = 5;
char y[] = "37";
char* z = x + y;
在這個例子中,z將會指向一個超過y位址5個位元組的記憶體位址,相當於指向y字串的指標之後的兩個空字元之處。這個位址的內容尚未定義,且有可能超出記憶體的定址界線,而且就這麼引用參考z會引起程式的終止。雖是一個良好型別,但卻不是記憶體安全的程式——如果以對型別安全語言而言不該發生為先決條件的話。