類型系統
類型系統
類型系統是在計算機科學中,類型系統用於定義如何將編程語言中的數值和表達式歸類為許多不同的類型,如何操作這些類型,這些類型如何互相作用。類型可以確認一個值或者一組值具有特定的意義和目的(雖然某些類型,如抽象類型和函數類型,在程序運行中,可能不表示為值)。類型系統在各種語言之間有非常大的不同,也許,最主要的差異存在於編譯時期的語法,以及運行時期的操作實現方式。
編譯器可能使用值的靜態類型以優化所需的存儲區,並選取對值運算時的較佳演演算法。例如,在許多C編譯器中,“浮點數”數據類型是以 32 比特表示,與IEEE 754 規格一致的單精度浮點數。因此,在數值運算上,C 應用了浮點數規範(浮點數加法、乘法等等)。
類型的約束程度以及評估方法,影響了語言的類型。更進一步,編程語言可能就類型多態性部分,對每一個類型都對應了一個極度個別的演演算法的運算。類型理論研究類型系統,儘管實際的編程語言類型系統,起源於電腦架構的實際問題、編譯器實現,以及語言設計。
定型(typing,又稱類型指派)賦予一組比特某個意義。類型通常和存儲器中的數值或對象(如變數)相聯繫。因為在電腦中,任何數值都是以一組比特簡單組成的,硬體無法區分存儲器地址、腳本、字元、整數、以及浮點數。類型可以告知程序和程序設計者,應該怎麼對待那些比特。
類型系統提供的主要功能有:
• 安全性
使用類型可允許編譯器偵測無意義的,或者是可能無效的代碼。例如,可以識出一個無效的表達式 "Hello, World" + 3,因為不能對(在平常的直覺中)逐字字元串加上一個整數。強類型提供更多的安全性,但它並不能保證絕對安全(詳情請見類型安全)
• 優化
靜態類型檢查可提供有用的信息給編譯器。例如,如果一個類型指明某個值必須以 4 的倍數對齊,編譯器就有可以使用更有效率的機器指令。
• 可讀性
在更具表現力的類型系統中,若其可以闡明程序設計者的意圖的話,類型就可以充當為一種文件形式。例如,時間戳記可以是整數的子類型;但如果程序設計者聲明一個函數為返回一個時間戳記,而不只是一個整數,這個函數就能表現出一部分文件的闡釋性。
• 抽象化(或模塊化)
類型允許程序設計者對程序以較高層次的方式思考,而不是煩人的低層次實現。例如,程序設計者可以將字元串想成一個值,以此取代僅僅是位元組的數組。或者類型允許程序設計者表達兩個子系統之間的介面。將子系統間交互時的必要定義加以定位,防止子系統間的通信發生衝突。
程序通常對每一個值關係一個特定的類型(儘管一個類型可以有一個以上的子類型)。其它的實體,如對象、模塊、通信頻道、依賴關係,或者純粹的類型自己,可以和一個類型關係。例如:
• 數據類型
一個數值的類型
• 類型
一個對象的類型
• 種類
一個類型的類型
在每一個編程語言中,都有一個特定的 類型系統,保證程序的表現良好,並且排除違規的行為。作用系統對類型系統提供更多細微的控制。
類型檢查所進行的檢驗處理以及實行類型的約束,可發生在編譯時期(靜態檢查)或運行時期(動態檢查)。靜態類型檢查是在編譯器所進行語義分析中進行的。如果一個語言強制實行類型規則(即通常只允許以不丟失信息為前提的自動類型轉換)就稱此處理為 強類型,反之稱為 弱類型。
如果一個編程語言的類型檢查,可在不測試運行時期表達式的等價性的情況下進行,該語言即為 靜態類型的。一個靜態類型的編程語言,是在運行時期和編譯時期之間的處理階段下重視這些區別的。如果程序的獨立模塊,可進行各自的類型檢查(獨立編譯),而無須所有會在運行時出現的模塊的那些信息,該語言即具有一個編譯時期階段。如果一個編程語言支持運行時期(動態)調度已標記的數據,該語言即為 動態類型的。如果一個編程語言破壞了階段的區別,因而類型檢查需要測試運行時期的表達式的等價性,該語言即為 依存類型的。
在動態類型中,經常在運行時期進行類型標記的檢查,因為變數所約束的值,可經由運行路徑獲得不同的標記。在靜態類型編程語言中,類型標記使用辨識聯合類型表示。
動態類型經常出現於腳本語言和RAD語言中。動態類型在解譯語言中極為普遍,編譯語言則偏好無須運行時期標記的靜態類型。對於類型和隱式類型語言較完整的列表參見類型和隱式類型語言。
術語 推斷類型(鴨子類型,duck typing)指的是動態類型在語言中的應用方式,它會“推斷”一個數值的類型。
某些靜態語言有一個“後門”,在這些編程語言中,能夠編寫一些不被靜態類型所檢查的代碼。例如,Java 和 C-風格的語言有“轉型”可用。在靜態類型的編程語言中,不必然意味著缺乏動態類型機制。例如 Java 使用靜態類型,但某些運算需要支持運行時期的類型測試,這就是動態類型的一種形式。更多靜態和動態類型的討論,請參閱編程語言。
對靜態類型和動態類型兩者之間的權衡也是必要的。
靜態類型在編譯時期時,就能可靠地發現類型錯誤。因此通常能增進最終程序的可靠性。然而,有多少的類型錯誤發生,以及有多少比例的錯誤能被靜態類型所捕捉,仍有爭論。靜態類型的擁護者認為,當程序通過類型檢查時,它才有更高的可靠性。雖然動態類型的擁護者指出,實際流通的軟體證明,兩者在可靠性上並沒有多大差別。可以認為靜態類型的價值,在於增進類型系統的強化。強類型語言(如 ML 和 Haskell)的擁護者提出,幾乎所有的臭蟲都可以看作是類型錯誤,如果編寫者以足夠恰當的方式,或者由編譯器推斷來聲明一個類型。
靜態類型通常可以編譯出速度較快的代碼。當編譯器清楚知道所要使用的數據類型,就可以產生優化過後的機器碼。更進一步,靜態類型語言中的編譯器,可以更輕易地發現較佳捷徑。某些動態語言(如 Common Lisp)允許任意類型的聲明,以便於優化。以上理由使靜態類型更為普及。參閱優化。
相較之下,動態類型允許編譯器和解譯器更快速的運作。因為源代碼在動態類型語言中,變更為減少進行檢查,並減少解析代碼。這也可減少編輯-編譯-測試-除錯的周期。
靜態類型語言缺少類型推斷(如 Java),而需要編寫者聲明所要使用的方法或函數的類型。編譯器將不允許編寫者忽略,這可為程序起附加性說明文件的作用。但靜態類型語言也可以無須類型聲明,所以與其說是靜態類型的代價,倒不如說是類型聲明的報酬。
靜態類型允許構造函數庫,它們的用戶不太可能意外的誤用。這可作為傳達庫開發者意圖的額外機制。
動態類型允許建構一些靜態類型系統所做不出來的東西。例如,eval 函數,它使得運行任意數據作為代碼成為可能(不過其代碼的類型仍是靜態的)。此外,動態類型容納過渡代碼和原型設計,如允許使用字元串代替數據結構。靜態類型語言最近的增強(如 Haskell 一般化代數數據類型)允許 eval 函數以類型安全的方式撰寫。
動態類型使元程序設計更為強大,且更易於使用。例如 C++ 模板的寫法,比起等價的 Ruby 或 Python 寫法要來的麻煩。更高度的運行時期構成物,如元類型(metaclass)和內觀(Introspection),對靜態類型語言而言通常更為困難。
強類型的基本定義即為,禁止錯誤類型的參數繼續運算。C語言的類型轉換即為缺乏強類型的證例;如果編寫者用 C 語言對一個值轉換類型,不僅令編譯器允許這個代碼,而且在運行時期中也同樣允許。這使得 C 代碼可更為緊密和快速,不過也使除錯變的更為困難。
部分學者使用術語 存儲器安全語言(或簡稱為 安全語言)形容禁止未定義運算髮生的語言。例如,某個存儲器安全語言將會檢查數組邊界。
設計精巧的語言也允許語言顯現出弱類型(藉由類型推斷之類的技術)的特性以方便使用,並且保留了強類型語言所提供的類型檢查和保護。例子包括 VBNet、C# 以及 Java。
運算符重載所帶來的簡化,像是不以算術運算中的加法來使用“+”,可以減少一些由動態類型所造成的混亂。例如,部分語言使用“.”或“&”來串連字元串。
編程語言的類型系統的第三種分類方法,就是類型運算和轉換的安全性。如果它不允許導致不正確的情況的運算或轉換,計算機科學就認為該語言是“類型安全”的。
術語“多態性”指的是:代碼(尤其是函數和類型)對各種類型的值能夠動作,或是相同數據結構的不同實體能夠控制不同類型的元素。為了提升復用代碼的潛在價值,類型系統逐漸允許多態性:在具有多態性的語言中,程序設計者只需要實現如列表或詞典的數據結構一次,而不是對使用到它的元素的每一個類型都規劃一次。基於這個原因,電腦學家也稱使用了一定的多態性的方法為泛型程序設計。類型理論的多態性基礎與抽象化、模塊化和(偶爾)子類型有相當密切的聯繫關係。
推斷類型
推斷類型(鴨子類型,Duck typing)最初是由 Dave Thomas 在 Ruby 社區中提出的,推斷類型用了這個論證法“如果它像什麼,而且其它地方也像什麼,那麼它就是什麼。”
這個技術之所以常被稱作“鴨子類型”,是基於這句格言:“如果它搖搖擺擺的走法很像鴨子,而且它的嘎嘎叫聲也像鴨子,那它就是一隻鴨子!”
許多靜態類型系統,如 C 和 Java,要求要 聲明類型:編寫者必須以指定類型明確地關係到每一個變數上。其它的,如 Haskell,則進行類型推斷:編譯器根據編寫者如何運用這些變數,以草擬出關於這個變數的類型的結論。例如,給定一個函數f(x,y),它將x 和y 加起來,編譯器可以推斷出x 和y 必須是數字——因為加法僅定義給數字。因此,任何在其它地方以非數值類型(如字元串或鏈表)作為參數來調用f 的話,將會發出一個錯誤。
在代碼中數值、字元串常數以及表達式,經常可以在詳細的前後文中暗示類型。例如,一個表達式 3.14 可暗示浮點數類型;而 [1, 2, 3] 則可暗示一個整數的鏈表;通常是一個數組。
類型的類型是一種 種類。在類型程序設計中有明確的種類,如 Haskell 編程語言的類型構造函數,在申請比較簡單的類型之後,其返回一個簡單的類型。例如,類型構造函數二選一 有這些種類* -> * -> *(* 代表種類),而且它的申請二選一 字元串 整數 是一個簡單的類型。然而,大多數編程語言的類型,是由編寫者來暗示或硬編碼,這就並未將種類的概念用作為首選層。
類型可分為幾個大類:
• 原始類型
這是最簡單的類型種類,例如:整數和浮點數
• 整數類型
全部是數字的類型,例如:整數和自然數
• 浮點數類型
以浮點數表示數字的類型
• 複合類型
由基本類型組合成的類型,例如:數組或記錄單元。抽象數據類型具有複合類型和界面兩種屬性,這取決於你提及哪一個。
• 子類型
• 派生類型
• 對象類型
例如:變數類型
• 不完全類型
• 遞歸類型
• 函數類型
例如:雙參函數
• 全稱量化類型
如參數化類型、類型變數
• 存在量化類型
如模塊
• 精鍊類型
識別其它類型的子集的類型
• 依存類型
取決於運行時期的數值的類型
• 所有權類型
描述或約束面向對象系統結構的類型
對於靜態類型語言的類型檢查器,必須檢驗所有表達式的類型,是否與前後文所期望的類型一致。例如指派語句x :=e,推斷表達式e 的類型,必定與聲明或推斷的變數類型 x 一致。這個一致性的概念,就稱為兼容性,是每一個編程語言所特有的。
很明顯,如果e 和 x 的類型相同,就允許指派,然後這是一個有效的表達式。因此在最簡單的類型系統中,問題從兩個類型是否兼容,簡化為兩個類型是否相等(或等價)。然而不同的語言對於兩個類型表達式是否理解為表示了相同類型,有著不同的標準。類型的相等理論的差異相當巨大,兩個極端的例子是結構類型系統(Structural type system),任兩個以相同結構所描述的值的類型都是等價的,且在標明類型系統(Nominative type system)上,沒有兩個獨特的語法構成的類型表達式表示同一類型,(即類型若要相等,就必須具有相同的“名字”)。
在子類型的語言中,兼容關係更加複雜。特別是如果A 是B 的子類型,那麼類型A 的值可用於類型B 也屬意料之中,但反過來就不是這樣。如同等價性,對每一個編程語言而言,子類型的關係的定義是不同的,可能存在各種變化。在語言中出現的參數或者特定的多態性,也可能意味著具有對類型的兼容性。
在強類型、靜態類型語言的支持者,和動態類型、自由形式的支持者之間,經常發生爭執。前者主張,在編譯的時候就可以較早發現錯誤,而且還可增進運行時期的性能。後者主張,使用更加動態的類型系統,分析代碼更為簡單,減少出錯機會,才能更加輕鬆快速的編寫程序。與此相關的是,考慮到在類型推斷的編程語言中,通常不需要手動聲明類型,這部分的額外開銷也就自動降低了。