变量和作用域
程序设计中一个必不可少的方面就是它需要提供一种通过名字(标识符)去使用计算对象的方式。他对应的就是程序中的变量,他的值也就是它所对应的对象。
函数名也算是一种变量
标识符规则
就像英文单词由 26 英文字母组成一样,标识符也有自己的规则。大多数语言都遵循以下规则:
- 只能包含字母、数字和下划线(JavaScript/Java 中 $ 符号也合法)
- 不能以数字开头
- 区分大小写
- 不能包含保留字(通常就是该语言的关键字)
目前的主流编程语言的字母比较宽泛甚至支持 Unicode 字符,因此中文编程不是梦。
变量定义
变量定义大致分为两种类型:
- 定义变量需要指定类型: C/java/C# 等都属于这一类,他们通常也被称为静态语言
- 定义变量不需要指定类型: Python/Scheme/JavaScript 等都属于这一类,他们也被称为动态语言
注意动态语言不一定就是弱类型
静态和动态的在变量定义的核心区别就是变量是否绑定数据类型。对于静态语言来说变量绑定数据类型,而数据类型决定了数据能够执行哪些操作,这样就能够在编译期间进行检查。但是他就让整个程序缺乏灵活性。而静态语言变量不绑定数据类型,数据类型由数据对象自身持有。
他们之间没有好坏之分,笼统说:静态语言比较健壮(编译时报错)通常用于开发大型程序,动态语言比较灵活通常作为脚本使用。
scheme 中通过 define 来定义变量:
JavaScript 中通过 var let 来定义变量,比较特殊的是他也运行不添加任何修饰来定义全局变量:
python 不需要添加任何修饰来定义变量:
java 则需要为变量指定类型, java10 中引入了 var 来根据初始值推断他的类型:
常量
常量是变量的特殊形式,它意味着值不允许改变,大多数语言都通过常量修饰符来指定变量为常量。
约定俗称的常量命名规则是全部大写,尽管它并不是强制的但是应当遵守。
JS 中通过 const 修饰来定义常量:
Python 没有常量语法,约定俗称的就是大写标识符的是常量:
java 中通过 final 来修饰常量:
环境或作用域
变量本质上是一个计算对象的映射。既然是映射就需要一个映射表来从变量获取该计算对象的值。而编程语言也确实是这么做的,他内部会维护一个变量到计算对象的映射表。当读取变量时会去查找这个映射表来获取计算对象的值。
但是这存在一个问题就是一旦变量过多就要考虑命名冲突的问题。而解决这个的方式就是引入作用域来区分环境。作用域本质上并不复杂,原来只有一个大家都能读取的映射表(全局环境),现在我们给他拆分了代码块、函数、类等维护自己的映射表(不同环境下的作用域)。
编程有个术语时上下文环境,它本质上就是说明在当前上下文能够读取那些变量,即能够读取哪一张映射表
整个作用域的发展经历了三个阶段:
- 全局作用域: 这个比较好理解,就是维护了一张映射表
- 动态作用域: 他同样是维护一张表,只不过在进入函数入口处(函数作用域下)将全局变量保存到一个新的变量中,在函数出口处给写会全局变量
- 静态作用域: 目前大多数现代语言都是用该类型作用域
动态作用域
这个只需要作为了解,他是在全局作用域上发展而来的。最初的很多语言都只有全局作用域,为了避免命名冲突程序员就在函数的入口处将原来的变量保存到一个新的变量中,在函数出口处给写回,例如 Perl 中:
这种需要程序员手动操作的往往会出现问题,Perl 4 中就引入了 local
声明来自动完成这一点:
但是动态作用域有一个非常大的问题就是被改写的值会影响到后续的函数调用:
$x = "global";
sub yubu{
local $x = "yobu";
%yobareru(); # --> 这里此时 $x 变为 yobu 了
}
sub yobareru{
print "$x\n";
}
&yobu(); # --> 实际输出 yobu
解决这个问题的方式也比较简单,通过参数的形式传递值。但是这又添加了程序员的负担。于是出现了静态作用域。
静态作用域
静态作用域简单粗暴,在需要作用域的时候直接创建一个新的映射表即可。在全局维护一个作用域,一旦进入函数将创建一个新的映射表,函数中的变量会直接添加到该映射表中。而读取会根据优先级首先读取函数映射表然后没有找到时读取全局映射表。
因此大多数静态作用域实际上就是全局作用域和函数作用域(Python 就是这两个),有些语言也引入了块作用域的概念(js 中 let 引入了块作用域)
他与动态作用域最大的区别就是: 静态作用域能够确保我们在编写程序时就知道他的变量具体属于哪个作用域,而不是像动态作用域那些有不同的可能。
x = "global" # 全局映射表
def yubu():
x = "yobu" # 进入函数会创建一个新的映射表,因此 x 变量在新的映射表中创建
print(f"x in yubu: {x}") # 会首先读取自己映射表中的 x
yobareru()
def yobareru():
print(f"x in yobareru: {x}") # 同样会读取自己映射表中的 x 如果没有找到就读取全局的
yubu()
# x in yubu: yobu
# x in yobareru: global
时至今日,所有的主流语言都选择使用静态作用域,当然在演化过程中他们之间也有些许不同。例如 LISP 本身是动态作用域,而他的方言 Schema 就是静态作用域。
JavaScript 对于没有任何声明的都属于全局作用域,而把 var 声明的变量是为静态作用域(函数作用域),甚至 ES6 引入了let 来进一步加强静态作用域(语法块作用域)。
晚一些面世的 Python 语言直接采用静态作用域,甚至不需要任何修饰。
Python的作用域问题
Python 的变量定义是不需要修饰的,即直接赋值就能够定义变量。由于常用静态作用域整体看起来好像没有问题,但是实际上存在两种弊病:
- 如果我们确实需要在函数内部修改全局变量该怎么办 => 使用 global 修饰
- 对于嵌套函数来说如果想要改变外部函数作用域的变量怎么办 => 使用 nonlocal 修饰
x = "global"
def foo():
x = 'new_global'
foo()
print(x) # -> global
def foo():
global x # 修饰表示 x 位于全局作用域中
x = 'new_global'
foo()
print(x) # -> new_global
def foo():
y = "old"
def bar(): # 嵌套函数同样有自己的作用域(实际上递归也有)
y = "new"
print(y) # new bar 嵌套函数的作用域中的值
bar()
print(y) # old
foo()
# new
# old
def foo():
y = "old"
def bar():
nonlocal y # nonlocal 修饰表示使用外部函数作用域
y = "new"
print(y) # new
bar()
print(y) # new
foo()
# new
# new
总结
变量本质上就类似 bash 中的 alias,即别名。因此可以将它抽象为一个具有映射关系的映射表。实际上程序内部也是这样实现的。
为了避免变量名重复以及更好的封装,我们只需要确保在不同的执行上下文能够看到的映射表内部不同就好了,这就是所谓的作用域或环境。大多数编程语言都具有块作用域(Python 没有,js由 let const 提供了支持)、函数作用域(类作用域实际上算函数作用域的一种)和全局作用域。
像 java 这种具有各种 public protected private 修饰的本质也是控制映射表的可见范围。