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)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」