如今大多數程序設計語言中都有表達式和/或對象的類型概念。類型起着兩種主要做用:html
爲許多操做提供了隱含的上下文信息,使程序員能夠在許多狀況下沒必要顯示的描述這種上下文。好比int類型的兩個對象相加就是整數相加、兩個字符串類型的對象相加就是拼接字符串、在Java和C#中new object()隱含在背後的就是要分配內存返回對象的引用等等。python
類型描述了其對象上一些合法的能夠執行的操做集合。類型系統將不容許程序員去作一個字符和一個記錄的加法。編譯器可使用這個合法的集合進行錯誤檢查,好的類型系統可以在實踐中捕獲不少錯誤程序員
從編譯方面的知識咱們能夠知道,計算機硬件能夠按多種不一樣的方式去解釋寄存器裏的一組二進制位。處理器的不一樣功能單元可能把一組二進制位解釋爲指令、地址、字符、各類長度的整數或者浮點數等。固然,二進制位自己是無類型的,對存儲器的哪些位置應該如何解釋,大部分硬件也無任何保留信息。彙編語言因爲僅僅是對一些二進制指令的「助記符號」翻譯,它也是這種無類型狀況。高級語言中則老是關聯值與其類型,須要這種關聯的一些緣由和用途就如前面說到的上下文信息和錯誤檢測。編程
通常來講,一個類型系統包含一種定義類型並將它們與特定的語言結構關聯的機制;以及一些關於類型等價、類型相容、類型推理的規則。 必須具備類型的結構就是那些能夠有值的,或者能夠引用具備值得對象的結構。類型等價規則肯定兩個值得類型什麼時候相同;類型相容規則肯定特定類型的值是否能夠用在特定的上下文環境裏;類型推理規則基於一個表達式的各部分組成部分的類型以及其外圍上下文來肯定這個表達式的類型。數組
在一些多態性變量或參數的語言中,區分表達式(如一個名字)的類型與它所引用的那個對象的類型很是重要,由於同一個名字在不一樣時刻有可能引用不一樣類型的對象。安全
在一些語言中,子程序也是有類型的,若是子程序是一級或者二級值,其值是動態肯定的子程序,這時語言就須要經過類型信息,根據特定的子程序接口(即參數的個數和類型)提供給這種結構的可接受的值集合,那麼子程序就必須具備類型信息。在那些不能動態建立子程序引用的靜態做用域語言(這種語言中子程序是三級值),編譯器時就能肯定一個名字所引用的子程序,所以不須要子程序具備類型就能夠保證子程序的正確調用。markdown
類型檢查時一個處理過程,其目的就是保證程序遵循了語言的類型相容規則,違背這種規則的狀況稱爲類型衝突。說一個語言是強類型的,那麼就表示這個語言的實現遵循一種禁止把任何操做應用到不支持這種操做的類型對象上的規則。說一個語言是靜態類型化(statically type)的,那麼它就是強類型的,且全部的類型檢查都能在編譯時進行(現實中不多有語言是真正的靜態類型,一般這一術語是指大部分類型檢查能夠在編譯器執行,其他一小部分在運行時檢查)。如C#咱們一般都認爲它是靜態類型化的語言。數據結構
動態(運行時)類型檢查是遲約束的一種形式,把大部分的檢查操做都推遲到運行的時候進行。採用動態做用域規則的語言大部分都是動態類型語言,由於它的名字和對象的引用都是在運行時肯定的,而肯定引用對象的類型則更是要在引用肯定以後才能作出的。架構
類型檢查是把雙刃劍,嚴格的類型檢查會使編譯器更早的發現一些程序上的錯誤,可是也會損失一部分靈活性;動態類型檢查靈活性大大的,可是運行時的代價、錯誤的推遲檢查,各類語言的實現也都在這種利弊上進行權衡。函數
多態性使得同一段代碼體能夠對多個類型的對象工做。它意味着可能須要運行時的動態檢查,但也未必必定須要。在Lisp、Smalltalk以及一些腳本語言中,徹底的動態類型化容許程序員把任何操做應用於任何對象,只有到了運行時採起檢查一個對象是否實現了具體的操做。因爲對象的類型能夠看做它們的一個隱式的(未明確聲明的,一個不恰當的比喻就如C#中的this)參數,動態類型化也被說成是支持隱式的參數多態性。
雖然動態類型化具備強大的威力(靈活性),但卻會帶來很大的運行時開銷,還會推遲錯誤報告。一些語言如ML採用了一種複雜的類型推理系統,設法經過靜態類型化支持隱式的參數多態性。
在面嚮對象語言裏,子類型多態性容許類型T的變量X引用了從T派生的任何類型的對象,因爲派生類型一定支持基類型的全部操做,所以編譯器徹底能夠保證類型T的對象能接受的任何操做,X引用的對象也都能接受。對於簡單的繼承模型,子類型多態的類型檢查就能徹底在編譯時實現。採用了這種實現的大多數語言(如C++,JAVA和C#)都提供另外一種顯示的參數化類型(泛型),容許程序員定義帶有類型參數的類。泛型對於容器(集合)類型特別有用,如T的列表(List)和T的棧(Stack)等,其中T只是一個類型佔位符,在初始化的這個容器對象時提供具體的類型來代替它。與子類型多態相似,泛型也能夠在編譯時完成類型檢查。好比C++的模板徹底就是編譯期間的東西,編譯後就徹底沒有了模板的痕跡;JAVA則是利用一種「擦除」的技術實現的泛型,須要在運行時作一些檢查。
如今至少存在三種不一樣的考慮類型問題的方式,分別稱之爲指稱的、構造的和基於抽象的
按照指稱的觀點,一個類型就是一組值,一個值具備某個類型的條件是他屬於這個值集合,一個對象具備某個類型的條件是他的值保證屬於這個值集合
從構造的觀點看,一個類型或者是以一小組內部類型,或者是經過對一個或幾個更簡單些的類型,應用某個類型的構造符構造出來的
從基於抽象的角度來看,一個類型就是一個接口,由一組定義良好並且具備相互協調的語義的操做組成。
在不一樣語言裏,有關類型的術語也不相同,這裏說的一般都是經常使用的術語,大部分語言多提供的內部類型差很少就是大部分處理器所支持的類型:整數、字符、布爾和實數。
通常語言規範中都會規定數值類型的精度問題,以及一些字符的編碼規定。一般特殊的一個數值類型是枚舉類型,具體的語法在不一樣的語言中略有差別,可是其也都是一個目的(用一個字符友好的表示一個數值)。
關於枚舉類型,由一組命名元素組成。在C中能夠這樣寫:
enum weekday { sun, mon, tue, wed, thu, fri, sat }; 複製代碼
在C中這樣的寫法和直接對裏面的元素直接賦值除了語法上效果徹底同樣。可是在以後的許多語言中,枚舉類型是一個真正的類型
還有一些語言中提供一種稱爲子界的類型,它表示一種基於基本數值的一個連續的區間。好比Pascal中表示1到100:
type test_score = 0..100 複製代碼
複合類型:由一些簡單的基本類型組合成的一些類型稱爲複合類型,好比常見的記錄、變體記錄、數組、集合、指針、表等,具體的都會在後面詳細介紹。
大多數的靜態類型語言中,定義一個對象都是須要描述清楚它的類型,進一步講,這些對象出現的上下文也都是有類型的,也就是說語言中的一些規則限制了這種上下文中能夠合法出現的對象類型。
類型相容肯定了一個特定類型的對象的可否用在一個特定上下文中。在最極端的狀況下,對象可以使用的條件就是它的類型與上下文所指望的類型等價。可是在大多數語言中,相容關係都比等價更寬鬆一些,即便對象與上下文的類型不一樣,它們也能夠相容。
而類型推理想回答的是從一個簡單的表達式出發構造另外一個表達式時,這整個的表達式的類型是什麼
在用戶能夠定義新類型的語言中,類型等價的定義通常基於兩種形式。
type R2 = record a : integer b : integer end; type R2 = record b : integer a : integer end; 複製代碼
基於類型定義的內容,就是它們由一樣的組成部分且按照一樣的方式組合而成
它的準肯定義在不一樣的語言中也不同,由於它們要決定類型之間的哪些潛在差別是重要的,哪些是能夠接受的(好比上面的兩個定義,是否還認爲是等價的)。結構等價是一種很直接的認識類型的方式,早期的一些語言(Algol 6八、Modula-三、ML)有些事基於結構等價的,如今的大部分語言(Java、C#)大都是基於名字等價了,爲什麼呢?由於從某種意義上看,結構等價是由底層、由實現決定的,屬於比較低級的思考方式。就如一個上下文,若是你傳遞了一個結構等價可是不是所期待對象,實施結構等價的編譯器是不會拒絕這種狀況的(假如這不是你但願的,那麼你也不會獲得任何提示或者錯誤信息,很難排查的)。
基於類型的詞法形式,能夠認爲是每個名字都引進一個新的類型;
它基於一種假設,就是說程序員花時間定義了兩個類型,雖然它們的組成部分可能相同,可是程序員要表達的意思就是這是兩個不一樣的類型。名字等價的常規判斷就很是簡單了,看看聲明兩個對象的類型是不是一個就是了。可是也會有一些特殊的狀況出現,好比類型別名(C、C++的程序員很熟悉這種東西吧),好比 typedef int Age; 就爲int類型從新定義了一個別名"Age"。那些認爲int不等價越Age的語言稱爲嚴格名字等價,認爲等價的稱爲寬鬆名字等價。其實這兩種也是很容易區分的,只要能區分聲明和定義兩個概念的差別就能夠區分。在嚴格名字等價中看待typedef int Age是認爲定義了一個新類型Age,在寬鬆名字等價看來這就是一個類型聲明而已,int和Age共享同一個關於整數的定義。
在靜態類型的語言中,若是「a=b」,那麼咱們會指望b的類型和a的相同;如今假定所提供的類型和指望的類型和所提供的類型相同,那麼咱們在要求某個類型的上下文中使用另一個類型時就須要顯示的寫出類型變換(或稱爲類型轉換)。根據具體的變換的具體狀況,在運行時執行這種變化會有如下三種主要的狀況出現:
所涉及的類型能夠認爲是結構等價的,這種狀況裏面由於涉及的類型採用了相同的底層的表示,則這種變換純粹就是概念上的操做,不須要運行時執行任何代碼。
所涉及的類型具備不一樣的值集合,但它們的值集合具備相同的表示形式。好比一個類型和它的子類型,一個整數和一個無符號的整數。拿無符號整數變換爲整數來講,因爲無符號整數的最大值是整數類型所容納不了的,則運行時就必須執行一些代碼來保證這種變換的合法性,若是合法則繼續下去,不然會產生一個動態語義錯誤。
所涉及的類型具備不一樣的底層表示,可是咱們能夠在它們的值之間定義某種對應關係。好比32位整數能夠變換到IEEE的雙精度浮點數,且不會丟失精度。浮點數也能夠經過舍入或割斷的形式變換成整數,可是會丟失小數部分。
有這麼一種狀況,咱們須要改變一個值,可是不須要改變它的二進制表示形式,更通俗點說就是咱們但願按照另一個類型的方式去解釋某個類型的二進制位,這種狀況稱爲非變換類型轉換。最簡單的一個例子好比說,一個byte類型的數值65,按byte類型來解釋它是65,若是按照char類型來解釋它就是字符「A」。好比C++中的static_cast執行類型變換,reinterpret_cast執行非變換的類型轉換。c中出現的union形式的結構,就能夠認爲是這種非變換的類型轉換的合法的安全的語言結構。在好比下面C中通常性非變換類型轉換代碼:
r=*((float *) &n); 複製代碼
任何非變換的類型轉換都極其危險的顛覆了語言的類型系統。在弱類型系統的語言中,這種顛覆可能很難發現,在強類型系統的語言中顯示的使用這種非變換的類型轉換,起碼從代碼上能夠看得出來它是這麼一回事,或多或少的有利於排查問題。
大多數語言的上下文中並不要求類型等價,相應的通常都是實施較爲「寬鬆」的類型相容規則。好比賦值語句要求右值相容與左值、參數類型相容,實際返回類型與指定的返回類型相容。在語言中,只要容許把一個類型的值用到指望的另一個類型的上下文中,語言都必須執行一個到所指望類型的自動隱式變換,稱爲類型強制(好比int b;double a=b;)。就像前面說的顯示的類型變換同樣,隱式的類型變換也可能須要執行底層代碼或者作一些動態類型檢查。
一個重載的名字可能引用不一樣類型的對象,這種歧義性須要經過上下文信息進行解析。好比a+b這個表達式能夠表示整數或者浮點數的加法運算,在沒有強制的語言中,a和b必須都是整數或都是浮點數。若是是有強制的語言,那麼在a或者b有一個是浮點數的狀況下,編譯器就必須使用浮點數的加法運算(另一個整數強制轉換爲浮點數)。若是語言中+只是進行浮點數運算,那麼即便a和b都是整數,也會被所有轉成浮點數進行運算(這代價就高了好多了)。
通用引用類型:一些語言根據實習需求,設計有通用的引用類型,好比C中的void*、C#中的Object,任意的值均可以賦值給通用引用類型的對象。可是問題是存進去容易取出來難,當通用引用類型是右值的時候,左值的類型可能支持某些操做,然而這些操做右值對象是不具有的。爲了保證通用類型到具體類型的賦值安全,一種解決辦法是讓對象能夠自描述(也就是這個對象包含其真實類型的描述信息),C++,JAVA,C#都是這種方式,C#中若是賦值的類型不匹配則會拋出異常,而C++則是使用dynamic_cast作這種賦值操做,具體的後果呢,也是C++程序員負責。
經過前面的類型檢查咱們能夠保證表達式的各各組成部分具備合適的類型,那麼這整個表達式的類型是什麼來着?其實在大多數的語言中也是比較簡單的,算術表達式的類型與運算對象相同、比較表達式老是布爾類型、函數調用的結果在函數頭聲明、賦值結果就是其左值的類型。在一些特殊的數據類型中,這個問題並非那麼清晰明瞭,好比子界類型、複合類型。好比下面的子界類型問題(Pascal):
type Atype=0..20; type Btype=10..20; var a: Atype; var b: Btype; 複製代碼
那麼a+b什麼類型呢???它確實是不能是Atype或者Btype類型,由於它可能的結果是10-40。有人以爲那就新構造一個匿名的子界類型,邊界時10到40。實際狀況是Pascal給的答案是它的基礎類型,也就是整數。
在Pascal中,字符串'abc'的類型是array[1..3] of char、而Ada則認爲是一種未徹底肯定的類型,該類型與任何3個字符數組相容,好比在Ada中'abc' & 'defg'其結果是一個7字符的數組,那麼這個7字符數組的類型是array[1..7] of cahr呢仍是某一個也是7個字符組成的類型array (weekday) of character呢,更或者是其餘任意一個也是包含七個字符數組的另一個類型。這種狀況就必須依賴表達式所處的上下文信息才能推到出來具體的類型來。
一些語言中稱記錄爲結構(struct),好比C語言。C++把結構定義爲class的一種特殊形式(成員默認全局可見),Java中沒有struct的概念,而C#則對struct採用值模型,對class採用引用模型。
一個簡單的結構體在C中能夠這樣定義:
struct element{ char name[2]; int number; double weight; Bool merallic; }; 複製代碼
等價於Pascal中的:
type two_chars=packed array [1..2] of char; type element - record name:two_chars; number:integer; weight:real; metallic:Boolean end 複製代碼
記錄裏面的成員(如name,number...)稱爲域(field)。在須要引用記錄中的域時,大部分語言使用「.」記法形式。好比Pascal中:
var copper:eement; copper.name=6.34; 複製代碼
大部分語言中還容許記錄的嵌套定義,好比在Pascal中:
type short_string=packed array[1..30] of char;
type ore=record
name:short_string;
element_yielded:record /*嵌套的記錄定義*/
name:two_chars;
number:integer;
weight:real;
metallic:Boolean
end
end
複製代碼
一個記錄的各個域一般被放入內存中的相鄰位置。編譯器在符號表中保存每一個域的偏移量,裝載和保存的時候經過基址寄存器和偏移量便可獲得域的內存地址。類型element在32位的機器中可能的佈局以下:
此處有圖
(圖在最後面,由於markdown的這個畫表格不符合這個要求,又不想引圖了,就直接用html寫了,會被擠到最後去)
在對結構體的存儲佈局方案上,若是使用正常排序,結構中的空洞會浪費空間。可是若是經過壓縮來節省空間,可是可能很帶來很嚴重的訪問時間的代價
數組是最多見也是最重要的複合數據類型。記錄用於組合一些不一樣類型的域在一塊兒;而數組則不一樣,它們老是同質的。從語義上看,能夠把數組想象成從一個下標類型到成員(元素)類型的映射。
有些語言要求下標類型必須是integer,也有許多語言容許任何離散類型做爲下標;有些語言要求數組的元素類型只能是標量,而大多數語言則容許任意類型的元素類型。也有一些語言容許非離散類型的下標,這樣產生的關聯數組只能經過散列表的方式實現,而沒法使用高效的連續位置方式存儲,好比C++中的map,C#中的Dictionary。在本節中的討論中咱們假定數組的下標是離散的。
大多數的語言都經過數組名後附加下標的方式(圓括號|方括號)來引用數組裏的元素。因爲圓括號()通常用於界定子程序調用的實際參數,方括號在區分這兩種狀況則有易讀的優點。Fortran的數組用圓括號,是由於當時IBM的打卡片機器上沒有方括號
對於數組的形狀在聲明中就已經描述,對於這種有靜態形狀的數組,能夠用一般的方式來管理內存:生存期是整個程序的數組使用棧分配,具備更通常的生存期的動態生成數組使用堆分配。可是對於在加工以前不知道其形狀的數組,或其形狀在執行期間可能改變的數組,存儲管理就會更復雜一點。
在編譯期間,符號表維護者程序中的每一個數組的維度和邊界信息。對於每一個記錄,它還維護着每一個域的偏移量。若是數組維度的數目和邊界是靜態已知的,編譯器就能夠在符號表中找出它們,以便計算數組元素的地址。若是這些值不是靜態已知的,則編譯器就必須生成代碼,在運行時從一個叫內情向量的數據結構來查找它
子程序參數是動態形狀數組最簡單的例子,其中數組的上下界在運行時才肯定,調用方都會傳遞數組的數據和一個適當的內情向量,可是若是一個數組的形狀只能到加工時才知道,這種狀況下仍能夠在子程序的棧幀裏爲數組分配空間,可是須要多作一層操做
在任意時間均可以改變形狀的數組,有時被稱爲是徹底動態的。由於大小的變化不會以先進先出的順序進行,因此棧分配就不夠用了。徹底動態的數組必須在堆中分配。好比Java中的ArrayList
大多數語言的實現裏,一個數組都存放在內存的一批連續地址中,好比第二個元素緊挨着第一個,第三個緊挨着第二個元素。對於多維數組而言,則是一個矩陣,會出現行優先和列優先的選擇題,這種選擇題對於語言使用者而言是透明的,而對語言的實現者則須要考慮底層方面的優化問題了。
在一些語言中,還有另一種方式,對於數組再也不用連續地址分配,也不要求各行連續存放,而是容許放置在內存的任何地方,再建立一個指向各元素的輔助指針數組,若是數組的維數多於兩維,就再分配一個指向指針數組的指針數組。這種方式稱爲行指針佈局,這種方式須要更多的內存空間,可是卻有兩個優勢:
許多語言中,字符串也就是字符的數組。而在另外一些語言中,字符串的狀況特殊,容許對它們作一些其餘數組不能用的操做,好比Icon以及一些腳本語言中就有強大的字符串操做功能。
字符串是編程中很是重要的一個數據類型,故而不少語言都對字符串有特殊的處理以便優化其性能以及存儲(好比C#中的字符串不可變性保證了性能,字符串駐留技術照顧了存儲方面的須要),因爲這些特殊的處理,故而各各語言中爲字符串提供的操做集合嚴重依賴語言設計者對於實現的考慮。
程序設計語言中的一個集合,也就是具備某個公共類型的任意數目的一組值的一種無序聚集。集合的元素所具備的類型叫作元類型或者基類型。如今的大多數程序設計語言都對集合提供了很大的支持,爲集合提供了不少相關的操做
所謂的遞歸類型,就是能夠在其對象中包含一個或多個本類型對象的引用類型。遞歸類型用於構造各類各樣的「連接」數據結構,好比樹。在一些對變量採用引用模型的語言中,很容易在建立這種遞歸類型,由於每一個變量都是引用;在一些對變量採用值模型的語言中,定義遞歸類型就須要使用指針的概念,指針就是一種變量,其值是對其餘對象的引用。
對於任何容許在堆裏分配新對象的語言,都存在一個問題:若這種對象不在須要了,什麼時候以及以何種方式收回對象佔用的空間?對於那些活動時間很短的程序,讓不用的存儲留在那裏,可能還能夠接受,畢竟在它不活動時系統會負責回收它所使用的任何空間。可是大部分狀況下,不用的對象都必須回收,以便騰出空間,若是一個程序不能把再也不使用的對象存儲回收,咱們就認爲它存在「內存泄漏」。若是這種程序運行很長一段時間,那麼它可能就會用完全部的空間而崩潰。許多早期的語言要求程序員顯示的回收空間,如C,C++等,另外一些語言則要求語言實現自動回收再也不使用的對象,如Java,C#以及全部的函數式語言和腳本語言。顯示的存儲回收能夠簡化語言的實現,但會增長程序員忘記回收再也不使用的對象(形成內存泄漏),或者不當的回收了不應回收的正在使用的對象(形成懸空引用)的可能性。自動回收能夠大大簡化程序員的工做,可是爲語言的實現帶來了複雜度。
對指針的操做包括堆中對象的分配和釋放,對指針間接操做以訪問被它們所指的對象,以及用一個指針給另外一個指針賦值。這些操做的行爲高度依賴於語言是函數式仍是命令式,以及變量/名字使用的是引用模型仍是值模型。
函數式語言通常對名字採用某種引用模型(純的函數式語言里根本沒有變量和賦值)。函數式語言裏的對象傾向於採起根據須要自動分配的方式。
命令式語言裏的變量可能採用值模型或引用模型,有時是二者的某種組合。好比 A=B;
Java的實現方式區分了內部類型和用戶定義的類型,對內部類型採用值模型,對用戶定義的類型採用則採用引用模型,C#的默認方式與Java相似,另外還提供一些附加的語言特性,好比「unsafe」可讓程序員在程序中使用指針。
在前兩篇的名字、做用域和約束中咱們列舉了對象的3種存儲類別:靜態、棧和堆。靜態對象在程序的執行期間始終是活動的,棧對象在它們的聲明所在的子程序執行期間是活動的,而堆對象則沒有明肯定義活動時間。
在對象不在活動時,長時間運行的程序就須要回收該對象的空間,棧對象的回收將做爲子程序調用序列的一部分被自動執行。而在堆中的對象,由程序員或者語言的自動回收機制負責建立或者釋放,那麼若是一個活動的指針並無引用合法的活動對象,這種狀況就是懸空引用。好比程序員顯示的釋放了仍有指針引用着的對象,就會形成懸空指針,再進一步假設,這個懸空指針原來指向的位置被其餘的數據存放進去了,可是實際卻不是這個懸空指針該指向的數據,若是對此存儲位置的數據進行操做,就會破壞正常的程序數據。
那麼如何從語言層面應對這種問題呢?Algol 68的作法是禁止任何指針指向生存週期短於這個指針自己的對象,不幸的是這條規則很難貫徹執行。由於因爲指針和被指對象均可能做爲子程序的參數傳遞,只有在全部引用參數都帶有隱含的生存週期信息的狀況下,纔有可能動態的去執行這種規則的檢查。
對程序員而已,顯示釋放堆對象是很沉重的負擔,也是程序出錯的主要根源之一,爲了追蹤對象的生存軌跡所需的代碼,會致使程序更難設計、實現,也更難維護。一種頗有吸引力的方案就是讓語言在實現層面去處理這個問題。隨着時間的推移,自動廢料收集回收都快成了大多數新生語言的標配了,雖然它的有很高的代價,但也消除了去檢查懸空引用的必要性了。關於這方面的爭執集中在兩方:以方便和安全爲主的一方,以性能爲主的另外一方。這也說明了一件事,編程中的不少地方的設計,架構等等方面都是在現實中作出權衡。
廢料收集通常有這兩種思想,就不詳細說了。
表具備遞歸定義的結構,它或者是空表,或者是一個有序對,有序對由一個對象和另外一個表組成。表對於函數式或者邏輯式語言程序設計很是適用,由於那裏的大多數工做都是經過遞歸函數或高階函數來完成的。
在Lisp中:
(cons 'a '(b)) => (a b) (car '(a b)) => a (cdr '(a b c)) => (b c) 複製代碼
在Haskell和Python還由一個很是有用的功能,叫作列表推導。在Python中能夠這樣推導出一個列表
[i * i for i in range(1, 100) if i % 2 == 1] 複製代碼
輸入/輸出(I/O)功能使程序能夠與外部世界通訊。在討論這種通訊時,將交互式I/O和文件I/O分開可能有些幫助。交互式IO一般意味着與人或物理設備通訊,人或設備都與運行着的程序並行工做,送給程序的輸入可能依賴程序在此以前的輸出。文件一般對應於程序的地址空間以外的存儲器,由操做系統實現。
有些語言提供了內置的File數據類型,另一些語言將IO工做徹底委託給庫程序包,這些程序包導出一個file類型。因此IO也算做是一種數據類型
對於簡單的基本數據類型,如整數、浮點數和字符,相等檢測和賦值相對來講都是直截了當的操做。其語義和實現也很明確,能夠直接按照二進制位方式比較或複製,可是,對於更加複雜或抽象的數據類型,就可能還須要其它的比較方式
就許多狀況下,當存在引用的狀況下,只有兩個表達式引用相同的對象時它們才相等,這種稱爲淺比較。而對於引用的對象自己存在相等的含義時,這種比較稱爲深比較。對於複雜的數據結構,進行深比較可能要進行遞歸的遍歷。因此相對來講,賦值也有深淺之分。深賦值時是進行完整的拷貝。
大多數的語言都使用淺比較和淺賦值
本文從語言爲什麼須要類型系統出發,解釋了類型系統爲語言提供了那些有價值的用途:1是爲許多操做提供隱含的上下文,使程序員在許多狀況下沒必要顯示的描述這種上下文;2是使得編譯器能夠捕捉更普遍的各類各樣的程序錯誤。 而後介紹了類型系統的三個重要規則:類型等價、類型相容、類型推理。以此3個規則推導出的強類型(毫不容許把任何操做應用到不支持該操做的對象上)、弱類型以及靜態類型化(在編譯階段貫徹實施強類型的性質)、動態類型化的性質以及在對語言的使用方面的影響。以及後續介紹了語言中常見的一些數據類型的用途以及語言在實現這種類型方面所遇到的問題以及其大體的實現方式。
4 byte/32bits | ||
name(2個字節) | 2個字節的空洞 | |
number(4個字節) | ||
weight (8個字節) |
||
metallic(1個字節) | 3個字節的空洞 |