关于 Emacs 变量,你需要了解什么

了解 Elisp 如何处理变量,以及如何在你的脚本和配置中使用它们。
108 位读者喜欢这篇文章。
Programming keyboard.

Opensource.com

GNU Emacs 是用 C 语言和 Emacs Lisp (Elisp) 编写的,Elisp 是 Lisp 编程语言的一种方言。因为它是一个恰好是 Elisp 沙箱的文本编辑器,所以了解基本的编程概念在 Elisp 中是如何工作的会很有帮助。

如果你是 Emacs 的新手,请访问 Sacha Chua 优秀的 Emacs 初学者资源列表。本文假设你熟悉常见的 Emacs 术语,并且知道如何阅读和评估基本的 Elisp 代码片段。理想情况下,你应该也听说过变量作用域以及它在另一种编程语言中是如何工作的。这些示例还假设你使用相当新的 Emacs 版本(v.25 或更高版本)。

Elisp 手册 包含了你需要知道的一切,但它是为已经知道他们在寻找什么的人编写的(在这方面它真的非常棒)。但是很多人想要更高级别地解释 Elisp 概念的资源,并减少信息量到最有用的部分。本文是我对这一需求的尝试——让读者对基础知识有一个很好的掌握,以便他们可以将它们用于自己的配置,并使人们更容易在手册中查找一些细节。

全局变量

defcustom 定义的用户选项和用 defvardefconst 定义的变量是全局的。用 defcustomdefvar 声明的变量的一个重要方面是,重新评估它们不会重置已经绑定的变量。例如,如果你在你的 init 文件中为 my-var 建立了一个绑定,像这样

(setq my-var nil)

评估以下形式不会将变量重置为 t

(defvar my-var t)

请注意,有一个例外:如果你用 C-M-x 评估上面的声明,它会调用 eval-defun,那么该值将被重置为 t。这样,如果需要,你可以强制设置该值。这种行为是故意的:你可能知道,Emacs 中的许多功能只在需要时加载(即,它们是自动加载的)。如果这些文件中的声明将变量重置为其默认值,这将覆盖你 init 文件中的任何设置。

用户选项

用户选项只是一个用 defcustom 声明的全局变量。与用 defvar 声明的变量不同,这样的变量可以通过 M-x customize 界面进行配置。据我所知,大多数人不太使用它,因为它感觉很笨拙。一旦你知道如何在你的 init 文件中设置变量,就没有 compelling 的理由去使用它。许多用户没有意识到的一个细节是,使用 customize 设置用户选项可能会执行代码,这有时用于运行额外的设置指令

(defcustom my-option t
  "My user option."
  :set (lambda (sym val)
         (set-default sym val)
         (message "Set %s to %s" sym val)))

如果你评估这段代码并使用 customize 界面通过 M-x customize-option RET my-option RET 更改值,lambda 将被调用,并且回显区域中的消息将告诉你选项的符号和值。

如果你在你的 init 文件中使用 setq 来更改此类选项的值,setter 函数将不会运行。要使用 Elisp 正确设置此类选项,你需要使用函数 customize-set-variable。或者,人们在他们的配置中使用各种版本的 csetq 宏来自动处理这个问题(如果你喜欢,可以使用 GitHub 代码搜索来发现更复杂的变体)

