關於 Emacs 中的變數你需要知道的事情
GNU Emacs 是由 C 和 Emacs Lisp(Elisp,Lisp 編程語言的一種方言)寫成,它是一個編輯器的同時,又碰巧是 Elisp 的沙盒。因此,理解 Elisp 中的一些基本編程概念會對你有一些幫助。
如果你是 Emacs 新手,請先閱讀 Sacha Chua 的《給 Emacs 新手的資源》精品帖。本篇文章假定你熟悉常見的 Emacs 術語,並且能夠閱讀並求值 Elisp 代碼的簡單片段。最好你也聽說過變數作用域的概念,知道它在其它編程語言中的作用。本篇文章中的示例假定你使用的是相對較新的 Emacs 版本(v.25 之後的版本)。
Elisp 手冊 包含了 Elisp 的方方面面,但它是寫給那些有明確查找目標的人們的(它在這方面也做得相當棒)。但是很多人想要能夠在更高的層次上解釋 Elisp 概念的材料,同時將信息壓縮成最精華的部分。本篇文章也正是我回應這種呼聲的一次嘗試,為讀者描繪基礎的大體輪廓。使他們能在配置中用上這些技巧,也讓他們在手冊中查詢細節變得更容易。
全局變數
用 defcustom
定義的用戶設置和用 defvar
或 defconst
定義的變數是全局的。使用 defcustom
或 defvar
聲明變數的一個非常重要的原因是,當一個變數已經被 綁定 ,對它們進行重新求值不會覆蓋掉已有的值。舉個栗子,如果你在初始化文件中對 my-var
進行如下綁定:
(setq my-var nil)
對如下表達式求值不會將變數覆蓋為 t
:
(defvar my-var t)
注意此處有一個例外:如果你用 C-M-x
快捷鍵對上述聲明求值,它將調用 eval-defun
函數,並將變數覆蓋為 t
。通過此方式,你可以按需將變數強制覆蓋。這種行為是刻意而為之的:你可能知道,Emacs 中的許多特性是按需載入的,也可以稱為自動載入。如果那些文件中的聲明將變數覆蓋為它們的默認值,那它也就覆蓋了你初始化文件中的設置。
用戶選項
用戶選項就是使用 defcustom
聲明的全局變數。與使用 defvar
聲明的變數不同,這些變數可以用 M-x customize
界面來配置。據我所知,大部分人因為覺得它開銷較大而不經常使用。一旦你知道如何在你的初始化文件中設置變數,也就沒有理由一定要去使用它了。許多用戶沒有意識到的一個細節是,通過 customize
的方式設置用戶選項能夠執行代碼,有的時間可用來運行一些附加的配置說明:
(defcustom my-option t
"My user option."
:set (lambda (sym val)
(set-default sym val)
(message "Set %s to %s" sym val)))
若你對這段代碼求值,並鍵入 M-x customize-option RET my-option RET
運行 customize
界面,lambda 匿名函數就會被調用,回顯區域就會顯示出該選項的符號名與值。
如果你在初始化文件中使用 setq
改變該選項的值,那麼匿名函數不會運行。要想在 Elisp 中正確設置一個選項,你需要使用函數 customize-set-variable
。或者,人們在他們的配置文件中使用了各種版本的 csetq
宏來自動處理(如你所願,你可以通過 GitHub 的代碼搜索發現更複雜的變體)。
(defmacro csetq (sym val)
`(funcall (or (get ',sym 'custom-set) 'set-default) ',sym ,val))
若你正在使用 use-package 宏,:custom
關鍵字會替你處理好以上這些。
在你將以上代碼放入到你的初始化文件中之後,你便可以使用 csetq
宏在設置變數的同時運行任何現存的 setter
函數。要證明這點,你可以使用此宏來改變上面定義的選項,並觀察回顯區域的消息輸出。
(csetq my-option nil)
動態綁定與詞法綁定
當你在使用其它編程語言時,你可能不會意識到動態綁定與詞法綁定的區別。當今的大部分編程語言使用詞法綁定,並且在學習變數作用域與變數查找時也沒有必要去了解它們之間的區別。
如此看來,Emacs Lisp 比較特殊因為動態綁定是默認選項,詞法綁定需要顯式啟用。這裡有一些歷史遺留原因,但在實際使用中,你應該時刻啟用詞法綁定,因為它更快並且不容易出錯。要啟用詞法綁定,只需將如下的注釋行作為你的 Emacs Lisp 文件的第一行:
;;; -*- lexical-binding: t; -*-
另一種方式,你可以調用 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
竟然成功了。
若你在頂部添加了特殊的詞法綁定注釋後,重新運行前面的示例,這段代碼將拋出 variable is void
錯誤,因為 other-functioin
無法識別變數 a
。如果你使用的是其它編程語言,這才是你所期望的行為。
啟用詞法綁定後,作用域會由周圍的代碼所定義。這並不單單是性能原因,時間也已經表明了詞法綁定才是更受喜愛的。
特殊變數與動態綁定
如你所知,let
用於臨時建立局部綁定:
(let ((a "I'm a")
(b "I'm b"))
(message "Hello, %s. Hello %s" a b))
接下來有趣的是——使用 defcustom
、defvar
以及 defconst
定義的變數被稱為特殊變數,不論詞法綁定是否啟用,它們都將使用動態綁定:
;;; -*- 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
變數定義的查找規則,但使用 defvar
、defconst
或 defcustom
定義的全局變數,能夠沿著調用棧在 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
獲取全局值。若要移除局部值,你可以調用 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
就是 Emacs 內建的一個例子。如果你在初始化文件中使用 setq
改變變數的值,根本不會影響默認值。只有在你載入初始化文件時正處在當前的緩衝區的局部值會被改變。因此,你需要使用 setq-default
來改變 indent-tabs-mode
的默認值。
結語
Emacs 是一個強大的編輯器,並且隨著你的定製它將變得更加強大。現在,你知道了 Elisp 是如何處理變數的,以及你應如何在你自己的腳本與配置中使用它們。
本篇文章此前採用 CC BY-NC-SA 4.0 許可證發布在 With-Emacs 上,經過修改(帶有合併請求)並在作者允許的情況下重新發布。
via: https://opensource.com/article/20/3/variables-emacs
作者:Clemens Radermacher 選題:lujun9972 譯者:cycoe 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive