1. 程式人生 > >PL真有意思(五):資料型別

PL真有意思(五):資料型別

前言

現在大多數程式設計語言中都有表示式和/或物件的型別概念。型別起著兩種主要作用:

  • 為許多操作提供了隱含的上下文資訊,使程式設計師可以在許多情況下不必顯示的描述這種上下文。比如int型別的兩個物件相加就是整數相加、兩個字串型別的物件相加就是拼接字串、在Java和C#中new object()隱含在背後的就是要分配記憶體返回物件的引用等等。

  • 型別描述了其物件上一些合法的可以執行的操作集合。型別系統將不允許程式設計師去做一個字元和一個記錄的加法。編譯器可以使用這個合法的集合進行錯誤檢查,好的型別系統能夠在實踐中捕獲很多錯誤

型別系統

從編譯方面的知識我們可以知道,計算機硬體可以按多種不同的方式去解釋暫存器裡的一組二進位制位。處理器的不同功能單元可能把一組二進位制位解釋為指令、地址、字元、各種長度的整數或者浮點數等。當然,二進位制位本身是無型別的,對儲存器的哪些位置應該如何解釋,大部分硬體也無任何保留資訊。組合語言由於僅僅是對一些二進位制指令的“助記符號”翻譯,它也是這種無型別情況。高階語言中則總是關聯值與其型別,需要這種關聯的一些原因和用途就如前面說到的上下文資訊和錯誤檢測。

一般來說,一個型別系統包含一種定義型別並將它們與特定的語言結構關聯的機制;以及一些關於型別等價、型別相容、型別推理的規則。 必須具有型別的結構就是那些可以有值的,或者可以引用具有值得物件的結構。型別等價規則確定兩個值得型別何時相同;型別相容規則確定特定型別的值是否可以用在特定的上下文環境裡;型別推理規則基於一個表示式的各部分組成部分的型別以及其外圍上下文來確定這個表示式的型別。

在一些多型性變數或引數的語言中,區分表示式(如一個名字)的型別與它所引用的那個物件的型別非常重要,因為同一個名字在不同時刻有可能引用不同型別的物件。

在一些語言中,子程式也是有型別的,如果子程式是一級或者二級值,其值是動態確定的子程式,這時語言就需要通過型別資訊,根據特定的子程式介面(即引數的個數和型別)提供給這種結構的可接受的值集合,那麼子程式就必須具有型別資訊。在那些不能動態建立子程式引用的靜態作用域語言(這種語言中子程式是三級值),編譯器時就能確定一個名字所引用的子程式,因此不需要子程式具有型別就可以保證子程式的正確呼叫。

型別檢查

型別檢查時一個處理過程,其目的就是保證程式遵循了語言的型別相容規則,違背這種規則的情況稱為型別衝突。說一個語言是強型別的,那麼就表示這個語言的實現遵循一種禁止把任何操作應用到不支援這種操作的型別物件上的規則。說一個語言是靜態型別化(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 68、Modula-3、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寫了,會被擠到最後去)

(table標籤和我部落格園的樣式生成的時候會出bug,刪除了)

在對結構體的儲存佈局方案上,如果使用正常排序,結構中的空洞會浪費空間。但是如果通過壓縮來節省空間,但是可能很帶來很嚴重的訪問時間的代價

陣列

陣列是最常見也是最重要的複合資料型別。記錄用於組合一些不同型別的域在一起;而陣列則不同,它們總是同質的。從語義上看,可以把陣列想象成從一個下標型別到成員(元素)型別的對映。

有些語言要求下標型別必須是integer,也有許多語言允許任何離散型別作為下標;有些語言要求陣列的元素型別只能是標量,而大多數語言則允許任意型別的元素型別。也有一些語言允許非離散型別的下標,這樣產生的關聯陣列只能通過散列表的方式實現,而無法使用高效的連續位置方式儲存,比如C++中的map,C#中的Dictionary。在本節中的討論中我們假定陣列的下標是離散的。

語法和操作

大多數的語言都通過陣列名後附加下標的方式(圓括號|方括號)來引用數組裡的元素。由於圓括號()一般用於界定子程式呼叫的實際引數,方括號在區分這兩種情況則有易讀的優勢。Fortran的陣列用圓括號,是因為當時IBM的打卡片機器上沒有方括號

維數、上下界和分配

對於陣列的形狀在宣告中就已經描述,對於這種有靜態形狀的陣列,可以用通常的方式來管理記憶體:生存期是整個程式的陣列使用棧分配,具有更一般的生存期的動態生成陣列使用堆分配。但是對於在加工之前不知道其形狀的陣列,或其形狀在執行期間可能改變的陣列,儲存管理就會更復雜一點。

  • 內情向量

在編譯期間,符號表維護者程式中的每個陣列的維度和邊界資訊。對於每個記錄,它還維護著每個域的偏移量。如果陣列維度的數目和邊界是靜態已知的,編譯器就可以在符號表中找出它們,以便計算陣列元素的地址。如果這些值不是靜態已知的,則編譯器就必須生成程式碼,在執行時從一個叫內情向量的資料結構來查詢它

  • 棧分配