(defmacro csetq (sym val)
  `(funcall (or (get ',sym 'custom-set) 'set-default) ',sym ,val))

如果你正在使用 use-package 宏,:custom 关键字将为你处理这个问题。

将上面的代码放入你的 init 文件后,你可以使用 csetq 以尊重任何现有 setter 函数的方式设置变量。你可以通过在使用此宏更改上面定义的选项时观察回显区域中的消息来证明这一点

(csetq my-option nil)

动态绑定和词法绑定

如果你使用其他编程语言,你可能没有意识到动态绑定和词法绑定之间的区别。如今,大多数编程语言都使用词法绑定,当学习变量作用域/查找时,没有必要知道这种区别。

Emacs Lisp 在这方面很特殊,因为动态绑定是默认的,而词法绑定必须显式启用。这有历史原因,实际上,你应该始终启用词法绑定,因为它更快且更不容易出错。要启用它,只需将以下注释行作为 Emacs Lisp 文件的第一行

;;; -*- lexical-binding: t; -*-

或者,你可以调用 M-x add-file-local-variable-prop-line,当你选择变量 lexical-binding 且值为 t 时,它将插入上面的注释行。

当加载具有这种特殊格式行的文件时,Emacs 会相应地设置变量,这意味着该缓冲区中的代码在启用词法绑定的情况下加载。交互式地,你可以使用 M-x eval-buffer,它会考虑词法绑定设置。

现在你已经知道如何启用词法绑定,明智的做法是了解这些术语的含义。使用动态绑定,程序执行期间建立的最后一个绑定用于变量查找。你可以通过将以下代码放入空缓冲区并执行 M-x eval-buffer 来测试这一点

(defun a-exists-only-in-my-body (a)
  (other-function))

(defun other-function ()
  (message "I see `a', its value is %s" a))

(a-exists-only-in-my-body t)

你可能会惊讶地发现,在 other-function 中查找变量 a 是成功的。

如果你在顶部带有特殊的 lexical-binding 注释的情况下重试前面的示例,代码将抛出“变量为空”错误,因为 other-function 不知道变量 a。如果你来自另一种编程语言,这是你期望的行为。

使用词法绑定,作用域由周围的源代码定义。这不仅是出于性能原因——经验和时间表明,这种行为是首选的。

特殊变量和动态绑定

你可能知道,let 用于临时建立局部绑定

(let ((a "I'm a")
      (b "I'm b"))
  (message "Hello, %s. Hello %s" a b))

这里是重点:用 defcustomdefvardefconst 声明的变量被称为特殊变量,并且无论是否启用词法绑定,它们都继续使用动态绑定

;;; -*- lexical-binding: t; -*-

(defun some-other-function ()
  (message "I see `c', its value is: %s" c))

(defvar c t)

(let ((a "I'm lexically bound")
      (c "I'm special and therefore dynamically bound"))
  (some-other-function)
  (message "I see `a', its values is: %s" a))

要在上面的示例中看到两条消息,请使用 C-h e 切换到 *Messages* 缓冲区。

let 或函数参数绑定的局部变量遵循由 lexical-binding 变量定义的查找规则,但是用 defvardefconstdefcustom 定义的全局变量可以在调用堆栈的深处被更改,其持续时间为 let 主体。

这种行为允许方便的临时自定义,并且在 Emacs 中经常使用,考虑到 Emacs Lisp 最初只有动态绑定作为唯一的选择,这并不令人惊讶。这是一个常见的示例,展示了如何临时写入只读缓冲区

(let ((inhibit-read-only t))
  (insert ...))

这是另一个经常看到的示例,用于执行区分大小写的搜索

(let ((case-fold-search nil))
  (some-function-which-uses-search ...))

动态绑定允许你以函数作者可能从未预料到的方式更改函数的行为。对于像 Emacs 这样设计和使用的程序来说,它是一个强大的工具和一个很棒的功能。

有一个需要注意的警告:你可能会意外地使用在其他地方声明为特殊变量的局部变量名。防止此类冲突的一个技巧是避免在局部变量的名称中使用破折号。在我当前的 Emacs 会话中,这只剩下少数潜在的冲突候选项

(let ((vars ()))
  (mapatoms
   (lambda (cand)
     (when (and (boundp cand)
                (not (keywordp cand))
                (special-variable-p cand)
                (not (string-match "-"
                                   (symbol-name cand))))
       (push cand vars))))
  vars) ;; => (t obarray noninteractive debugger nil)

缓冲区局部变量

每个缓冲区都可以具有变量的局部绑定。这意味着当此缓冲区为当前缓冲区时进行的任何变量查找都将显示该缓冲区的变量局部值,而不是默认值。局部变量是 Emacs 中的一个重要功能;例如,它们被主模式用来建立其缓冲区局部的行为和设置。

你已经在本文中看到了一个缓冲区局部变量:用于 lexical-binding 的特殊注释行,它将缓冲区局部绑定到 t。在 Emacs 中,在特殊注释行中定义的此类缓冲区局部变量也称为文件局部变量

任何全局变量都可以被缓冲区局部变量阴影化。例如,以上定义的 my-var 变量,你可以像这样在本地设置它

(setq-local my-var t)
;; or (set (make-local-variable 'my-var) t)

my-var 是缓冲区的局部变量,当你评估上面的代码时,它是当前缓冲区。如果你在它上面调用 describe-variable,文档会告诉你局部值和全局值。在编程方面,你可以使用 buffer-local-value 检查局部值,并使用 default-value 检查默认值。要删除本地版本,你可以调用 M-x kill-local-variable

另一个需要注意的重要属性是,一旦变量是缓冲区局部的,任何进一步使用 setq(当此缓冲区为当前缓冲区时)都将继续设置局部值。要设置默认值,你需要使用 setq-default

因为局部变量旨在用于缓冲区自定义,所以它们最常在模式钩子中使用。一个典型的例子是这样的

(add-hook 'go-mode-hook
          (defun go-setup+ ()
            (setq-local compile-command
              (if (string-suffix-p "_test.go" buffer-file-name)
                  "go test -v"
                (format "go run %s"
                        (shell-quote-argument
                         (file-name-nondirectory buffer-file-name)))))))

这为 go-mode 缓冲区设置了 M-x compile 使用的编译命令。

另一个重要的方面是一些变量是自动缓冲区局部的。这意味着一旦你 setq 这样的变量,它就会为当前缓冲区设置一个局部绑定。这个功能不应该经常使用(因为这种隐式行为不太好),但是如果你想,你可以像这样创建这样的自动局部变量

(defvar-local my-automatical-local-var t)
;; or (make-variable-buffer-local 'my-automatical-local-var)

变量 indent-tabs-mode 是一个内置的例子。如果你在你的 init 文件中使用 setq 来更改此变量的值,它根本不会影响默认值。只有在加载你的 init 文件时当前缓冲区的的值会被更改。因此,你需要使用 setq-default 来更改 indent-tabs-mode 的默认值。

结语

Emacs 是一个强大的编辑器,你越是改变它以适应你的需求,它就变得越强大。现在你已经知道 Elisp 如何处理变量,以及如何在你自己的脚本和配置中使用它们。


本文先前出现在 With-Emacs,根据 CC BY-NC-SA 4.0 许可协议发布,并已获得作者许可进行改编(通过合并请求)和重新发布。

接下来阅读

谁在乎 Emacs?

GNU Emacs 已经存在很长时间了——自 1983 年以来——但其持续的开发使其在今天仍然具有现实意义。

(团队,红帽)
2020 年 2 月 24 日
User profile image.
Emacs 博客作者和包维护者。

评论已关闭。

© . All rights reserved.