上一章:文本解析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))
局部变量可以让程序更安全,也更优雅。