全局变量的值会一直保留,直到被新值显式替换为止。有时,给变量赋予一个 局部值(local value) 会非常有用 — 这种值只在 Lisp 程序的特定范围内生效。 当变量拥有局部值时,我们称该变量被 局部绑定(locally bound) 到这个值上,并称它为 局部变量(local variable)。
例如,当一个函数被调用时,其参数变量会接收局部值,这些值就是调用该函数时传入的实际参数;这些局部绑定在函数体内部生效。再比如,let 特殊形式会为指定的变量显式建立局部绑定,这些绑定仅在 let 形式的函数体内部有效。
我们也将(概念上)保存全局值的地方称为 全局绑定(global binding)。
建立一个局部绑定会保存变量之前的值(或无值状态)。我们说,之前的值 被遮蔽(shadowed) 了。
全局值和局部值都可能被遮蔽。
如果一个局部绑定正处于生效状态,对该局部变量使用 setq 会将指定的值存储到这个局部绑定中。
当该局部绑定不再生效时,之前被遮蔽的值(或无值状态)会恢复。
一个变量同一时刻可以拥有多个局部绑定(例如在嵌套的 let 表达式中对同一个变量进行绑定)。 当前绑定(current binding) 指的是实际生效的那个局部绑定,它决定了对该变量符号求值时返回的值,也是 setq 所操作的绑定。
在大多数情况下,你可以把当前绑定理解为:最内层的局部绑定;如果不存在局部绑定,则是全局绑定。更精确地说,有一条名为 作用域规则(scoping rule) 的规则,决定了局部绑定在程序中的生效位置。 Emacs Lisp 默认的作用域规则叫做 动态作用域(dynamic scoping),它的含义很简单:程序执行到任意位置时,某个变量的当前绑定,就是最近创建且仍然存在的那个绑定。 关于动态作用域的细节,以及另一种作用域规则 —— 词法作用域(lexical scoping),see Scoping 变量绑定的作用域规则。 最近,Emacs 正越来越多地使用词法绑定,目标是最终将其设为默认规则。特别地,所有 Emacs Lisp 源文件以及 *scratch* 缓冲区都已使用词法作用域。
用于创建局部绑定的特殊形式是 let 和 let*:
这个特殊形式会根据 bindings 为一组变量建立局部绑定,然后按文本顺序执行所有的 forms。它的返回值是 forms 中最后一个表达式的值。由 let 创建的局部绑定只在 forms 主体内部生效。
每个 bindings 元素有两种形式:(i) a 一个符号,此时该符号会被局部绑定到 nil;(ii) a 形如 (symbol value-form) 的列表,此时 symbol 会被局部绑定到 value-form 的求值结果。如果省略 value-form,则使用 nil。
bindings 里的所有 value-form,都会在对任何符号进行绑定 之前 按顺序求值。举个例子:z 会被绑定到 y 的旧值 2,而不是 y 的新值 1。
(setq y 2)
⇒ 2
(let ((y 1)
(z y))
(list y z))
⇒ (1 2)
另一方面, 绑定 的执行顺序并未明确规定:在下面的示例中,最终输出的结果可能是 1,也可能是 2。
(let ((x 1)
(x 2))
(print x))
因此,避免在同一个 let 形式中对同一个变量进行多次绑定。
该特殊形式与 let 类似,但它会在计算完一个变量的局部值后立即绑定该变量,再去计算下一个变量的局部值。因此,bindings 中的表达式可以引用在这个 let* 形式中前面已绑定的符号。
请将下面这个示例与前文 let 的示例对比查看。
(setq y 2)
⇒ 2
(let* ((y 1)
(z y)) ; Use the just-established value of y.
(list y z))
⇒ (1 1)
简单来说,上一个例子中 let* 对 x 和 y 的绑定,等价于使用嵌套的 let 绑定:
(let ((y 1))
(let ((z y))
(list y z)))
这个特殊形式与 let* 类似,但它会先绑定所有变量,再计算任何局部值。然后再将计算好的值赋给这些已被局部绑定的变量。这一形式只在词法绑定生效时有用,适用于你需要创建闭包,且该闭包需要引用那些在使用 let* 时尚未生效的绑定的场景。
例如,下面是一个运行一次后就将自身从钩子中移除的闭包:
(letrec ((hookfun (lambda ()
(message "Run once")
(remove-hook 'post-command-hook hookfun))))
(add-hook 'post-command-hook hookfun))
该特殊形式与 let 类似,但它会对所有变量进行动态绑定。这一用法很少见 —— 通常你希望对普通变量使用词法绑定,对特殊变量(即用 defvar 定义的变量)使用动态绑定,而这正是 let 的默认行为。
dlet 适用于与旧代码交互的场景:这些代码假定某些变量是动态绑定的(see 动态绑定),但又不方便用 defvar 去定义这些变量。dlet 会临时将被绑定的变量设为特殊变量,执行表达式,然后再把这些变量恢复为非特殊变量。
该特殊形式是受 Scheme 语言启发设计的循环结构。它与 let 类似:会为 bindings 中的变量建立绑定,然后对 body 进行求值。但除此之外,named-let 还会将 name 绑定到一个局部函数上 —— 这个函数的形参是 bindings 中的变量,函数体则是 body。这使得 body 可以通过调用 name 实现递归调用自身,调用 name 时传入的参数会作为递归调用中被绑定变量的新值。
以下是一个对数字列表求和的循环示例:
(named-let sum ((numbers '(1 2 3 4))
(running-sum 0))
(if numbers
(sum (cdr numbers) (+ running-sum (car numbers)))
running-sum))
⇒ 10
在 body 中 尾部位置(tail positions) 发生的对 name 的递归调用,会被优化为 尾部调用(tail calls)。这意味着,无论递归深度如何,它们都不会消耗额外的栈空间。此类递归调用实际上会带着变量的新值,跳转到循环的起始处重新执行。
一个函数调用处于尾部位置,是指它是整个执行过程中最后一步操作,且其返回值就是 body 的返回值。上文对 sum 的递归调用就是如此。
named-let 仅在启用词法绑定时可用。See 词法绑定。
下面是创建局部绑定的其他所有功能的完整列表:
变量还可以拥有缓冲区局部绑定(see 缓冲区局部变量);少数变量拥有终端局部绑定(see Multiple Terminals)。 这类绑定在行为上与普通局部绑定有些相似,但它们的局部化效果取决于你在 Emacs 中的当前位置。