上一章:文本解析segmentfault
上一章實現的解析器程序——固然僅僅是玩具,有幾處頗爲醜陋,還有一處存在着安全問題。安全
安全第一。先從安全問題開始。觀察如下代碼:函數
(defun text-match (src dest) (setq n (length dest)) (if (< (length src) n) nil (string= (substring src 0 n) dest)))
上述代碼定義的這個函數可判斷字符串對對象 src
的內容是否以字符串對象 dest
的內容做爲開頭,例如code
(princ\' (text-match "I have a dream!" "I have"))
輸出 t
。這不是問題。問題在於假若緊接着執行對象
(princ\' n)
輸出 6
。字符串
問題是什麼呢?在 text-match
這個函數定義的外部,可以訪問在函數的定義內部的一個變量,宛若他人的手指能夠觸及個人內臟……這是否是一個安全問題?get
這種匪夷所思的現象之因此出現,是由於 setq
定義的變量是全局變量。在一個程序裏,假若有一個全局變量,那麼在這個程序的任何一個角落皆能訪問和修改這個變量。string
全局變量不能夠沒有,但不可濫用。對於 text-match
這樣的函數,在其定義裏使用全局變量,屬於濫用。class
回憶一下 simple-md-parser.el 裏的代碼裏 every-line
函數的定義:變量
(defun every-line (result in-code-block) (if (= (point) (point-max)) result (progn (if (text-match (current) "```") (progn (if in-code-block (progn (setq result (cons '代碼塊結束 result)) (setq in-code-block nil)) (progn (setq result (cons '代碼塊開始 result)) (setq in-code-block t)))) (progn (if in-code-block (setq result (cons '代碼塊 result)) (setq result (cons '未知 result))))) (forward-line 1) (every-line result in-code-blcok))))
在這個函數裏,我在多處用 setq
反覆定義了兩個變量 result
和 in-code-block
,可是假若調用這個函數以後再執行如下程序
(princ\' result) (princ\' in-code-block)
Elisp 解釋器在對 (princ\' result)
進行求值時會出錯,它會抱怨:
Symbol’s value as variable is void: result
意思是,result
這個變量未被定義。爲何會這樣呢?
緣由是它們也都是函數的參數,在函數定義的內部能夠訪問和修改它們,而在函數定義的外部卻不能。所以,函數的參數是局部變量。
Elisp 語言以及其餘 Lisp 方言,正是基於函數的參數構造了局部變量,而且爲了簡化構造過程,提供了 let
表達式。
let
表達式能夠初始化局部變量,並將限定其生存範圍。例如
(let ((a 1) (b "Hello") (c '世界)) (princ\' a) (princ\' b) (princ\' c))
可定義三個局部變量 a
,b
和 c
,它們僅在 let
表達式內部有效——能夠使用,也能夠修改。
使用 let
表達式,可讓不安全的 text-match
函數規矩一些:
(defun text-match (src dest) (let ((n (length dest))) (if (< (length src) n) nil (string= (substring src 0 n) dest))))
如今,假若再執行
(princ\' (text-match "I have a dream!" "I have")) (princ\' n)
Elisp 解釋器在對 (princ\' n)
求值時會抱怨變量 n
未定義,而後終止。
在 let
表達式裏,也能夠不對局部變量進行初始化。例如
(let (a b c) (princ\' a) (princ\' b) (princ\' c))
結果輸出:
nil nil nil
未進行初始化的局部變量,Elisp 解釋器會認爲它們的值是 nil
。
局部變量不只能讓函數更爲安全,甚至對函數的定義和調用也能產生一些美容效果。
simple-md-parser.el 裏定義的 every-line
函數,其調用形式是
(every-line '() nil)
須要給它兩個初始的參數值,它方能得以運行。雖然它能正確地解決問題,可是卻不美觀,猶如一件電器,它能正常工做,只是有兩個線頭露在了外面。基於 let
表達式,在函數的定義能夠去掉這兩個參數。例如:
(let ((result '()) (in-code-block nil)) (defun every-line () (if (= (point) (point-max)) result (progn (if (text-match (current) "```") (progn (if in-code-block (progn (setq result (cons '代碼塊結束 result)) (setq in-code-block nil)) (progn (setq result (cons '代碼塊開始 result)) (setq in-code-block t)))) (progn (if in-code-block (setq result (cons '代碼塊 result)) (setq result (cons '未知 result))))) (forward-line 1) (every-line)))) (every-line))
上述代碼因爲略微複雜,致使程序結構不夠清晰,假若隱去一些代碼,便清楚得多。例如
(let ((result '()) (in-code-block nil)) (defun every-line () ... 省略的代碼 ...) (every-line))
所表達的主要含義是:在 let
表達式裏定義了函數 every-line
,而後調用該函數。注意觀察,此時,該函數是沒有任何參數。
不過,將函數的定義放到 let
表達式內,這個函數會被 Elisp 就地求值了。假若依然但願它保持函數的尊嚴,而不是每次使用它都要揹負一個冗長的 let
表達式,只需將整個 let
表達式封裝爲一個函數便可。例如
(defun every-line\' () (let ((result '()) (in-code-block nil)) (defun every-line () ... 省略的代碼 ...) (every-line)))
上述代碼不只彰顯了能夠在 let
表達式裏定義一個函數,也彰顯了能夠在一個函數的定義裏定義一個函數。不過,我認爲內外兩個函數的名字最好換一下,即
(defun every-line () (let ((result '()) (in-code-block nil)) (defun every-line\' () ... 省略的代碼 ...) (every-line\')))
如今,我以爲美觀多了。由於 simple-md-parser.el 的最後兩行代碼,如今能夠寫成
(find-file "foo.md") (princ\' (every-line))
對於上一章實現的列表反轉函數也能夠採用相似的辦法予以美化。例如
(defun reverse-list (x) (let ((y '())) (defun reverse-list\' () (if (null x) y (progn (setq y (cons (car x) y)) (reverse-list\' (cdr x))))) (reverse-list\')))
如此,以前的代碼
(setq x '(5 4 3 2 1)) (princ\' (reverse-list x '()))
如今可寫成
(setq x '(5 4 3 2 1)) (princ\' (reverse-list x))
局部變量可以讓程序更安全,也更優雅。