实用zhlisp编程06:变量
Category:中文学习第6章 变量
我们需要了解的下一个基本程序构造单元是变量。Common Lisp 支持两种类型的变量:词法(lexical)变量和动态(dynamic)变量。这两种变 量类型分别对应于其他语言中的局部变量和全局变量,不过也只能说是大致相似。一方面,某些语言中的局部变量更像是 Common Lisp 的动态变量 。另一方面,某些语言中的局部变量虽然是词法作用域的,但却并没有提供由 Common Lisp 的词法变量所提供的所有功能。尤其是并非所有语言都提供了支持闭包的词法作用域变量。
许多含有变量的表达式都可以同时使用词法和动态变量,这样一来更令人困惑了。因此本章我先讨论同时涉及到两种类型的 Lisp 变量的几个方面,然后再谈及词法变量和动态变量各自的特征。随后再讨论 Common Lisp 的通用赋值操作符 赋值,它用于为变量和其他任何可以保存值的地方赋予新值。
6.1 变量的基础知识
和其他语言一样,Common Lisp 中的变量是一些可以保存值的具名位置。但在 Common Lisp 中,变量并非像在 Java 和 C++ 等语言中那样带有确定的类型,也就是说不需要为每一个变量声明其可以保存对象的类型。相反,一个变量可以保存任何类型的值,并且这些值带有可用于运行期类型检查的类型信息。因此,Common Lisp 是动态类型的——类型错误会被动态地检测到。举个例子,假如这些值某个并非数字的对象传给了 +
函数,那么 Common Lisp 将会报类型错误。而另一方面,Common Lisp 是一种强类型语言,因为所有的类型错误都将被检测到——无法将一个对象作为其不属于的类型的实例来对待。
至少从概念上来说,Common Lisp 中所有的值都是对象的引用。 因此,将一个变量赋予新值就会改变该变量所指向的对象,而对之前被引用的对象却没有影响。尽管如此,如果一个变量保存了对一个可变对象的引用,那么就可以用该引用来修改此对象,而这种改动将应用于任何带有相同对象引用的代码。
而另一种已经用到的引入新变量的方式是定义函数形参。正如前一章所示,在用 函数 来定义函数时,形参列表定义了当函数被调用时用来保存实参的变量,例如,下列函数定义了三个变量 x
、y
和 z
,用来保存其实参:
(函数 临时 (x y z) (+ x y z))
每当函数被调用时,Lisp 就会创建新的绑定来保存由函数调用者所传递的实参。绑定代表了变量在运行期的存在。单个变量即可以在程序源代码中所指出的那种东西。在程序运行过程中可以有多个不同的绑定,单个变量甚至可以同时带有多重绑定,例如,一个递归函数的形参会在每一次函数调用中被重新绑定。
和所有 Common Lisp 变量一样,函数形参也可以保存对象引用。 因此,可以在函数体内为一个函数形参赋予新值,而这却并不会影响到同样函数的 另一个调用所创建的绑定。但如果改变了传递给函数的可变对象,则这些改 动将会被调用者看到,因为无论调用者还是被调用者都在引用同一个对象。
引入新变量的另一种方式是使用 变量 特别操作符。下面就是一个 变量 形式的结构:
(变量 (临时变量iable*) body-form*)
其中每一个 variable
都是一个变量的初始化形式。每一个初始化形式要么是一个含有变量名和初值形式的列表,要么就是一个简单的变量名——作为将变量初始化到 NIL 的简略写法。例如,下面的 变量 形式会将三个变量 x
、y
和 z
绑定到初始值 10、20 和 NIL 上:
(变量 ((x 10) (y 20) z) ...)
当这个 变量 形式被求值时,所有的初始值形式都将首先被求值,然后创建出新的绑定,并在形式体被执行之前这些绑定将初始化到适当的初始值上。在 变量 形式体中,变量名将引用新创建的绑定。在 变量 形式体执行结束后,这些变量名将重新引用在执行 变量 之前它们所引用的内容,如果有的话。
形式体中最后一个表达式的值将作为 变量 表达式的值返回。和函数形参一样,由 变量 所引入的变量将在每次进入 变量 时被重新绑定。
函数形参和 变量 变量的作用域(变量名可被用来引用该绑定的程序区域)被限定在引入该变量的形式之内,该形式即函数定义或 变量,被称为绑定形式。你很快将看到,词法变量和动态变量使用两种略有不同的作用域机制,但两者的其作用域都被界定在绑定形式之内。
如果嵌套了引入同名变量的绑定形式,那么最内层的变量绑定将覆盖外层的绑定。例如,在调用下面的函数时,将创建一个形参 x
的绑定来保存函数的参数。第一个 变量 创建了一个带有初始值 2 的新绑定,而内层的 变量 创建了另外一个绑定,其初始值为 3。右边的竖线标记出了每一个绑定的作用域。
(函数 临时 (x) (格式 t "参数: ~a~%" x) ; |<------ x is argument (变量 ((x 2)) ; | (格式 t "在外 变量: ~a~%" x) ; | |<---- x is 2 (变量 ((x 3)) ; | | (格式 t "在内 变量: ~a~%" x)) ; | | |<-- x is 3 (格式 t "在外 变量: ~a~%" x)) ; | | (格式 t "参数: ~a~%" x)) ; |
每一次对 x
的引用都将指向最小封闭作用域中的绑定。一旦程序控制离开了一个绑定形式的作用域,其最近的闭合作用域中的绑定就被解除覆盖,并且 x
将转而指向它。因此,调用 foo
将得到这样的输出:
CL-USER> (临时 1) 参数: 1 在外 变量: 2 在内 变量: 3 在外 变量: 2 参数: 1 NIL
后面的章节将讨论其他可作为绑定形式使用的程序构造,其特点在于所引入的新变量名只能用于该构造。
例如,你将在第 7 章里遇到 计数循环 循环,一种基本的计数循环。它引入了一个变量用来保存每次通过循环时递增的计数器的值。例如下面这个可以打印从 0 到 9 的数字循环,它绑定了变量 x
:
(计数循环 (x 10) (格式 t "~d " x))
另一个绑定形式是 变量 的变种:变量*。两者的区别在于,在一个 变量 中,被绑定的变量名只能被用在 变量 的形式体之内——变量 形式中变量列表之后的那部分;但在一个 变量* 中,每个变量的初始值形式,都可以引用到那些在变量列表中早先引入的变量。因此可以写成下面这样:
(变量* ((x 10) (y (+ x 10))) (列表 x y))
但却不能这样写:
(变量 ((x 10) (y (+ x 10))) (列表 x y))
不过也可以通过嵌套的 变量 来达到相同的效果:
(变量 ((x 10)) (变量 ((y (+ x 10))) (列表 x y)))
6.2 词法变量和闭包
默认情况下,Common Lisp 中所有的绑定形式都将引入词法作用域变量。词法作用域的变量只能由那些在文本上位于绑定形式之内的代码所引用。词法作用域应该被那些曾经使用 Java、C、Perl 或者 Python 来编程的人们所熟悉,因为它们都提供词法作用域的局部变量。如此说来,Algol 程序员们也该对其感到自然才是,因为 Algol 在 20 世纪 60 年代首先引入了词法作用域。
尽管如此,但 Common Lisp 的词法变量还是带有一些变化的,至少和最初的 Algol 模型相比是这样。变化之处在于将词法作用域和嵌套函数一起使用时,按照词法作用域的规则,只有文本上位于绑定形式之内的代码可以指向一个词法变量。但是当一个匿名函数含有一个对来自封闭作用域之内词法变量的引用时,将会发生什么呢?例如,在下面的表达式中:
(变量 ((计数 0)) #'(lambda () (赋值 计数 (1+ 计数))))
根据词法作用域规则,表达式 形式中对 计数 的引用应该是合法的,而这个含有引用的匿名函数将被作为 变量 形式的值返回,并可能会通过 函数调用 被不在 变量 作用域之内的代码所调用。这样会发生什么呢?正如你将看到的那样,当 计数 是一个词法变量时,情况一切正常。本例中,当控制流进入 变量 形式时所创建的 计数 绑定将被尽可能地保留下来,只要某处保持了一个对 变量 形式所返回的函数对象的引用即可。这个匿名函数被称为一个闭包,因为它 “封闭包装” 了由 变量 创建的绑定。
理解闭包的关键在于,被捕捉的是绑定而不是变量的值。因此,一个闭包不仅可以访问它所闭合的变量的值,还可以对其赋予可在闭包被调用时不断变化的新值。例如,可以像下面这样将前面的表达式所创建的闭包捕捉到一个全局变量里:
(全局变量 *fn* (变量 ((计数 0)) #'(lambda () (赋值 计数 (1+ 计数)))))
然后每次当你调用它时,计数 的值将被加 1:
CL-USER> (函数调用 *fn*) 1 CL-USER> (函数调用 *fn*) 2 CL-USER> (函数调用 *fn*) 3
单一闭包可以简单地通过引用变量来闭合许多变量绑定,或是多个闭合可以捕捉相同的绑定。例如,下面的表达式返回由三个闭包所组成的列表,一个可以递增其所闭合的 计数 绑定的值,另一个可以递减它,还有一个返回它的当前值。
(变量 ((计数 0)) (列表 #'(lambda () (递增 计数)) #'(lambda () (递减 计数)) #'(lambda () 计数)))
6.3 动态变量
词法作用域的绑定通过限制作用域(其中给定的名字只具有字面含义)使代码易于理解,这就是大多数现代语言将词法作用域用于局部变量的原因。尽管如此,但有时的确需要全局变量——一种可以从程序的任何位置访问到的变量。尽管随意使用全局变量将使代码变得杂乱无章,就像毫无节制地使用 goto
那样,但全局变量确实有其合理的用途,并以某种形式存在于几乎每种编程语言里。 正如你即将看到的,Lisp 的全局变量和动态变量都更为有用并且更易于管理。
Common Lisp 提供了两种创建全局变量的方法:空值全局变量 和 全局变量。两种形式都接受一个变量名、一个初始值以及一个可选的文档字符串。在被 空值全局变量 和 全局变量 定义以后,该名字可被用于任何位置来指向全局变量的当前绑定。如同在前面章节里所看到的那样,全局变量习惯上被命名为以 *
开始和结尾的名字。你将在本节的后面看到遵守该命名约定的重要性。空值全局变量 和 全局变量 的示例如下:
(空值全局变量 *计数* 0 "到目前为止所做的小部件计数。") (全局变量 *gap-tolerance* 0.001 "小部件间隙允许的容差。")
两种形式的区别在于 全局变量 总是将初始值赋给命名的变量,而 空值全局变量 只有当变量未定义时才这样做。一个 空值全局变量 形式也可以不带初始值来使用,从而在不给定其值的情况下定义一个全局变量。这样一个变量被称为_未绑定的_。
从实践上来讲,应该使用 空值全局变量 来定义某些变量,这些变量所含数据是应持久存在的,即使用到该变量的源码发生改变时也应如此。例如,假设前面定义的两个变量是一个用来控制部件工厂的应用程序的一部分,那么 空值全局变量 来定义 *计数* 变量就比较合适,因为目前已生产的部件数量不会因为对部件生产的代码做了某些改变而就此作废。
另一方面,假如变量 *gap-tolerance*
对于部件生产代码本身的行为具有影响。如果你决定使用一个或紧或松的容差值,并且改变了全局变量 形式中的值,那么就要在重新编译和加载文件时让这一改变产生效果。
在用 空值全局变量 和 全局变量 定义了一个变量之后,就可以从任何一个地方引用它。例如,可以定义下面的函数来递增已生产部件的数量:
(函数 增量余数新对小窗口计数 () (递增 *计数*))
全局变量的优势在于不必到处传递它们。多数语言将标准输入与输出流保存在全局变量里正是出于这个原因——永远不会知道什么时候会向标准输出流打印东西,并且你也不想仅仅由于日后有人需要,就使每个函数都不得不接受并传递含有这些流的参数。
不过,一旦像标准输出流这样的值被保存在一个全局变量中,并且已经编写了引用那个全局变量的代码,那么试图通过更改变量值来临时改变代码行为的做法就颇为诱人了。
例如,假设正工作的一个程序中含有的某些底层日志函数会将输出打印到位于全局变量 *standard-output*
中的流上。现在假设在程序的某个部分里,想要将所有这些函数所生成的输出捕捉到一个文件里,那么可以打开一个文件并将得到的流赋予 *standard-output*
。现在底层函数们将把它们的输出发往该文件。
这样工作得很好,但假如完成工作时忘记将 *standard-output*
重新设置回最初的流上,那么程序中所有用到 *standard-output*
的其他代码也会将把它们的输出发往该文件。
真正所需的代码包装方式似乎应如下所述:“在从这里以下的所有代码中——所有它调用的函数以及它们进一步调用的函数,诸如此类,直到最底层的函数全局变量 *standard-output*
都应使用该值。”然后当上层的函数返回时,*standard-output*
应该自动恢复到其原来的值。
这看起来正像是 Common Lisp 的另一种变量,即动态变量所做的事。当绑定了一个动态变量时,例如通过一个 变量 变量或函数形参,在被绑定项上所创建的绑定替换了在绑定形式期间的对应全局绑定。与一个词法绑定——只能被绑定形式的词法作用域之内的代码所引用——所不同的是,动态绑定可以被任何在绑定形式执行期间所调用到的代码所引用。显然所有全局变量事实上都是动态变量。
因此,如果想要临时重定义 *standard-output*
,只需重新绑定它即可,比如说可以使用 变量。
(变量 ((*standard-output* *some-other-流*)) (stuff))
在任何由于调用 stuff
而运行的代码中,对 *standard-output*
的引用将使用由 变量 所建立的绑定,并且当 stuff
返回并且程序控制离开 变量时,这个对 *standard-output*
的新绑定将随之消失,并且接下来对 *standard-output*
的引用将看到 变量 之前的绑定。在任何给定时刻,最近建立的绑定会覆盖所有其他的绑定。从概念上讲,一个给定动态变量的每个新绑定都将被推到一个用于该变量的绑定栈中,而对该变量的引用总是使用最近的绑定。当绑定形式返回时,它们所创建的绑定会被从栈上弹出,从而暴露出前一个绑定。
一个简单的例子就能揭示其工作原理。
(空值全局变量 *x* 10) (函数 临时 () (格式 t "X: ~d~%" *x*))
上面的 空值全局变量 为变量 *x*
创建了一个到数值10的全局绑定。函数 foo
中,对 *x*
的引用将动态地查找其当前绑定。如果从最上层调用foo
,由 空值全局变量 所创建的全局绑定就是唯一可用的绑定,因此它打印出 10:
CL-USER> (临时) X: 10 NIL
但您可以使用变量创建临时隐藏全局绑定的新绑定,临时并将打印不同的值。
CL-USER> (变量 ((*x* 20)) (临时)) X: 20 NIL
但你也可以用 变量 创建一个新的绑定来临时覆盖全局绑定,这样 foo
将打印一个不同的值:
CL-USER> (临时) X: 10 NIL
现在定义另一个函数:
(函数 bar () (临时) (变量 ((*x* 20)) (临时)) (临时))
注意到中间那个对 foo
的调用被包装在一个将 *x*
绑定到新值 20 的 变量 形式中。运行 bar
得到的结果如下所示。
CL-USER> (bar) X: 10 X: 20 X: 10 NIL
正如你所看到的,第一次对 foo
的调用看到了全局绑定,其值为 10。然而,中间的那个调用却看到了新的绑定,其值为 20。但在 变量 之后,临时 再次看到了全局绑定。
和词法绑定一样,赋予新值仅会影响当前绑定。为了理解这点,可以重定义 foo
来包含一个对 *x*
的赋值。
(函数 临时 () (格式 t "Before assignment~18tX: ~d~%" *x*) (赋值 *x* (+ 1 *x*)) (格式 t "After assignment~18tX: ~d~%" *x*))
现在 foo
打印 *x*
的值,对其递增,然后再次打印它。如果你只运行 foo
,你将看到这样的结果:
CL-USER> (临时) Before assignment X: 10 After assignment X: 11 NIL
这看起来很正常,现在运行 bar
:
CL-USER> (bar) Before assignment X: 11 After assignment X: 12 Before assignment X: 20 After assignment X: 21 Before assignment X: 12 After assignment X: 13 NIL
注意到 *x*
从 11 开始——之前的 foo
调用真的改变了全局的值。来自 bar
的第一次对 foo
的调用将全局绑定递增到 12。中间的调用由于 变量 的关系没有看到全局绑定,然后最后一个调用再次看到了全局绑定,并将其从 12 递增到 13。
那么它是怎样工作的呢?变量 是怎样知道当它绑定 *x*
时,它打算创建一个动态绑定而不是一个词法绑定呢?它知道是因为该名字已经被声明为特别的(special)。 每一个由 空值全局变量 和 全局变量 所定义的变量其名字都将被自动声明为全局特别的。这意味着无论何时你在绑定形式中使用这样一个名字,无论是在 变量 中,或是作为一个函数形参,又或是在任何创建新变量绑定的构造中,被创建的绑定将成为一个动态绑定。这就是为什么命名约定如此重要——如果你使用了一个变量,以为它是词法变量,而它却刚好是全局特别的变量,这就很不好。一方面,你所调用的代码可能在你意想之外改变了绑定的值;而另一方面,你可能会覆盖一个由栈的上一级代码所建立的绑定。如果总是按照*命名约定来命名全局变量,就不会在打算建立词法绑定时却意外使用了动态绑定。
也有可能将一个名字声明为局部特别的,如果在一个绑定形式里将一个名字声明为特别的,那么为该变量所创建的绑定将是动态的而不是词法的。其他代码可以局部地声明一个名字为特别的,从而指向该动态绑定。尽管如此,局部特别变量使用相对较少,所以你不需要担心它们。
动态绑定使全局变量更易于管理,但重要的是注意到它们将允许超距作用的存在。绑定一个全局变量具有两种超距效果——它可以改变下游代码的行为,并且它也开启了一种可能性,使得下游代码可以为栈的上一级所建立的绑定赋予一个新的值。你应该只有在需要利用这两个特征时才使用动态变量。
6.4 常量
我尚未提到的另一种类型的变量是所谓的 “常值变量”。所有的常量都是全局的,并且使用 常量 的定义,常量 的基本形式与 全局变量 相似。
(常量 name initial-值-form [ 函数说明-字符串 ])
与 空值全局变量 和 全局变量 相似,常量 在其使用的名字上产生了一个全局效果——从此该名字仅被用于指向常量;它不能被用作函数形参或是用任何其他的绑定形式进行重绑定。因此,许多 Lisp 程序员遵循了一个命名约定,用以 +
开始或结尾的名字来表示常量,这一约定在某种程度上不像全局特殊名字的 *
命名约定那样流行,但同样也不错。
关于 常量,需要注意的另一点是,尽管语言允许通过重新求值一个带有一个初始值形式的 常量 来重定义一个常量,但在重定义之后究竟发生什么是没有定义的。在实践上,多数实现将要求任何对引用了该常量的代码进行求值以便它们能看到新值,因为老的值可能已经被内联到代码中了。因此最好只用 常量 来定义那些真正是常量的东西,例如π的值。而对于那些可能想改变的东西,则应转而使用 全局变量。
6.5 赋值
一旦创建了绑定,就可以对它做两件事:获取当前值以及为它设置新值。正如在第 4 章里所看到的,一个符号求值到它所命名的变量的值,因此,可以简单地通过引用这个变量来得到它的当前值。而为绑定赋予新值则要使用 赋值 宏——Common Lisp 的通用赋值操作符。下面是 赋值 的基本形式:
(赋值 place 值)
因为 赋值 是宏,所以它可以检查它所赋值的 place
上的形式,并展开成适当的底层操作来修改那个位置,当该位置是变量时,它展开成一个对特殊操作符 变量赋值 的调用,后者可以访问到词法和动态绑定。 例如,为了将值 10 赋给变量 x
,可以写成这样:
(赋值 x 10)
正如早先所讨论的,为一个绑定赋予新值对该变量的任何其他绑定没有效果。并且它对赋值之前绑定上所保存的值也没有任何效果。因此,函数
正如早先所讨论的,为一个绑定赋予新值对该变量的任何其他绑定没有效果。并且它对赋值之前绑定上所保存的值也没有任何效果。因此,函数
(函数 临时 (x) (赋值 x 10))
中的 赋值 对于 临时 之外的任何值都没有效果,这个当 临时 被调用时所创建的绑定被设置到 10,立即替换了作为参数传递的任何值。特别是在如下形式中。
(变量 ((y 20)) (临时 y) (打印 y))
将打印出 20 而不是 10,因为传递给 临时 的 y
的值在该函数中变成了 x
的值,随后又被 赋值 设置成新值。
赋值 也可用于依次对多个位置赋值。例如,与其像下面这样:
(赋值 x 1) (赋值 y 2)
也可以写成这样:
(赋值 x 1 y 2)
赋值 返回最近被赋予的值,因此也可以像下面的表达式那样嵌套调用 赋值,将 x
和 y
赋予同一个随机值:
(赋值 x (赋值 y (随机数 10)))
6.6 广义赋值
当然,变量绑定并不是唯一可以保留值的位置,Common Lisp 还支持复合数据结构,包括数组、哈希表、列表以及由用户定义的数据结构,所有这些都含有多个可用来保存值的位置。
后续的章节里将讨论那些数据结构,但就目前所讨论的赋值主题而言,你应该知道 赋值 可以为任何位置赋值。当描述不同的复合数据结构时,我将指出哪些函数可以作为 赋值 的 “位置” 来使用。总之,如果需要对位置赋值,那么几乎肯定用到 赋值。虽然在此不予介绍,但 赋值 经拓展后甚至可为由用户定义的位置赋值。
从这个角度来说,赋值和多数源自 C 的语言中的 =
赋值操作符没有区别。在那些语言里,=
操作符可以将新值赋给变量、数组元素和类的字段。在诸如 Perl 和 Python 这类支持哈希表作为内置数据类型的语言里,=
也可以设置哈希表项的值。表 6-1 总结了 =
在这些语言里的不同用法。
表6-1。用=其他语言分配
分配给…… | Java,C,C ++ | Perl | Python |
……变量 | x = 10; | $x = 10; | x = 10 |
…数组元素 | a[0] = 10; | $a[0] = 10; | a[0] = 10 |
…哈希表条目 | — | $h移位{‘key’} = 10; | h移位[‘key’] = 10 |
……对象中的字段 | o.区域 = 10; | $o->{‘区域’} = 10; | o.区域 = 10 |
赋值 以同样的方式工作——赋值 的第一个参数用来保存值的位置,而第二个参数提供了值。和这些语言中的 =
操作符一样,你可以使用和正常获取其值相同的形式来表达位置。 因此,表 6-1 中赋值语句的 Lisp 等价形式分别为:取数组 是数组访问函数,取哈希表 做哈希表查找,而 区域 可能是一个访问某用户定义对象看名为 区域 的成员的函数。如下所示:
简单变量: (赋值 x 10) 数组: (赋值 (取数组 a 0) 10) 哈希表: (赋值 (取哈希表 'key h移位) 10) 名为'区域'的插槽:(赋值 (区域 o) 10)
注意,当到用 赋值 赋值对一个作为更大对象一部分的位置进行赋值时,与赋值一个变量具有相同的语义:被修改的位置对之前保存在该位置上的对象没有任何影 响。再一次,这跟 =
在 Java、Perl 和 Python中的行为非常相似。
6.7 其他修改位置的方式
尽管所有的赋值都可以用 赋值 来表达,但有些固定模式(比如例基于当前值来赋予新值)由于经常使用,因此有它们自己的操作符。例如,尽管可以像这样使用 赋值 来递增一个数:
(赋值 x (+ x 1))
或是像这样来递减它:
(赋值 x (- x 1))
但这跟 C 风格的 ++x
和 --x
相比就显得很冗长了。相反,可以使用宏 递增 和 递减,它们以默认为 1 的特定数量对一个位置的值进行递增和递减。
(递增 x) === (赋值 x (+ x 1))(递减 x) === (赋值 x (- x 1))(递增 x 10) === (赋值 x (+ x 10))
类似 递增 和 递减 这种宏被称为_修改宏_(modify macro),修改宏是建立在 赋值 之上的宏,其基于作用位置上的当前值来赋予该位置一个新值,修改宏的 主要好处是,它们比用 赋值 写出的同样修改语句更加简洁。另外,修改宏所定义的方式使其可以安全地用于那些表达式必须只被求值一次的位置。一个有趣的例子是下面这个表达式,其中的 递增 会递增一个数组中任意元素的值:
(递增 (取数组 *array* (随机数 (长度 *array*))))
将如果将它就地转换成一个 赋值 表达式可能看起来像这样:
(赋值 (取数组 *array* (随机数 (长度 *array*))) (1+ (取数组 *array* (随机数 (长度 *array*)))))
但这不会正常工作,因为两次对 随机数 的调用不一定能返回相同的值——该表达式将很可能抓取数组中一个元素的值,将其递增,然后将其作为新值保存到另一个不同的数组元素上。与之相比,上面的 递增 表达式却能产生正确的行为,因为它知道如何处理这个表达式:
(取数组 *array* (随机数 (长度 *array*)))
取出其中可能带有副作用的部分,从而确保它们仅被求值一次。在本例中,经展 开后,它差不多会等价于以下形式。
(变量 ((tmp (随机数 (长度 *array*)))) (赋值 (取数组 *array* tmp) (1+ (取数组 *array* tmp))))
一般而言,修改宏可以保证以从左到右的顺序,对它们的参数和位置形式的子形式每个只求值一次。
第 3 章里那个微型数据库中曾用来向 *db*
变量添加元素的 添加 宏则是另一个修改宏。第 12 章在讲到如何在 Lisp 中表示列表时会详细地介绍它及其对应的 移除前 和 PUSHNEW 是如何工作的。
最后有两个稍微有些难懂但很有用的修改宏,它们是 交换 和 值左移。交换 在位置之前旋转它们的值。如果有两个变量 a
和 b
,那么如下调用
(交换 a b)
将交换两个变量的值并返回NIL。由于 a
和 b
是变量并且你不需要担心副作用,因此前面的 交换 表达式等价于下面这个:
(变量 ((tmp a)) (赋值 a b b tmp) nil)
其他类型位置上的 赋值 等价表达式可能会更加复杂一些。
值左移 与之相似,除了它将值向左侧移动而不是旋转它们——最后一个参数提供了一个值用来移动到最数第二个参数上,而其他的值将向左移动一个,第一个参数 的最初的值将被简单地返回。这样,下面的表达式
(值左移 a b 10)
将等价于如下形式。同样,不必担心副作用:
(变量 ((tmp a)) (赋值 a b b 10) tmp)
交换和 值左移 都可被用于任意多个参数,并且和所有的修改宏一样,它们可以保证以从左到右的顺序对每个参数仅求值一次。
学完了 Common Lisp 函数和变量的基础知识以后,下面将开始介绍一个令 Lisp 始终区别于其他语言的重要特性:宏。
1动态变量有时也称为特殊 变量,原因您将在本章后面看到。重要的是要注意这个同义词,因为一些人(和Lisp实现)使用一个术语而其他人使用另一个术语。
2早期Lisps倾向于将动态变量用于局部变量,至少在解释时。Elma,Emacs中使用的Lisp方言,在这方面有点回归,继续只支持动态变量。其他语言概括了从动态到词汇变量的这种转变 – local例如,Perl的变量是动态的,而my在Perl 5中引入的变量是词汇的。Python从来没有真正的动态变量,只是在2.2版中引入了真正的词法作用域。(与Lisp相比,Python的词汇变量仍然有限,因为语言的语法中赋值和绑定的混合。)
3实际上,总是会检测到所有类型错误并不完全正确 – 可以使用可选声明来告诉编译器某些变量将始终包含特定类型的对象并关闭某些区域中的运行时类型检查代码 但是,这种声明用于在开发和调试代码之后优化代码,而不是在正常开发期间。
4作为优化,某些类型的对象(例如低于特定大小和字符的整数)可以直接在内存中表示,其中其他对象将由指向实际对象的指针表示。但是,由于整数和字符是不可变的,因此在不同变量中可能存在多个“相同”对象的副本并不重要。这是第4章中讨论同样和相同讨论的根源。
5在编译器 – 编写器术语中,Common Lisp函数是“按值传递”。但是,传递的值是对对象的引用。这类似于Java和Python的工作方式。
6变量表单和函数参数中的变量由完全相同的机制创建。实际上,在一些Lisp方言中 – 虽然不是Common Lisp– 变量只是一个宏,它扩展为对匿名函数的调用。也就是说,在那些方言中,有以下几种:
(变量 ((x 10)) (格式 t "~a" x))
是一个宏形式,扩展为:
((表达式 (x) (格式 t "~a" x)) 10)
7 Java将全局变量伪装成公共静态字段,C使用extern 变量,Python的模块级和Perl的包级变量同样可以从任何地方访问。
8如果您特别想要重置空值全局变量ed变量,可以直接使用赋值它设置 或使用它进行非绑定MAKUNBOUND,然后重新评估空值全局变量表单。
9如果系统是多线程的,临时重新分配* st与ard-output *的策略也会中断 – 如果有多个控制线程试图同时打印到不同的流,它们都会尝试将全局变量设置为他们想要使用的流,互相踩踏。您可以使用锁来控制对全局变量的访问,但是您并没有真正获得多个并发线程的好处,因为无论打印什么线程都必须锁定所有其他线程,直到它们完成为止,即使他们想要打印到不同的流。
10可以参考约束的间隔的技术术语是其范围。因此, 范围和范围是互补的概念 – 范围是指空间,而范围是指时间。词汇变量具有词汇范围,但无限期,意味着它们会无限期地保持不变,这取决于它们需要多长时间。相比之下,动态变量具有不确定的范围,因为它们可以从任何地方而不是动态范围引用。为了进一步混淆问题,不确定范围和动态范围的组合经常被误称的动态范围所指。
11虽然标准没有规定如何将多线程结合到Common Lisp中,但提供多线程的实现遵循在Lisp机器上建立的实践,并在每个线程的基础上创建动态绑定。对全局变量的引用将查找最近在当前线程或全局绑定中建立的绑定。
12这就是动态变量有时也被称为特殊变量的原因。
13如果你一定要知道,你可以看一下 DECLARE,SPECIAL和LOCALLY在HyperSpec。
14语言本身定义的几个关键常量不遵循这个惯例 – 尤其是T和NIL。当想要t用作局部变量名时,这偶尔会很烦人。另一个是PI,它保持数学常数pi的最佳长浮点近似。
15一些老派Lispers喜欢使用变量赋值变量,但现代风格倾向于赋值用于所有作业。
16查找 DEF赋值,DEFINE-赋值-EXP与ER了解更多信息。
17 Algol派生语法在左侧的“地点” =和右侧的新值的分配的普遍性产生了术语左值,“左值”的缩写,意思是可以分配给的东西,和 r值,意思是提供价值的东西。编译器黑客会说,“ 赋值将其第一个参数视为左值。”
18 C程序员可能想要将变量和其他地方视为持有指向真实对象的指针; 分配给变量只是更改它指向的对象,而分配给复合对象的一部分类似于通过指向实际对象的指向。C ++程序员应该注意到=C ++在处理对象时的行为- 即成员副本 – 是非常特殊的。
http://mip.i3geek.com