关于范围:静态(词法)范围与动态范围(伪代码)

Static (Lexical) Scoping vs Dynamic Scoping (Pseudocode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Program A()
{
    x, y, z: integer;

    procedure B()
    {
        y: integer;
        y=0;
        x=z+1;
        z=y+2;
    }

    procedure C()
    {
        z: integer;

        procedure D()
        {
            x: integer;
            x = z + 1;
            y = x + 1;
            call B();
        }

        z = 5;
        call D();
    }

    x = 10;
    y = 11;
    z = 12;
    call C();
    print x, y, z;
}

据我所知,使用静态作用域运行该程序的结果是:x=13、y=7和z=2。

但是,当使用动态范围界定运行它时,结果是:x=10、y=7和z=12。

这些结果就是我们教授给我们的。然而,我一辈子都无法理解他是如何取得这些成果的。是否有人可能遍历伪代码并在两种不同类型的作用域中解释它们的值?


对于静态(词汇)范围,程序源代码的结构决定了您所引用的变量。对于动态范围,程序堆栈的运行时状态决定了您所引用的变量。这可能是一个非常不熟悉的概念,因为现在广泛使用的每种编程语言(除了EmacsLisp)基本上都使用词汇范围,这对于人类和分析工具来说都是非常容易理解的。

考虑这个更简单的示例程序(用您的伪代码语法编写):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
program a() {
  x: integer; //"x1" in discussions below
  x = 1;

  procedure b() {
    x = 2; // <-- which"x" do we write to?
  }

  procedure c() {
    x: integer; //"x2" in discussions below
    b();
  }

  c();
  print x;
}

程序和编译器将这两个变量都称为x,但我已经将它们标记为x1x2,以便于下面的讨论。

通过词法范围界定,我们在编译时根据程序源代码的静态词法结构来确定所指的x。在定义b时,作用域中x的最里面的定义是x1,所以所讨论的写入解决了x1,这就是x = 2写入的地方,所以我们在运行这个程序时打印2

通过动态范围界定,我们有一堆在运行时跟踪的变量定义——因此,我们写入的x取决于范围内的确切内容以及在运行时动态定义的内容。开始运行ax => x1推到栈上,调用cx => x2推到栈上,当我们到达b时,栈顶是x => x2所以我们写入x2。这使x1保持原样,因此我们在程序结束时打印1

此外,考虑一下这个稍有不同的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
program a() {
  x: integer; //"x1" in discussions below
  x = 1;

  procedure b() {
    x = 2; // <-- which"x" do we write to?
  }

  procedure c() {
    x: integer; //"x2" in discussions below
    b();
  }

  c();
  b();
}

注:b两次调用,第一次通过c调用,第二次直接调用。在词汇范围内,上述解释没有改变,我们两次都写入x1。但是,对于动态范围,它取决于运行时如何绑定x。我们第一次调用b时,我们写入x2,如上所述--但第二次,我们写入x1,因为这是堆栈顶部的内容!(当c返回时弹出x => x2)

这是你教授的代码,用哪个精确变量来注释,用词法范围来写。在程序结束时打印的写入文件用*标记:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
program A()
{
    x, y, z: integer; // x1, y1, z1

    procedure B()
    {
        y: integer; // y2
        y=0; // y2 = 0
        x=z+1; // x1 = z1 + 1 = 12 + 1 = 13*
        z=y+2; // z1 = y2 + 2 = 0 + 2 = 2*
    }

    procedure C()
    {
        z: integer; // z2

        procedure D()
        {
            x: integer;  // x2
            x = z + 1; // x2 = z2 + 1 = 5 + 1 = 6
            y = x + 1; // y1 = x2 + 1 = 6 + 1 = 7*
            call B();
        }

        z = 5; // z2 = 5
        call D();
    }

    x = 10; // x1 = 10
    y = 11; // y1 = 11
    z = 12; // z1 = 12
    call C();
    print x, y, z; // x1, y1, z1
}

这里是动态范围。注意,只有在b*标签的位置发生了变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
program A()
{
    x, y, z: integer; // x1, y1, z1

    procedure B()
    {
        y: integer; // y2
        y=0; // y2 = 0
        x=z+1; // x2 = z2 + 1 = 5 + 1 = 6
        z=y+2; // z2 = y2 + 2 = 0 + 2 = 2
    }

    procedure C()
    {
        z: integer; // z2

        procedure D()
        {
            x: integer;  // x2
            x = z + 1; // x2 = z2 + 1 = 5 + 1 = 6
            y = x + 1; // y1 = x2 + 1 = 6 + 1 = 7*
            call B();
        }

        z = 5; // z2 = 5
        call D();
    }

    x = 10; // x1 = 10*
    y = 11; // y1 = 11
    z = 12; // z1 = 12*
    call C();
    print x, y, z;
}


静态作用域和动态作用域是在用任何语言编写的程序中查找具有特定唯一名称的特定变量的不同方法。

它特别有助于解释程序或编译器决定在何处以及如何查找变量。

考虑代码如下,

1
2
3
4
5
6
7
8
9
10
f2(){

   f1(){
   }

   f3(){
    f1()
   }

}

静态的:

这基本上是文本的,第一个变量被定义或不被签入本地函数(让我们把它命名为f1()),如果不在本地函数f1()中,那么变量将在包含此函数的函数f2()中被搜索(我的意思是f1()),这将继续……直到找到变量。

动态的:

这与静态不同,从某种意义上说,因为它更为运行时或动态,第一个变量被定义或不被签入本地函数,如果不在本地函数f1()中,那么变量将在调用此函数的函数f3()中被搜索(我的意思是f1()),这将继续……直到找到变量。


关键是词汇图如下所示:

1
B <- A -> C -> D

虽然调用图如下所示:

1
     A -> C -> D -> B

唯一的区别是B族的血统。在词汇图中,b直接定义在a(全局范围)的范围内。在动态图片中,b处的堆栈已经在c的顶部有d,然后是a。

这种差异相当于关键词xz在b中是如何解析的。从词法上讲,它们与A.xA.z标识,但动态上,它们与D.x标识,并且(由于不存在D.z)与C.z标识。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def B:
    B.y = 0
    x = z + 1
    z = y + 2
def C:
    def D:
        D.x = z + 1
        y = D.x + 1
        call B
    C.z = 5
    call D
A.x, A.y, A.z = 10, 11, 12
call C
print A.x, A.y, A.z

上面我试图更清楚地表示您的代码。注意,根据名称解析的两种方法,d突变A.y,而b只突变A.xA.z,如果选择词法而不是动态范围界定。

注意,虽然一个函数只定义了一次*,但是从多个地方调用它是很常见的(甚至可以递归地调用它自己)。因此,虽然使用静态代码执行词法范围界定非常简单,但是动态范围界定更复杂,因为在对该函数的不同调用期间(要求您单步执行程序并跟踪调用堆栈ch的方式),同一关键字(在同一函数中)可能解析为不同的变量(来自不同的名称空间)。执行过程中的更改)。

*(模板语言除外….)