這兩篇寫了詞法分析和語法分析,比較偏向實踐。這一篇來看一下語言設計裏一個比較重要的部分:名字。在大部分語言裏,名字就是標識符,若是從抽象層面來看名字就是對更低一級的內存之類的概念的一層抽象。可是名字還有其它相關的好比它的約束時間和生存週期等等程序員
約束就是兩個東西之間的一種關聯,例如一個名字和它所命名的事物,約束時間就是指建立約束的時間。有關的約束能夠在許多不一樣的時間做出算法
這就是爲何基於編譯的語言實現一般會比基於解釋器的語言的實現更高效的緣由,由於基於編譯的語言在更早的時候就作了約束,好比對於全局變量在編譯時就已經肯定了它在內存中的佈局了數據結構
在名字和它們所引用的對象的約束之間有幾個關鍵事件:閉包
對象的生存期和存儲分配機制有關模塊化
全局變量是靜態對象最顯而易見的例子,還有構成程序的機器語言翻譯結果的那些指令,也能夠看做是靜態分配對象。函數
還有像每次調用函數都會保持相同的值的局部變量也是靜態分配的。對於數值和字符串這些常量也是靜態分配。佈局
還有用來支持運行時的各類程序,好比廢料收集和異常處理等等也能夠看做是靜態分配優化
若是一種語言容許遞歸,那麼局部變量就不能使用靜態分配的方式了,由於在同一時刻,一個局部變量存在的實例個數是不肯定的翻譯
因此通常對於子程序,都用棧來保存它相關的變量信息。在運行時,一個子程序的每一個實例都在棧中有一個相應的棧幀,保存着它的參數、返回值、局部變量和一些簿記信息設計
堆是一塊存儲區域,其中的子存儲塊能夠在任意時間分配與釋放。由於堆具備它的動態性,因此就須要對堆空間進行嚴格的管理。許多存儲管理算法都維護着堆中當前還沒有使用的存儲塊的一個連接表,稱爲自由表。
初始時這個表只有一個塊,就是整個堆,每當遇到分配請求時,算法就在表中查找一個大小適當的塊。因此當請求次數增多,就會出現碎片問題,也須要相應的解決
因此有廢料收集的語言其實就是對堆的管理
一個約束起做用的那一段程序正文區域,稱爲這個約束的做用域。
如今大多數語言使用的都是靜態做用域,也就是在編譯時就肯定了。也有少數語言使用動態做用域,它們的約束須要等到運行時的執行流才能肯定
在使用靜態做用域的語言,也叫做詞法做用域。通常當前的約束就是程序中包圍着一個給定點的最近的,其中有與該名字匹配的聲明的那個快中創建的那個約束。好比C語言在進入子程序時,若是局部變量和全局變量,那麼當前的約束就是與局部變量關聯,直到退出子程序才撤銷這個約束
可是有的語言提供了一種能夠提供約束的生存期的機制,好比Fortran的save和C的static
有許多語言容許一個子程序嵌套在另外一個子程序的。這樣有關約束的定義一般來講都是首先用這個名字在當前、最內層的做用域中查找相應的聲明,若是找不到就直接到更外圍的做用域查找當前的約束,直到到達全局做用域,不然就發生一個錯誤
訪問非局部變量
上面提到的訪問外圍做用域的變量,可是當前子程序只能訪問到當前的棧幀,因此就須要一個調用幀鏈來讓當前的做用域訪問到外圍做用,經過調用順序造成一個靜態鏈
關於約束還有一個問題,就是在同一做用域裏,先聲明的名字是否能使用在此以後的聲明
在Pascal裏有這樣兩條規則:
因此在這兩個的相互做用下,會形成一個讓人吃驚的問題
const N = 10; procedure foo; const M = N; (*靜態語義錯誤*) N = 20;
可是在C、C++和Java等語言就不會出現這個問題,它們都規定標識符的做用域不是整個塊,而是從其聲明到塊結束的那一部分
而且C++和Java還進一步放寬了規則,免除了使用以前必須聲明的要求
恰當模塊化的代碼能夠減小程序員的思惟負擔,由於它最大限度的減小了理解系統的任意給定部分時所需的信息量。在設計良好的程序中,模塊之間的接口應儘量的小,全部可能改變的設計決策都隱藏在某個模塊裏。
模塊能夠將一組對象(如子程序、變量、類型)封裝起來。使得:
模塊使咱們很容易的建立各類抽象,可是若是須要多個棧的實例,那麼就須要一個讓模塊成爲一個類型的管理器。這種管理器組織方式通常都是要求在模塊中增長建立/初始化函數,並給每個函數增長一個用於描述被操做的實例
對於像這種多實例的問題,除了管理器,在許多語言裏的解決方法都是能夠將模塊看做是類型。當模塊是類型的時候,就能夠將當前的方法認爲是屬於這個類型的,簡單來講就是調用方法變化了
push(A, x) -> A.push(x)
本質上的實現區別不大
在更面向對象裏的方法裏,能夠把類看做是一種擴充了一種繼承機制的模塊類型。繼承機制鼓勵其中全部操做都被看做是從屬於對象的,而且新的對象能夠從現有對象繼承大部分的操做,而不須要爲這些操做重寫代碼。
類的概念最先應該是起源於Simula-67,像後來的C++,Java和C#中的類的思想也都起源於它。類也是像Python和Ruby這些腳本語言的核心概念
從模塊到模塊類型再到類都是有其思想基礎,可是最初都是爲了更好的數據抽象。可是即便有了類也不能徹底取代模塊,因此許多語言都提供了面向對象和模塊的機制
在使用動態做用域的語言中,名字與對象間的約束依賴於運行時的控制流,特別是依賴子程序的調用順序
n : integer procedure first n := 1 procedure second n : integer first() n := 2 if read_integer() > 0 second() else first() write_integer()
這裏最後的輸出結果徹底取決於read_integer讀入的數字的正負,若是爲正,輸出就爲2,不然就打印一個1
爲了跟蹤靜態做用域程序中的哥哥名字,編譯器須要依靠一個叫作符號表的數據結構。從本質上看,符號表就是一個記錄名字和它已知信息的映射關係的字典,可是因爲做用域規則,因此還須要更強大的數據結構。像以前那個寫編譯器系列的符號表就是使用哈希表加上同一層做用域鏈表來實現的
而對於動態做用域來講就須要在運行時執行一些操做
在基於指針的數據結構使用別名是很天然的狀況,可是使用別名可能會致使編譯器難以優化或者形成像懸空引用的問題,因此須要謹慎使用
在大多數語言中都或多或少的提供了重載機制,好比C語言中(+)能夠被用在整數類型也能夠用在浮點數類型,還有Java中的String類型也支持(+)運算髮
要在編譯器的符號表中處理重載問題,就須要安排查找程序根據當前的上下文環境返回一個有意義的符號
好比C++、Java和C#中的類方法重載均可以根據當前的參數類型和數量來判斷使用哪一個符號
C++、C#和Haskell都支持用戶定義的類型重載內部的算術運算符,在C++和C#的內部實現中一般是將A+B看做是operator+(A, B)的語法糖
對於名字,除了重載還有兩個重要的概念:強制和多態。這三個概念都用於在某些環境中將不一樣類型的參數傳給一個特定名字的子程序
強制是編譯器爲了知足外圍環境要求,自動將某類型轉換爲另外一類型的值的操做
因此在C中,定義一個計算整數或者浮點數兩個值中的最小值的函數
double min(double x, double y);
只要浮點數至少有整數那麼多有效二進制位,那麼結果就必定會是正確的。由於編譯器會對int類型強制轉換爲double類型
這是強制提供的方法,可是多態性提供的是,它使同一個子程序能夠不加轉換的接受多種類型的參數。要使這個概念有意義,那麼這多種類型確定要具備共同的特性
顯式的參數多態性就叫作泛型,像Ada、C++、Clu、Java和C#都支持泛型機制,像剛纔的例子就能夠在Ada中用泛型來實現
generic type T is private; with function "<" (x, y : T) return Boolean; function min(x, y : T) return T; function min(x, y : T) return T is begin if x < y then return x; else return y; end if; end min function string_min is new min(string, "<") function date_min is new min(date, date_precedes);
像List和ML中就能夠直接寫
(define min (lambda (a b) (if (< a b) a b)))
其中有關類型的任何細節都由解釋器處理
提到引用環境的約束就有兩種方式:淺約束和深約束
推遲到調用時創建約束的方式淺約束。通常動態做用域的語言默認是淺約束,固然動態做用域和深約束也是能夠組合到一塊兒的。
執行時依然使用傳遞時的引用環境,而非執行時的引用環境。那麼這種規則稱爲深約束,通常靜態做用域的語言默認是深約束
爲了實現神約束,須要建立引用環境的一種顯示錶示形式,並將它與對有關子程序的引用捆綁在一塊兒,這樣的捆綁叫作閉包
總而言之,若是子程序能夠被看成參數傳遞,那麼它的引用環境同樣也會被傳遞過去
通常而言,在語言中,若是一個值能夠賦值給變量、能夠看成參數傳遞、能夠從子程序返回,那麼它被稱爲具備一級狀態(和咱們在js中說函數是一等公民一個含義)。大多數的語言中數據對象都是一級狀態。二級狀態是隻能看成參數傳遞;三級值則是連參數也不能作,好比C#中一些+-*/等符號。
在一級子程序會出現一個複雜性,就是它的生存期可能持續到這個子程序的做用域的執行期外。爲了不這一問題,大部分函數式語言都表示局部變量具備非受限的生命週期,它們的生命週期無限延長,直到GC能證實這些對象不再使用了纔會撤銷。那麼不撤銷帶來的問題就是這些子程序的存儲分配基於棧幀是不行了,只能是基於堆來分配管理。爲了維持能基於棧的分配,有些語言會限制一級子程序的能力,好比C++,C#,都是不容許子程序嵌套,也就從根本上不會存在閉包帶來的懸空引用問題。
這一篇從名字入手,介紹了名字與其背後的對象的約束關係、以及約束時間的概念;而後介紹了對象的分配策咯(靜態、棧、堆);緊接着討論了名字與對象之間創建的約束的生命週期,並由此引出了做用域的概念;進一步延伸出多個約束組成的引用環境的相關概念以及問題。