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 scopeindefinite 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 或者用 uninternobarray 里除去时才能消除。而 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)
posted @   masimaro  阅读(4)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
点击右上角即可分享
微信分享提示