Emacs 折腾日记(十二)——变量
本文是依据 emacs lisp 简明教程 而来
在此之前我们已经了解了elisp中的全局变量和函数中的局部变量,也了解了elisp中各种数据类型。这一篇主要谈谈elisp中各种变量的生命周期和作用域
let 绑定的变量
使用let绑定的变量只在let范围内有效,如果是多层嵌套的let,只有最里层的那个变量是有效的,用 setq 改变的也只是最里层的变量,而不影响外层的变量。比如
(progn
(setq foo "I'm global variable!")
(let ((foo 5))
(message "foo value is: %S" foo) ;; ==> "foo value is: 5"
(let (foo)
(setq foo "I'm local variable!")
(message foo)) ;; ==> "i’m local variable!"
(message "foo value is still: %S" foo)) ;; ==> "foo value is still: 5"
(message foo)) ;; ==> "i’m global variable!"
这个有点像C/C++中的{} 定义的语句块中的变量只在当前 {} 内有效,当内部变量与外部变量重名的时候只影响{}内,而不影响外部。我们可以给出这样的c++代码
int n = 0;
printf("n = %d\n", n);
{
int n = 10;
printf("n = %d\n", n);
}
printf("n = %d\n", n);
elisp中的let两边的括号就有点像这里的 {},出了这个范围定义的变量就无效了。
但是定义变量使用的是栈空间,而程序的栈空间是有大小限制的,一旦超过这个范围就会发生栈溢出。在C/C++程序中一般发生在递归层数过大。我们可以在编译时修改这个栈空间的大小。
在elisp中也有变量控制递归层数,它就是 max-specpdl-size 但是在最新的29中已经将它弃用,新版的emacs可以使用 max-lisp-eval-depth 来限制specpdl堆栈的大小,该堆栈主要用于 存储动态变量绑定和 unwind-protect 激活等。
buffer-local 变量
顾名思义,它的值只在当前buffer中生效,在其他buffer中可能是另外的值。它有点像之前介绍的vim中的setlocal变量,仅在当前缓冲区内生效。该特性常用于实现缓冲区特定的配置和行为。
声明一个 buffer-local 的变量可以用 make-variable-buffer-local 或用 make-local-variable。这两个函数的区别在于前者是在所有缓冲区都创建一个 buffer-local 的变量。而后者只在声明时所在的缓冲区内产生一个局部变量,而其它缓冲区仍然使用的是全局变量。一般来说推荐使用 make-local-variable。
下面来举例说明它们的区别,在举例之前介绍例子中用到的函数或者宏
with-current-buffer, 它的使用方式是
(with-current-buffer buffer
body)
其中 buffer 可以是一个缓冲区对象,也可以是缓冲区的名字。它的作用是使其中的 body 表达式在指定的缓冲区里执行。
default-value 可以访问符号所对应的全局变量
下面是使用 make-local-variable 创建buffer-local变量的例子
(setq foo "i'm a global variable!")
(make-local-variable 'foo)
foo ;; ==> "i'm a global variable!"
(setq foo "i'm buffer-local variable in scratch buffer")
foo ;; ==> "i'm buffer-local variable in scratch buffer"
(with-current-buffer "*Messages*"
(progn
(message "%s" foo) ;; ==> "i'm a global variable!"
(setq foo "i'm buffer-local variable in message buffer")
(message "%s" foo))) ;; ==> "i'm buffer-local variable in message buffer"
(default-value 'foo) ;; ==> i'm buffer-local variable in message buffer
上述代码因为message buffer 中未定义foo的buffer-local 变量,所以它修改的是全局变量的值,我们使用 default-value 发现全局变量的值被修改了。
(setq foo "i'm a global variable!")
(make-variable-buffer-local 'foo)
foo
(setq foo "i'm buffer-local variable in scratch buffer")
foo
(with-current-buffer "*Messages*"
(progn
(message "%s" foo)
(setq foo "i'm buffer-local variable in message buffer")
(message "%s" foo)))
(default-value 'foo) ;; ==> "i'm a global variable!"
前面的结果都一样,但是我们关注一下最后输出的全局的 foo 变量,它的值没有被修改,而使用 make-local-variable 的时候它被修改了。这是因为 make-variable-buffer-local 会在每一个缓冲区内都创建一个 buffer-local 的拷贝,所以后面在 message 缓冲区中修改的是缓冲区内自己的 buffer-local 变量而不影响全局变量的值,而 make-local-variable则不同,它会在 message 缓冲区中为foo也创建一个buffer-local变量。message 缓冲区中修改的是 buffer-local 变量,不影响全局的foo变量。
一般来说根据实际情况选择使用哪种,我并不是资深的elisp开发者,以我浅薄的认知,一般使用 make-local-variable情况较多,它影响面较小,仅仅影响当前缓冲区。而make-variable-buffer-local 影响面较大,一旦使用它设置了local-buffer 变量,那么在后面其他缓冲区中想要使用 setq 设置全局的值就没那么简单了。这种一般是需要隐藏起来的核心变量,例如某些功能依靠这个变量来驱动,一旦修改了可能导致后面的代码运行行为不准确。可能会使用 make-variable-buffer-local 定义,不让用户自己随便修改全局的值。
我们可以使用 setq-default 来设置全局变量的值。这里的例子就不给出了,各位读者可以根据上面的例子稍加修改就可以了。
测试一个变量是不是 buffer-local 可以用 local-variable-p。
这里我们再介绍一个新的函数 get-buffer。它需要一个buffer的名称作为参数,返回这个buffer的对象,如果未找到对应的buffer,则返回nil。
(setq foo 5)
(make-local-variable 'foo)
(local-variable-p 'foo) ;; ==> t
(local-variable-p 'foo (get-buffer "*Messages*")) ;; ==> nil
如果要在当前缓冲区里得到其它缓冲区的 buffer-local 变量的值可以用 buffer-local-value
(setq foo "i'm a global variable!")
(make-local-variable 'foo)
(setq foo "i'm buffer-local variable in scratch buffer")
(with-current-buffer "*Messages*"
(buffer-local-value 'foo (get-buffer "*scratch*"))) ;; ==> "i'm buffer-local variable in scratch buffer"
变量的作用域
在之前我们已经介绍过几种变量,分别是
- 使用
setq或者defvar定义的变量,它们是全局变量,即使在一些语法块或者函数中定义的,在外围也可以正常访问 - 使用
let或者let*定义的变量,只在let语法块中有效 - 使用
make-local-variable或者make-variable-buffer-local定义的buffer-local 变量,在当前buffer中有效
但是函数参数列表的变量生命周期与我们平常在C/C++、Java、Python等语言中有些不同。
作用域(scope)是指变量在代码中能够访问的位置。emacs lisp 这种绑定称为 indefinite scope。indefinite scope 也就是说可以在任何位置都可能访问一个变量名。而 lexical scope(词法作用域)指局部变量只能作用在函数中和一个块里(block)。
比如 let 绑定和函数参数列表的变量在整个表达式内都是可见的,这有别于其它语言词法作用域的变量。先看下面这个例子
(defun foo(x)
(getx))
(defun getx()
x)
(message "%s" (foo "hello,x")) ;; ==> "hello,x"
我们可以看到最终成功输出了结果,而根据之前学习C/C++的经验,在C、C++等语言中,这样的代码是无法执行成功的,因为在getx中并未定义x的值。但是在elisp 中,foo函数执行期间,x变量的都是有效的且可以正常访问到的。当然,在elisp中,let也具有这一效果,在let的语法块中,定义的变量总是有效的。例如
(let ((x "hello, x"))
(message "%s" (getx)))
(defun getx()
x)
在let语句块中 x 是一直有效的,如果脱离let,在最外层调用 getx 将会得到一个x未定义的错误
需要注意的是,上面的例子无法再使用
C-x C-e来一条条的执行了,这个时候需要使用eval-buffer来执行整个缓冲区的代码。
emacs 从 24.1 版本开始,引入了 lexical binding 这一特性,在代码中如果启用这个属性,那么它将采用C/C++ 等普通编程语言的那种 lexical scope 的作用形式。官方文档中提到使用 lexical binding 特性可以提高代码的运行效率,并且鼓励使用。可以在代码文件最开始的位置添加这么一个注释 -*- lexical-binding: t -*- 来开启这一特性。上面的代码只要加上这一特性就能得到不一样的结果
;; -*- lexical-binding: t -*-
(let ((x "hello, x"))
(message "%s" (getx)))
(defun getx()
x)
这个时候执行将会得到一个错误信息 "Symbol’s value as variable is void: x",x这个符号是一个未定义的变量。
生存期是指程序运行过程中,变量什么时候是有效的。全局变量和 buffer-local 变量都是始终存在的,前者只能当关闭emacs 或者用 unintern 从 obarray 里除去时才能消除。而 buffer-local 的变量也只能关闭缓冲区或者用 kill-local-variable 才会消失。而对于局部变量,elisp 使用的方式称为动态生存期:只有当绑定了这个变量的表达式运行时才是有效的。
在 emacs lisp 简明教程 中举了一个闭包的例子,在elisp中并不支持闭包,它采用的是与普通编程语言一样的变量生存周期,这里我就不列出来了。JavaScript等语言中是支持闭包的,有兴趣的读者可以去看看JavaScript中的闭包。
其他函数
一个符号如果值为空,直接使用可能会产生一个错误。可以用 boundp 来测试一个变量是否有定义。这通常用于 elisp 扩展的移植(用于不同版本或 XEmacs)。对于一个 buffer-local 变量,它的缺省值可能是没有定义的,这时用 default-value 函数可能会出错。这时就先用 default-boundp 先进行测试。
使一个变量的值重新为空,可以用 makunbound。要消除一个 buffer-local 变量用函数 kill-local-variable。可以用 kill-all-local-variables 消除所有的 buffer-local 变量。但是有属性 permanent-local 的不会消除,带有这些标记的变量一般都是和缓冲区模式无关的,比如输入法。
(setq foo "I'm local variable!")
foo ; ==> "I'm local variable!"
(boundp 'foo) ; ==> t
(default-boundp 'foo) ; ==> t
(with-current-buffer "*Messages*"
(boundp 'foo)) ; ==> t
(makunbound 'foo) ; ==> foo
foo ; This will signal an error
(boundp 'foo) ; ==> t
(default-boundp 'foo) ; ==> t
(kill-local-variable 'foo) ; ==> foo
(with-current-buffer "*Messages*"
(boundp 'foo)) ; ==> t
上面的例子需要注意以下几点
- 这里只是使变量的值为空,并没有消除这个变量的符号。所以在执行makunbound 之后,关于foo 是否绑定的测试都是t
- kill-local-variable 只是消除了foo作为buffer-local变量,并没有影响到全局变量,所以在messages-buffer中测试它仍然是有效的变量
变量命名习惯
对于变量的命名,有一些习惯,这样可以从变量名就能看出变量的用途:
- hook 一个在特定情况下调用的函数列表,比如关闭缓冲区时,进入某个模式时。
- function 值为一个函数
- functions 值为一个函数列表
- flag 值为 nil 或 non-nil
- predicate 值是一个作判断的函数,返回 nil 或 non-nil
- program 或 -command 一个程序或 shell 命令名
- form 一个表达式
- forms 一个表达式列表。
- map 一个按键映射(keymap)

浙公网安备 33010602011771号