子程式引數是動態形狀陣列最簡單的例子,其中陣列的上下界在執行時才確定,呼叫方都會傳遞陣列的資料和一個適當的內情向量,但是如果一個數組的形狀只能到加工時才知道,這種情況下仍可以在子程式的棧幀裡為陣列分配空間,但是需要多做一層操作

  • 堆分配

在任意時間都可以改變形狀的陣列,有時被稱為是完全動態的。因為大小的變化不會以先進先出的順序進行,所以棧分配就不夠用了。完全動態的陣列必須在堆中分配。比如Java中的ArrayList

#### 記憶體佈局

大多數語言的實現裡,一個數組都存放在記憶體的一批連續地址中,比如第二個元素緊挨著第一個,第三個緊挨著第二個元素。對於多維陣列而言,則是一個矩陣,會出現行優先和列優先的選擇題,這種選擇題對於語言使用者而言是透明的,而對語言的實現者則需要考慮底層方面的優化問題了。

在一些語言中,還有另外一種方式,對於陣列不再用連續地址分配,也不要求各行連續存放,而是允許放置在記憶體的任何地方,再建立一個指向各元素的輔助指標陣列,如果陣列的維數多於兩維,就再分配一個指向指標陣列的指標陣列。這種方式稱為行指標佈局,這種方式需要更多的記憶體空間,但是卻有兩個優點:

  • 首先,可能加快訪問數組裡單獨元素的速度;
  • 其次,允許建立不用長度的行,而且不需要再各行的最後留下對齊所用的空洞空間,這樣節省下來的空間有時候可能會超過指標佔據的空間。C,C++和C#都支援連續方式或行指標方式組織多維陣列,從技術上講,連續佈局才是真正的多維陣列,而行指標方式則只是指向陣列的指標陣列。

字串

許多語言中,字串也就是字元的陣列。而在另一些語言中,字串的情況特殊,允許對它們做一些其他陣列不能用的操作,比如Icon以及一些指令碼語言中就有強大的字串操作功能。

字串是程式設計中非常重要的一個數據型別,故而很多語言都對字串有特殊的處理以便優化其效能以及儲存(比如C#中的字串不可變性保證了效能,字串駐留技術照顧了儲存方面的需要),由於這些特殊的處理,故而各各語言中為字串提供的操作集合嚴重依賴語言設計者對於實現的考慮。

集合

程式設計語言中的一個集合,也就是具有某個公共型別的任意數目的一組值的一種無序彙集。集合的元素所具有的型別叫做元型別或者基型別。現在的大多數程式設計語言都對集合提供了很大的支援,為集合提供了很多相關的操作

指標和遞迴型別

所謂的遞迴型別,就是可以在其物件中包含一個或多個本型別物件的引用型別。遞迴型別用於構造各種各樣的“連結”資料結構,比如樹。在一些對變數採用引用模型的語言中,很容易在建立這種遞迴型別,因為每個變數都是引用;在一些對變數採用值模型的語言中,定義遞迴型別就需要使用指標的概念,指標就是一種變數,其值是對其他物件的引用。

對於任何允許在堆裡分配新物件的語言,都存在一個問題:若這種物件不在需要了,何時以及以何種方式收回物件佔用的空間?對於那些活動時間很短的程式,讓不用的儲存留在那裡,可能還可以接受,畢竟在它不活動時系統會負責回收它所使用的任何空間。但是大部分情況下,不用的物件都必須回收,以便騰出空間,如果一個程式不能把不再使用的物件儲存回收,我們就認為它存在“記憶體洩漏”。如果這種程式執行很長一段時間,那麼它可能就會用完所有的空間而崩潰。許多早期的語言要求程式設計師顯示的回收空間,如C,C++等,另一些語言則要求語言實現自動回收不再使用的物件,如Java,C#以及所有的函式式語言和指令碼語言。顯示的儲存回收可以簡化語言的實現,但會增加程式設計師忘記回收不再使用的物件(造成記憶體洩漏),或者不當的回收了不該回收的正在使用的物件(造成懸空引用)的可能性。自動回收可以大大簡化程式設計師的工作,但是為語言的實現帶來了複雜度。

語法和操作

對指標的操作包括堆中物件的分配和釋放,對指標間接操作以訪問被它們所指的物件,以及用一個指標給另一個指標賦值。這些操作的行為高度依賴於語言是函式式還是命令式,以及變數/名字使用的是引用模型還是值模型。

函式式語言一般對名字採用某種引用模型(純的函式式語言里根本沒有變數和賦值)。函式式語言裡的物件傾向於採取根據需要自動分配的方式。

命令式語言裡的變數可能採用值模型或引用模型,有時是兩者的某種組合。比如 A=B;

  • 值模型: 把B的值放入A。
  • 引用模型: 使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個規則推匯出的強型別(絕不允許把任何操作應用到不支援該操作的物件上)、弱型別以及靜態型別化(在編譯階段貫徹實施強型別的性質)、動態型別化的性質以及在對語言的使用方面的影響。以及後續介紹了語言中常見的一些資料型別的用途以及語言在實現這種型別方面所遇到的問題以及其大致的實現方式