Common Lisp 支持两种类型的变量:词法变量(lexical)和动态变量(dynamic)
一 变量的基础知识
和常见的编程语言一样,Common Lisp中的变量是一些可以保存值的具名位置,但在Common Lisp 中,变量并不像在C++或Java那样带有确定的类型,一个变量可以保存任何类型的值,并且这些变量带有可用于运行期类型检查的类型信息。因此,Common Lisp是动态类型的,如果将某个并非数字的对象传给了+函数,那么Common Lisp将会报类型错误。
某些特定类型的,诸如整数与字符,它们会在内存中直接表示,除此之外,Common Lisp中所有值都是对象的引用,如果一个变量保存了对一个可变对象的引用,那么就可以用该引用来修改此对象,而这种改动将应用于任何带有相同对象引用的代码。
一种常用的引入新变量的方式是定义函数形参,用DEFUN来定义函数时,形参列表定义了当函数被调用时用来保存实参的变量。例如,下列函数定义了三个变量 x, y 和 z,用来保存其实参。(defun foo (x y z) (+ x y z))
, 每当函数被调用时,Lisp就会创建新的绑定来保存由函数调用者所传递的实参
引入变量的另一种方式是使用LET特殊操作符:
1 | (let (variable*) |
其中每一个variable都是一个变量初始化形式,每一个初始化形式要么是一个含有变量名和初值形式的列表,要么就是一个简单的变量名,这样该变量默认为NIL。下面的LET形式将三个变量x, y和z绑定到初始值10,20和NIL上。
1 | (let ((x 10) (y 20) z) |
当这个LET形式被求值时,所有的初始值形式都将首先被求值,然后创建出新的绑定并在形式体被执行之前,这些绑定将初始化到适当的初始值上。在LET形式体中,变量名将引用新创建的绑定。在LET形式体执行结束后,这些变量名将重新引用在执行LET之前所引用的内容,如果有的话。形式体中最后一个表达式的值作为LET表达式的值返回。
如果嵌套了引入同名变量的绑定形式,那么最内层的变量绑定将覆盖外层的绑定。
另一个引入变量的方式是LET的变体: LET*。两者的区别是,在一个LET中,被绑定的变量名只能在LET的形式体之内(LET形式中变量列表之后的那部分),但在LET*中,每个变量的初始值形式,都可以引用到那些在变量列表中早先引入的变量,例如:
1 | (let* ((x 10) |
二 词法变量和闭包
默认情况下,Common Lisp 中所有的绑定形式都将引入词法作用域变量,词法作用域的变量只能由那些在文本上位于绑定形式之内的代码所引用(也就是下列…处的代码),类似于Java,C,C++中的局部变。
1 | (let ((x 10) (y 20) z) |
但是当一个匿名函数含有一个对来自封闭作用域之内的词法变量的引用时,将会发生什么呢?
1 | (let ((count 0)) #'(lambda () (setf count (1+ count)))) |
这个含有引用的匿名函数将被作为LET形式的值返回,并可能通过FUNCALL被不在LET作用域之内的代码所调用。当控制流进入LET形式时所创建的count绑定将被尽可能地保留下来,只要某处保持了一个对LET形式所返回的函数对象的引用即可。这个匿名函数被称为一个闭包,因为它“封闭包装”了由LET创建的绑定。
理解闭包的关键在于,被捕捉的是绑定而不是变量的值。因此,一个闭包不仅可以访问它所闭合的变量的值,还可以对其赋予在闭包被调用时不断变化的新值。例如,可以像下面这样将前面的表达式所创建的闭包捕捉到一个全局变量里:
1 | (defparameter *fn* (let ((count 0)) #'(lambda () (setf count (1+ count))))) |
然后每当调用它时,count的值将被加1:
1 | CL-USER> (funcall *fn*) |
单一闭包可以简单的引用变量来闭合许多变量绑定,或是多个闭合可以捕捉相同的绑定。例如,下面的表达式返回由三个闭合所组成的列表,一个可以递增其所闭合的count绑定的值,另一个可以递减它,还有一个返回他的值。
1 | (let ((count 0)) |
三 动态变量
动态变量也叫全局变量,一种可以从程序的任何位置访问到的变量。Common Lisp 提供了两种创建全局变量的方式:DEFVAR和DEFPARAMETER,两种方式都接受一个变量名,一个初始值以及一个可选的文档字符串。全局变量习惯上被命名为以*开始和结尾的名字:
1 | (defvar *count* 0 |
两种方式的区别是DEFPARAMETER总是将初始值赋给命名的变量,而DEFVAR只有当变量未定义时才这样做。DEFVAR也可以不带初始值来使用,这样的变量称为未绑定的。从实践上来讲,应该使用DEFVAR来定义某些变量。
有时我们需要临时改变全局变量的值,并在代码块结束时自动使用之前的值,这时我们可以使用LET形式。
1 | (let ((*standard-output* *some-other-stream*)) |
在任何由于调用stuff而运行的代码中, 对*standard-output*
的引用将使用由LET所创建的绑定,并且当stuff返回并且程序控制离开LET时,这个对*standard-output*
的新绑定将消失。
四 常量
除了词法变量和动态变量外,还有一种类型的变量是常值变量,所有常量都是全局的,并且使用DEFCONSTANT定义,DEFCONSTANT的基本形式与DEFPARAMETER相似:
1 | (defconstant name initial-value-form [documentation-string]) |
被DEFCONSTANT定义的常量不能被用作函数形参或是任何其他的绑定形式进行重新绑定,因此许多程序员遵循了一个命名约定,用以+开始和结尾的名字来表示常量。
五 赋值
一旦创建了绑定,就可以对它做两件事:获取当前值以及为它设置新值,一个符号被求值为它所命名的变量的值,而为绑定赋予新值可以使用SETF宏,这是Common Lisp的通用赋值操作符。
1 | (setf place value) |
因为SETF是宏,所以它可以检查它所赋值的place上的形式,并展开成适当的底层操作来修改那个位置。当该位置是变量时,它展开成一个对特殊操作符SETQ的调用,后者可以访问到词法和动态绑定。
1 | (setf x 10) |
SETF也可用于依次对多个位置赋值,例如:
1 | (setf x 1) |
可以写成:
1 | (setf x 1 y 2) |
SETF返回最近被赋予的值:
1 | (setf x (setf y (random 10))) |
这样 x 和 y都被赋予同一个随机值。
六 广义赋值
SETF可以为任何位置赋值,例如数组,哈希表,列表以及由用户定义的数据结构,所有这些数据结构都含有多个可用来保存值的位置。
对于Python来说:
赋值对象 | python | Lisp |
---|---|---|
变量 | x = 10 | (setf x 10) |
数组元素 | a[0]=10 | (setf (aref a 0) 10) |
哈希表项 | hash[‘key’]=10 | (setf (gethash ‘key hash) 10) |
对象字段 | o.fiele=10 | (setf (field o) 10) |
其中AREF是数组访问函数,GETHASH是哈希表查找,而field可能是一个访问某用户定义对象中名为field的成员的函数。 |
七 其他修改位置的方式
尽管所有的赋值都可以用SETF来表达,但也存在一些固定的模式:
1 | (incf x) |
以上三行代码与以下三行代码等价:
1 | (setf x (+ x 1)) |
类似INCF和DECF这种宏称为修改宏,修改宏是建立在SETF之上的宏。修改宏所定义的方式使其可以安全地用于那些表达式必须只被求职一次的位置。
有两个稍微有些难懂但很有用的修改宏是ROTATEF和SHIFTF。ROTATEF在位置之间轮换它们的值:
1 | (rotatef a b) |
上述代码将交换两个变量的值并返回NIL。它等价于以下形式:
1 | (let ((tep a)) (setf a b b tep) nil) |
将上述代码格式化得到以下代码:
1 | (let ((tmp a)) ; Step 1 |
ROTATEF和SHIFTF都可以被用于任意多个参数,并且和所有的修改宏一样,它们可以保证以从左到右的顺序对每个参数只求值一次。