第五章 词法变量

优质
小牛编辑
122浏览
2023-12-01

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

5.1 let 和 let*

并不是一定要显式的创建过程才可以创建局部变量。有个特殊的代码结构let可以创建一列局部变量以便在其结构体中使用:

(let ((x 1)
      (y 2)
      (z 3))
  (list x y z))
=>  (1 2 3)

lambda一样,在let结构体中,局部变量x(赋值为1)会暂时隐藏全局变量x(赋值为20)。

局部变量xyz分别被赋值为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还是用来创建点对。

5.2 fluid-let

一个词法变量如果没有被隐藏,在它的作用域内一直都为可见状态。有时候,我们有必要将一个词法变量临时地设置为一个固定的值。为此我们可使用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-letlet的效果完全不同。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
<!-- -->