Scheme的变量有一定的词法作用域,即它们在程序代码中只对特定范围的代码结构可见。迄今为止我们所见过的全局变量也没有例外的:它们的作用域是整个程序,这也是一种特定的作用范围。
我们也碰见过一些示例包含局部变量。它们都是lambda过程的参数,当过程被调用时这些变量会被赋值,而它们的作用域仅限于在过程的内部。例如:
(define x 9)
(define add2 (lambda (x) (+ x 2)))
x => 9
(add2 3) => 5
(add2 x) => 11
x => 9
这里有一个全局变量x
,还有一个局部变量x
,就是在过程add2
中那个字母x
。全局变量x
的值一直是9。第一次调用add2
过程时,局部的x
会被赋值为3,而第二次调用add2
时,局部变量x
的会被赋值为全局变量x
的值,即9
。
当过程的调用结束时,全部变量x
仍然是9。
而set!
代码结构可修改变量的赋值。
(set! x 20)
上面代码将全局变量x
的值9修改为20,因为对于set!
全局变量是可见的。如果set!
是在add2
过程体内被调用,那修改的就是局部变量x
:
(define add2
(lambda (x)
(set! x (+ x 2))
x))
这里set!
在局部变量x
上加上2,并且会返回局部变量x的新值。(从结果来看,我们无法区分这个过程和先前的add2
过程)。
我们可以像先前一样使用全局的x
做参数值来调用add2
:
(add2 x) => 22
(记住全局变量x的值现在是20,而不是9!)
add2
过程内的set!
调用仅会影响局部变量x。尽管局部变量x被赋了全局变量x的值,但后者不会因为set!
为局部变量x
赋值而受影响。
x => 20
注意我们做这些讨论是因为我们为局部变量和全局变量使用了同样的标识x
。在某些代码中,这个叫x
的标识符指的是语法闭包中的局部x
变量,这会暂时隐藏闭包外或全局变量x
的值。例如,
(define counter 0)
(define bump-counter
(lambda ()
(set! counter (+ counter 1))
counter))
bump-counter
是一个没有参数的过程(没有参数的过程也称作thunk
). 它没有引入局部变量和参数,这样就不会隐藏任何值。在每次调用时,它会修改全局变量counter
的值,让它增加1,然后返回它当前的值。下面是一些bump-counter
的成功调用示例:
(bump-counter) => 1
(bump-counter) => 2
(bump-counter) => 3
并不是一定要显式的创建过程才可以创建局部变量。有个特殊的代码结构let可以创建一列局部变量以便在其结构体中使用:
(let ((x 1)
(y 2)
(z 3))
(list x y z))
=> (1 2 3)
和lambda
一样,在let
结构体中,局部变量x
(赋值为1)会暂时隐藏全局变量x
(赋值为20)。
局部变量x
、y
、z
分别被赋值为1、2、3,这个初始化的过程并不作为let
过程结构体的一部分。因此,在初始化时对x
的引用都指向了全局变量x
,而不是局部变量x
。
(let ((x 1)
(y x))
(+ x y))
=> 21
上面代码中,因为局部变量x
被赋值为1,而y
被赋上了值为20的全局变量x
。
有时候为了方便,希望用let
依次创建局部变量,即在初始化区域中用先创建的变量为后创建的变量赋值。let*
结构就可以这样做:
(let* ((x 1)
(y x))
(+ x y))
=> 2
在初始化y变量时的x,指的是前面刚创建好的变量x。
这个例子完全等价于下面这个let
嵌套的程序,更深了说,实际上就是let
嵌套的缩写。
(let ((x 1))
(let ((y x))
(+ x y)))
=> 2
我们也可以把一个过程做为值赋给变量:
(let ((cons (lambda (x y) (+ x y))))
(cons 1 2))
=> 3
在这个let
构结体中,变量cons
将它的参数进行相加。而在let
结构的外面,cons
还是用来创建点对。
一个词法变量如果没有被隐藏,在它的作用域内一直都为可见状态。有时候,我们有必要将一个词法变量临时地设置为一个固定的值。为此我们可使用fluid-let
结构(fluid-let
是一个非标准的特殊结构。可参见8.3,在Scheme中定义fluid-let)。
(fluid-let ((counter 99))
(display (bump-counter)) (newline)
(display (bump-counter)) (newline)
(display (bump-counter)) (newline))
这和let
看起来非常相像,但并不是暂时的隐藏了全局变量counter的值,而是在fluid-let
执行体中临时的将全局变量counter
的值设置为了99直到执行体结束。因此执行体中的三句display
产生了结果
100
101
102
当fluid-let
表达式计算结束后,全局变量counter
会恢复成之前的的值。
counter => 3
注意fluid-let
和let
的效果完全不同。fluid-let
不会和let
一样产生一个新的变量。它会修改已经存的变量的值绑定,当fluid-let
结束时这个修改也会结束。
为了清楚的说明这一些,可以思考这个根据前一个示例用let
替换fluid-let
后的程序。这次的输出是
4
5
6
即,初始值为3的全局变量counter
,被每一次bump-counter
的调用更新。而新创建的初始值为99的词法变量counter
并没有影响到bump-counter
的执行,因为尽管bump-counter
是在局部变量counter
的作用域内被调用的,但bump-counter
的结构体并不在这个作用域内。所以bump-counter
中的counter
仍然指的是全局变量counter
,最后的值为6。
counter => 6