LISP - 闭包
Lisp - 闭包与词法作用域
Section titled “Lisp - 闭包与词法作用域”闭包是 Lisp 及其他函数式语言中最强大的特性之一。它是一个函数,能够捕获并记住其词法环境——即函数创建时作用域内的变量和绑定。这使得函数能够维护状态并在可能已不复存在的上下文中运行。
- 词法作用域:Lisp 使用词法(或静态)作用域。这意味着变量的作用域由其在源代码中的物理位置决定。一个函数可以访问其包含的代码块中的变量。这就是闭包得以实现的基础。
- 头等函数:在 Lisp 中,函数是头等公民。它们可以存储在变量中,作为参数传递给其他函数,并从函数中返回结果。闭包是结合了这一特性和词法作用域的直接产物。
- 环境捕获:当创建
lambda(一个匿名函数)时,它会捕获其引用的词法变量的快照。这个被捕获的环境与函数捆绑在一起,形成了闭包。
经典示例:函数工厂
Section titled “经典示例:函数工厂”演示闭包最常见的方式是使用函数工厂:一个创建并返回其他函数的函数。在这里,我们将创建一个 adder 工厂。
main.lisp
Section titled “main.lisp”(defun make-adder (n) "一个函数工厂。返回一个新函数(一个闭包),该函数将其参数加上 N。" (lambda (x) (+ x n))) ; 这个 `lambda` 捕获了 `n` 的值
;; --- 创建并使用一些加法器函数 ---
;; 创建两个独立的闭包,每个都有其捕获的环境。(let ((add-5 (make-adder 5)) ; 这个闭包记住 n=5 (add-10 (make-adder 10))) ; 这个闭包记住 n=10
;; `funcall` 用于调用存储在变量中的函数。 (format t "Calling add-5 with 3: ~a~%" (funcall add-5 3)) (format t "Calling add-10 with 3: ~a~%" (funcall add-10 3))
;; `let` 绑定中的原始 `n` 已经消失,但闭包仍然有效。 (format t "Calling add-5 again with 100: ~a~%" (funcall add-5 100)))执行代码后,会返回以下结果:
Calling add-5 with 3: 8Calling add-10 with 3: 13Calling add-5 again with 100: 105make-adder函数接受一个参数n,并立即返回一个lambda函数。- 返回的
lambda之所以成为闭包,是因为它引用了n,而n是make-adder的词法环境的一部分。 - 当我们调用
(make-adder 5)时,会得到一个“硬编码”为加5的函数,因为它已经捕获了n的那个特定值。 - 每次调用
make-adder都会创建一个新的、独立的闭包,拥有其自己的捕获环境。
实际用例:封装状态
Section titled “实际用例:封装状态”闭包非常适合创建简单的有状态对象,而无需完整的 defclass。下面是一个创建计数器的函数。
main.lisp
Section titled “main.lisp”(defun make-counter () "创建一个计数器函数,每次调用时都会增加其内部计数。" (let ((count 0)) (lambda () (incf count)))) ; `incf` 递增被捕获的 `count` 变量
(let ((counter-a (make-counter)) (counter-b (make-counter)))
(format t "Counter A: ~a~%" (funcall counter-a)) (format t "Counter A: ~a~%" (funcall counter-a))
(format t "Counter B: ~a~%" (funcall counter-b)) (format t "Counter A: ~a~%" (funcall counter-a)))Counter A: 1Counter A: 2Counter B: 1Counter A: 3如你所见,counter-a 和 counter-b 是独立的,各自拥有私有的 count 变量,这得益于闭包的强大功能。