乐趣区

关于lisp:变量

上一章:文本解析

上一章实现的解析器程序——当然仅仅是玩具,有几处颇为俊俏,还有一处存在着平安问题。

全局变量

平安第一。先从平安问题开始。察看以下代码:

(defun text-match (src dest)
  (setq n (length dest))
  (if (< (length src) n)
      nil
    (string= (substring src 0 n) dest)))

上述代码定义的这个函数可判断字符串对对象 src 的内容是否以字符串对象 dest 的内容作为结尾,例如

(princ\'(text-match"I have a dream!""I have"))

输入 t。这不是问题。问题在于假使紧接着执行

(princ\' n)

输入 6

问题是什么呢?在 text-match 这个函数定义的内部,可能拜访在函数的定义外部的一个变量,宛若别人的手指能够涉及我的内脏……这是不是一个平安问题?

这种匪夷所思的景象之所以呈现,是因为 setq 定义的变量是全局变量。在一个程序里,假使有一个全局变量,那么在这个程序的任何一个角落皆能拜访和批改这个变量。

全局变量不能够没有,但不可滥用。对于 text-match 这样的函数,在其定义里应用全局变量,属于滥用。

局部变量

回顾一下 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))

结语

局部变量可让程序更平安,也更优雅。

退出移动版