程序中的命名与作用域

最近在读《代码之髓》,里面讲解了很多编程语言中的concepts是为何产生的。本篇笔记主要参考了书中的第7章:名字与作用域。

转自:米游社@战小医仙

命名

计算机在执行程序时是根据内存地址中的信息来执行的,其无需得知每个变量的名字是什么。因此早期的编程中是没有命名这一概念的。后来为了方便人在编程时对变量所代表的含义和类型进行记忆,才开始给变量和函数进行取名。例如,相比于"经度:108.654519、纬度:34.255314"来说,中国西部科技创新港更容易理解和记忆,又比如相比"8.7.198.46"来说,"某个不存在的网站"这种描述更令人印象深刻。

那么计算机是如何将名字和变量及函数对应起来的呢,答案是使用对照表。这类似与我们在图书馆里找一本书,通过在数据库中检索,便可以将书名转换为分类编号,再通过分类编号既可以找到想要借阅的书在哪里。以创新港的图书馆为例:

1
《代码之髓》=>TP312/159,《古今数学思想第一册》=>O11/510-1/C.1

有了这样的对照表,计算机在处理"找到《代码之髓》"这个命令时,便可以转换成"找到编号为TP312/159的图书"来进行执行。

以python为例:

1
2
3
4
5
6
7
x = 'python'
y = 'hello'
z = x

id(x) # => 2256008235312
id(y) # => 2256008254512
id(z) # => 2256008235312

在上述程序中,计算机建立了如下的对照表:

graph LR
x[x]-->a[2256008235312]
y[y]-->b[2256008254512]
z[z]-->a

在上面的对照表中,变量x和变量z指向了同一个内存地址,因此,当变量z发生改变时,变量x也会相应的发生改变。只不过python中str不可变罢了

命名冲突

在早期的程序设计中,对照表是整个程序所共有的。这就会产生一些问题,例如下面这段C++代码:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;

int i=0;

int main()
{
for (i=0;i<10;i++){
print();
}
}

这个循环理应运行10次结束,然而,如果在函数print中对i进行了操作呢:

1
2
3
4
void print(){
cout<<"ok"<<endl;
i--;
}

由于变量i在函数内部和函数外部中都被使用了,使得其无法在预期的10次for循环之后直接结束。将会无限次循环。

如何避免这种命名冲突呢,一种方法是取更长的名字,在名字中说明这个变量是用于哪里的。例如i_in_printi_in_main,在多人合作开发时采用变量名申请制来进行管理。

然另一种方法就是引入作用域。下文将主要围绕作用域展开。

作用域

作用域是指名字的有效范围,为了不让print函数中对i的操作影响到外部,那么在函数执行之前将其值存储起来,在函数执行结束之后再取出来便可以解决这个问题。这种机制被称为动态作用域

动态作用域

Perl语言为例,在函数执行入口保存变量值,在结束时重新写回变量

1
2
3
4
5
sub shori{
$old_i = $i;
# do something
$i = $old_i
}

然而,这样的作法存在一个问题,那就是函数在嵌套调用时,对变量的修改会作用到被调用的函数中,以下面这两个函数为例:

1
2
3
4
5
6
7
8
9
10
$x = "global"

sub yobu {
local $x = "yobu";
&yobarebu();
}

sub yobarebu {
print "$x\n";
}

调用函数yobu时,变量x被改写为yobu,在函数运行结束重新写回之前,调用yobarebu函数时显示使用的变量x的值为yobu。这说明函数yobu中对变量的更改对于在其中调用的函数运行有影响。

产生这种现象的原因时,在动态作用域中,所有变量共享一张全局对照表,而在进入函数时,会创建一张动态对照表,这张表是所有函数共享的,所有的函数都可以读取到,这就造成了上述现象的发生。

总结来说,在动态作用域中,每个变量在查找对应的地址时,按照由近到远的顺序来查找。这造成了函数嵌套调用时相互影响的现象,为了解决这种现象,我们可以给每个函数使用单独的对照表进行存储变量,这就是静态作用域。

静态作用域

静态作用域每个函数单独使用一张变量表。计算机在寻找变量时,首先从函数内部的变量表中进行查找,如果找不到则到全局变量表中进行查找。这很好的解决了动态作用域下函数嵌套调用时产生的问题。

然而,静态作用域在使用时也存在一些问题。一是嵌套函数的问题,以Python为例:

1
2
3
4
5
6
7
x = "global"
def foo():
x = "foo"
def bar():
print(x)
bar()
foo()

在该嵌套函数中,bar函数中输出的x乍一看应该是foo,然而,在Python 2中,函数bar在查找变量x时,首先查找局部对照表,没找到时直接在全局对照表中进行查找,因此输出结果是global。这种设计给程序带来了很多误解。直到Python 3中才解决了这个问题。

第二个问题是外部作用域的再绑定问题。当我们想要在函数中修改一个外部作用域的值时,反而会创建一个新的变量,从而无法修改外部作用域的值。例如:

1
2
3
4
5
6
7
8
9
x = "global"
def foo():
x = "foo"
def bar():
x = "bar"
# 创建了一个新的x
# 无法修改外面的x
bar()
foo()

在Python 3.0中,引入了nonlocal关键字来解决这个问题:

1
2
3
4
5
6
7
8
9
10
x = "global"
def foo():
x = "foo"
def bar():
nonlocal x
x = "bar"
# 创建了一个新的x
# 无法修改外面的x
bar()
foo()

小结

计算机技术的发展带来了更强大的计算能力,从而出现了越来越复杂的程序。命名与作用域的发展凸显了当程序规模很大时,为变量和函数命名时人与计算机之间的矛盾。编程语言中的任何特性都并非凭空产生,而是为了解决某些问题而出现的。