變量

上一章:文本解析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 反覆定義了兩個變量 resultin-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))

可定義三個局部變量 abc,它們僅在 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))

結語

局部變量可以讓程序更安全,也更優雅。

相關文章
相關標籤/搜索