程序原本(二十二):语言及其面临的系统——语言(你真的理解这行代码吗?)

阅读数:30 2019 年 9 月 28 日 18:10

程序原本(二十二):语言及其面临的系统——语言(你真的理解这行代码吗?)

绑定有很多种情况,几乎所有在程序代码中用到的标识,都存有绑定的问题。最常见的例如变量9

9 请容许我再次指出 Dijkstra 在《结构程序设计》中所说:“人们一旦了解在程序设计中如何使用变量,他就掌握了程序设计的精华。”

复制代码
var aNum = 100;

上述一句代码(的语法),对应着三个语义:

(1) 有一个标识,记为aNum

(2) 标识所指代的数据,其值是可变的(即,变量);

(3) 该数据当前值为 100。

对于大多数语言来说,第二个语义是一项执行过程中的限制,亦即表明任何可以访问到 aNum 标识的代码都可以改变它的值。为了简便,我们暂不讨论这一个语义的绑定问题。但第一、三个语义所述的:

有标识aNum,其数据当前值为 100

就明显作用于我们的计算:如果数据无值,或值不确定,则我们无法进行后续的计算过程。所以对于代码aNum = 100,其在语义上的“值 100”将在何时被绑定到标识aNum,就是一个相当重要的问题。

以一个代码片段来看(相信我,这是实际可运行的代码):

复制代码
// 代码1
function process_part_one() {
aNum = aNum * 2;
var aNum = 100; 5
}

根据我们对这段代码的语法约定,第2、5行决定了代码片段中的标识符的生存周期10。但在这个生存周期中,aNum是何时有“值 100”的含义的呢?

10 如你所知的,这是一个函数。大多数语言的函数,都约定了其(形式上的)函数体内的标识符的生存周期。

对于这个问题,一些语言认为,表达“aNum变量具有初值 100”这样的语义,应该是计算过程的前设,即对计算前提条件的声明。而声明并不是计算,因此声明语法与执行语法也应当分别对待。例如 Pascal,就处理为这样的语法(在var部分做声明,在begin...end部分处理执行):

复制代码
// 代码2, pascal 风格
procedure process_part_one;
var
aNum = 100;
begin
aNum := aNum * 2;
end;

另一些语言则认为,声明语法可以理解为对执行环境的预置,也有执行含义,因此可以允许“即用即声明”,例如 C。但是语义上,这是因“(需)即用”而进行的声明,不可能出现“已用”而未声明的情况。所以即使 C 语言,也会因为语义上无法解释,而判断上述代码 1 违例。

还有一些语言认为:

复制代码
var aNum

是一个语义,它只表明标识aNum的存在,直到函数调用时才绑定另外一个语义:初始操作,亦即是为函数内所有存在的标识进行初始化。在这样的认识下,“代码1”中的

复制代码
// 代码3
function process_part_one() {
var aNum
}

就具有了完整的语义。这个语义(即“初始操作”)与实际的计算机行为,是在把process_part_one()作为函数调用时,才进行绑定和实施的。其效果是:

  • 在生命周期(代码3的2~4行,或代码1的2~6行)中,记有一个标识aNum;且,
  • 在该标识所处生命周期开始时,总会有一个初始化动作,使该标识具有一个初值:无值;且,
  • 设整个计算环境中,无值是一种值,记为undefined

你可能已经知道,这就是 JavaScript 的实现方法11。进一步地,除开上述代码在生命周期上的解释之外,代码1被理解为:

11 在 JavaScript 的实现中,这里被实现为闭包。当一个闭包被创建时,它的上下文自然被初始化,而非(显式地)进行一个赋以初值的操作。你可以认为闭包是一个内存块,它创造的时候就是一块空的内存 (full by Nil)。

复制代码
// 代码4
function process_part_one() {
aNum = aNum * 2; // aNum 有初值,记为 undefined
aNum = 100;
}

我们看到,代码1中的语法:

复制代码
var aNum = 100

所具有的两个语义(本小节开始处的语义 1 和语义 3)被分别绑定在函数调用开始和代码4的第4行;且一个标识的初值含义,总是明确地被约定为undefined。由此一来,上述代码1整体的语义就确定了。

评论

发布