靜態作用域和動態作用域
所謂的作用域就是指某段程序文本代碼。一個聲明起作用的那一段程序文本區域,則稱為這個聲明的作用域。靜態作用域是指聲明的作用域是根據程序正文在編譯時就確定的,有時也稱為詞法作用域。而在采用動態作用域的語言中,程序中某個變量所引用的對象是在程序運行時刻根據程序的控制流信息來確定的。
大多數現在程序設計語言都是采用靜態作用域規則,而只有為數不多的幾種語言采用動態作用域規則,包括APL、Snobol和Lisp的早期方言。而采用靜態作用域的語言中,基本都是最內嵌套作用域規則:由一個聲明引進的標識符在這個聲明所在的作用域里可見,而且在其內部嵌套的每個作用域里也可見,除非它被嵌套於內部的對同名標識符的另一個聲明所掩蓋。為了找到某個給定的標識符所引用的對象,應該在當前最內層作用域里查找。如果找到了一個聲明,也就可以找到該標識符所引用的對象。否則我們就到直接的外層作用域里去查找,並繼續向外順序地檢查外層作用域,直到到達程序的最外嵌套層次,也就是全局對象聲明所在的作用域。如果在所有層次上都沒有找到有關聲明,那么這個程序就有錯誤。
設想標識符x出現在某個函數體中,而x又不是在該函數體內定義的,那么x的值必然依賴於該函數外部的某個聲明。那么在程序運行時,程序要查找標識符x所引用的對象則必須依據該語言所采用的作用域規則。靜態作用域和動態作用域的一個重要區別在於:靜態作用域規則查找一個變量聲明時依賴的是源程序中塊之間的靜態關系;而動態作用域規則依賴的是程序執行時的函數調用順序。說的具體點,就是靜態作用域查找的是距離當前作用域最近的外層作用域中同名標識符的聲明,而動態作用域則是查找最近的活動記錄(關於活動記錄見我的上一篇隨筆)中的同名標識符聲明。下面我們通過一個小例子來解釋這點。
下面是一小段C語言代碼(我們這里假設不同的聲明在不同的塊,也對應着每個聲明在不同的活動記錄中)。
1 int x = 1;
2 int g(int z) { return x + z; }
3 int f(int y)
4 {
5 int x = y + 1;
6 return g(y*x);
7 }
8 f(3);
調用f(3)會引起函數f內部調用g(12),於是函數g的定義中的表達式x+z會被執行。調用g之后,控制棧中的活動記錄包括最外層x的定義、對函數f的調用和對g的調用,如圖1所示。
圖 1
此時控制棧中有兩個名為x的變量,一個在最外層作用域中定義,另一個是函數f的局部變量。在采用動態作用域規則的情況下,表達式x+z中的x會從最近的活動記錄中尋找變量x的值,也即函數調用f(3)的活動記錄中的x值,則x的值4;而如果采用靜態作用域規則,表達式x+z中的變量x則從函數g定義所在作用域的外層作用域中尋找,此時x的值為1。
從上面這個簡單的例子,我們初步了解了靜態作用域和動態作用域對變量查找的不同處理規則。但在語言的實現中,系統如何去維護靜態作用域呢?下面我們簡單地了解一下活動記錄中的訪問鏈。
活動記錄的訪問鏈
在采用靜態作用域和塊結構的語言中,控制棧的活動記錄中通過訪問鏈來維護靜態作用域。一個活動記錄的訪問鏈(access link)指向源程序中最近的外層塊所對應的活動記錄。訪問鏈有時也稱為靜態鏈(static link)。下面我們就通過上面那個C語言程序片段來簡單了解訪問鏈的控制。當執行語句f(3),並在函數f內部調用g(12)后,控制棧中部分的活動記錄如圖2所示。
圖 2
這里假設C語言中每個聲明視為在不同的塊中,即每個聲明都需要一個活動記錄。圖2中左邊的箭頭表示的是控制鏈,右邊的箭頭表示的是訪問鏈。其中每個控制鏈都是指向前一個活動記錄,而訪問鏈而可能跳過若干活動記錄,但都是指向控制棧中之前的某個活動記錄。另外需要注意一下幾點:
- 函數g聲明位於變量x的聲明所在的作用域內部,所以包含函數g聲明的活動記錄的訪問鏈指向變量x聲明的活動記錄。同理,包含函數f聲明的活動記錄的訪問鏈指向包含函數g聲明的活動記錄。
- 執行函數調用f(3)時,壓入一個與函數f的函數體相關的活動記錄,在創建這個活動記錄時,系統會去尋找包含f聲明的活動記錄(記為f_record),然后將新創建的活動記錄的訪問鏈指向活動記錄f_record。
- 執行函數調用g(12)時,新建一個與函數g的函數體相關的活動記錄,然后壓入控制棧中。該活動記錄的訪問鏈則指向包含函數g聲明的活動記錄。
- 在執行表達式x+z時,在函數調用g(12)的活動記錄中沒有找到有關變量x的聲明,則通過該活動記錄的訪問鏈找到包含函數g聲明的活動記錄,依然沒有找到變量x的聲明,然后繼續沿着訪問鏈向上查找,最終找到最外層作用域中的變量x的聲明(即x=1)。
上面通過一個簡單的例子說明了活動記錄的訪問鏈如何維護靜態作用域規則。實際上,只有在允許函數定義在另一個函數定義內部或者其他嵌套塊內部的語言才需要訪問鏈。C語言的所有函數定義都是在最外層定義域中的,所以不需要訪問鏈(本文C語言示例中對聲明的處理做了假設)。