众力资讯网

什么是变量?——访张基温教授

0. 引 言变量(variable)是程序中出现频率最高的术语。以前访问张教授时,他曾经说过,变量及其相关术语是目前国内

0. 引  言

变量(variable)是程序中出现频率最高的术语。以前访问张教授时,他曾经说过,变量及其相关术语是目前国内使用最混乱、概念错误最多的部分。当时就请张基温教授能否抽时间把这个问题系统地讲解一下。老人家答应了。前不久,我们也听到一些这方面的声音。于是联系张基温教授,看是否整理好了。他说,已经初步整理,但还没有细看。我们也不想拖了,就恳求张教授,能否把初稿先给我们看看。几次下来,他答应了,并嘱告大家仅仅是初稿,就算参加讨论的发言稿吧。

Variable is the most frequently used term in programming. During a previous visit to Professor Zhang, he once said that variables and their related terms are currently the most confusing and have the most conceptual errors in domestic usage. At that time, we asked Professor Zhang Jiwen if he could take the time to systematically explain this issue, and the elderly professor agreed. Not long ago, we also heard some voices on this matter. So we contacted Professor Zhang Jiwen to see if he had sorted it out. He said that he had made a preliminary arrangement but hadn't looked at it carefully. We didn't want to delay any longer, so we begged Professor Zhang to let us see the first draft. After several attempts, he agreed and told everyone that it was just a first draft, and it could be regarded as a speech draft for the discussion.

1. 变量的第一要义:计算对象角色化

1.1 数学中的变量的形成和内涵

通常认为,变量源自代数,而代数源于弗朗索瓦·韦达(François Viète,1540—1603)的 “字母符号革命”:将计算中的数据分为已知和未知两种角色,分别用辅音字母和元音字母代替来抽象地描述计算。大约1591年,他在著作《分析术引论》中,系统地提出符号代数思想,将方程的根与系数符号化,给出了二次方程的一般解法,将算术运算升级为符号代数。

但是,这种 “用角色名替代具体数值” 的抽象思想远在公元前,已经出现在古老的中国。成书于西汉(公元前202年—8年)的《周髀算经》中,就记载有数学家商高首提:“故折矩以为勾广三,股修四,径隅五”,并由此可以推测出,远在西周(前1046年—前771年)间,中国人就开始把直角三角形三边的具体数字分别用“勾”“股”“弦”命名,并揭示了勾股定理的算法。在最迟成书于东汉(25—220年)的《九章算术・方田》中,记载有“今有圆田,周三十步,径十步”,即将圆周长命名为 “周”,将直径命名为“径”,并得出了“周三径一”和“周径相乘,四而一”的计算规则。

此外,印度的婆罗摩岌多(C Brahmagupta),在公元628年成全书的《婆罗摩历算书》中已经提出二次方程通解公式,并使用梵文符号表示未知数;公元9世纪的阿拉伯罕默德·本·穆萨·阿尔·花剌子模(Abu Abdullah Muhammad ibn Muso al-Xorazmiy,约780—约850年)在《al-jabr(代数)》中已经提出六种标准方程(如ax² = bx);12—13世纪,意大利数学家斐波那契(Leonardo Pisano ,Fibonacci, Leonardo Bigollo,1175年-1250年)的《算盘书》将阿拉伯代数引入欧洲。

与上述这些中国、印度和阿拉伯等的成就相比,韦达的成就在于在时间节点上,正好链接了笛卡尔(René Descartes,1596—1650)等数学家的努力,形成了中世纪数学与现代数学,使数学从“算术技巧”发展为“结构科学”。

1637年,笛卡尔在《几何学》中用字母 x, y, z 确立了三维坐标系。

1673年,戈特弗里德·莱布尼茨(Gottfried Leibniz,1646—1716)在微积分研究中首次明确区分常量 (constant)与 变量 (variable)。

1718年,约翰·伯努利(Johann Bernoulli,1667—1748)定义函数 为“由变量和常量构成的表达式”。

1748年,莱昂哈德·欧拉(Leonhard Euler,1707—1783)在教科书《无穷小分析引论》中,给出了变量的经典定义:“变量是可以取不同值的量,其值的变化可能是连续的或离散的。” 使变量成为分析数学的标准术语,并区分自变量与因变量。

20世纪30年代中期开始,布尔巴基学派的结构主义兴起。他们认为是变量使得数学从 “具体数值计算”(如算术)迈向 “抽象结构研究”(如泛函分析)。他们将变量定义为 “逻辑角色的符号载体”。他们认为,这种角色化抽象让数学得以描述从代数方程到拓扑空间的复杂系统,是数学成为 “科学皇后” 的核心驱动力,并表现为如下三个特征:

(1)符号化:将数学从 “算学” 提升为 “符号科学”,实现对一般规律的表达;

(2)动态化:使数学能描述运动与变化;

(3)抽象化:从具体数值到代数结构、随机现象、空间变换,使变量成为连接数学各分支与现实世界的纽带。

1.2 在程序设计中,三种不同角色化引用对象的变量语义谱系

在程序设计语言中变量机制(Variable Notation),是从1954年问世的FORTRAN语言开始的。它是由IBM 的约翰·巴克斯(John Warner Backus,1924年—2007年)团队开发的世界第一款高级程序设计语言,旨在实现计算范式的三重突破:

在物理层,用角色化符号替代内存单元编号(如用 AREA 替代内存地址 #3047),并把这种角色化符号称为变量;

在语义层,用自然语言替代底层指令,如用=表示将一个数值送入一个存储单元;

在计算层,反映出计算过程中数据的运动和变化。

例1 一个计算圆面积的FORTRAN 54程序——变量名的角色特征示例。

这种基于角色抽象的变量设计思想,在 1958 年 Algol 58 的形式化语法中得到理论升华,其《算法语言报告》明确提出 "变量是计算对象的语义载体"。这一思想深刻影响了后续编程语言发展。使变量设计已形成3种基本语义谱系:

(1)原生型变量(Primitive Variables):也称直存型(Directly Stored Variables)。其核心语义是内存直接存储所引用的操作对象,对其进行角色化,代表性语言是C。

(2)引用型变量(Reference Variable):其核心语义是内存间接存储所引用的操作对象对其进行角色化,并采用垃圾回收机制(Garbage Collection,GC)回收其占用的内存空间,典型语言是Java。

(3)符号型变量(Symbolic Variables):其核心语义是在用变量名直接角色化所引用的操作对象,典型语言是Lisp。

2. 原生型变量与栈分配

2.1 原生型变量对操作对象的角色化

原生型变量是早期高级程序设计语言的变量类型。这种变量的特点是:每个变量都有自己独立的存储空间,用来存储承担这一角色的数据对象。用这类变量对操作对象的角色化过程如下:

S1:变量的声明与定义。在计算机早期,变量的声明和定义是同一概念,就是讲变量的名字和类型通知编译器。以便存储分配和类型检查等。并且在FORTRAN 54中,采用的是隐式类型规则(I-N规则):仅把操作对象分为两类:以 I、J、K、L、M、N 开头,默认为 INTEGER 类型;其他字母开头则为 REAL(浮点型)。到FORTRAN 57才提出数据类型关键字和显式声明(如INTEGER :: i, j),但显式声明并不强制。到了ALGOL 60 才设计了系统的类型关键字,如integer、real、boolean、char(字符)、array(数组)、record(记录)、procedure(子程序),并强制要求变量声明时指定类型。此时变量名不仅是角色符号,还关联了数据类型约束,就成为了“带类型的内存存储单元的命名”。到了C语言,将声明与定义区分,但Java又不区分。

S2:计算操作对象的表达式。

S3:将变量与计算得到的操作对象的值绑定——存储到为变量分配的存储空间中。在多数程序设计语言中,将这个操作称为assignment(因为是将操作对象的值送入变量,所以中国的计算机先辈将其翻译为汉语色彩的“赋值”),并用操作符“=”表示。

S4:此后,在程序中遇到一个变量名,就是引用了它所存储的对象值。

例2  将上述S1、S2、S3用两个C语句实现的C示例。

在C语言中,还可以进一步将以上三步归并为一个语句实现。

2.2  引入栈分配(stack allocation)

栈是一种先进后出存储结构。早在汇编语言时期,栈就被用于临时存储数据。1958年,Lisp问世,作为支持递归的函数式语言,其解释器也依赖“调用栈”管理递归调用,但其早期更偏向于解释器层面的“栈”,而非编译型语言的“栈”存储。1958年,艾兹格·W·迪科斯彻(Edsger Wybe Dijkstra,1930年—2002年)在设计Algol 60时,引入了块结构的概念,并使用栈为每个局部变量单独分配内存空间。从此栈成为现代高级语言的函数调用、局部变量关键技术,成为了模块化程序设计的基础机制。利用这一机制,可以由操作系统自动地进行局部变量的存储空间创建和回收。变量栈分配的过程如下。

S1:函数调用

S1.1: 保存上下文:将当前函数的寄存器值(如程序计数器 PC)压入栈。

S1.2: 传递参数:将函数参数压入栈(顺序因语言 / ABI 而异)。

S1.3: 跳转到被调用函数:更新 PC 到被调用函数的起始地址。

S2:栈帧创建

S2.1: 调整栈指针:为被调用函数的局部变量预留空间(通过减小 SP 值)。

S2.2: 初始化局部变量:在预留空间内分配,按声明或编译器优化顺序排列。

S3:变量使用:函数内通过栈指针偏移量访问局部变量。

S 4:函数返回

S4.1 销毁局部变量:无需显式操作,栈指针恢复到调用前的位置(增加 SP 值)。

S4.2 恢复上下文:从栈中弹出保存的寄存器值。

S4.3 返回值传递:通常通过寄存器(如 EAX)返回结果。

S4.4 跳回调用函数:恢复 PC 到调用函数的下一条指令。

栈分配使块内的变量的存储空间随着块的退出而自动回收,避免内存泄漏。这种分配释放内存的方式,对编程语言的发展产生了深远影响,逐渐演变成现在的栈分配机制。

3. 堆分配、GC与引用型变量

3.1 堆分配与GC

1)堆内存的基本结构

堆(heap)是内存中的一块连续区域,可以由程序设计者(而非操作系统)手动管理。1958年,LISP率先将堆机制用于动态变量存储(尤其是复杂数据结构)。尽管LISP的函数调用仍要依赖栈由传统方式实现,但它在堆的动态分配和回收机制上的创新,还是奠定了现代语言中堆的核心地位。

堆分配机制主要由如下两部分组成。

(1)堆管理器(如 glibc 的malloc实现)负责维护堆的分配状态,记录已分配和空闲的内存块。

(2)内存块:堆中的基本分配单位,包含:

头部信息:大小、是否已分配标记、指向前 / 后块的指针等。

用户数据区:实际存储变量或对象的区域。

填充字节:用于内存对齐(如按 8 字节或 16 字节对齐)。

2)堆分配(heap allocation)步骤

C 语言也是最早引入堆分配变量的语言之一。在 C 语言中,通过malloc、calloc、realloc等函数来实现堆内存的分配4。例如,使用malloc函数可以在堆上分配指定字节数的内存空间,返回一个指向该内存块的指针。calloc函数则是在堆内存中分配n * size个字节的存储空间,并将分配的存储空间中的原始数据清空。realloc函数主要用于修改之前通过malloc函数或者realloc函数分配内存块的大小4。当程序不再需要使用堆上分配的内存时,需要使用free函数来释放内存,以避免内存泄漏。

下面以C语言为例,介绍堆分配过程。

S1:请求堆内存。即程序通过函数(如 C 的malloc(size)、C++ 的new)向堆管理器请求指定大小的内存。

S2:堆管理器查找空闲块,有如下3种适应度。

(1)首次适应(First Fit):遍历空闲链表,找到第一个足够大的块。

(2)最佳适应(Best Fit):找到大小最接近请求的块,减少内部碎片。

(3)快速适应(Fast Fit):维护多个链表,每个链表存储特定大小的块。

S3:分配内存块,有如下4种分配策略。

(1)整块分配:若找到的块恰好等于请求大小,标记为 “已分配”。

(2)分割块:若块大于请求大小,将其分割为两部分:

(3)已分配块:大小等于请求,标记为 “已分配”。

(4)剩余空闲块:更新头部信息,插入空闲链表。

S4:返回内存地址。返回用户数据区的起始地址(跳过头部信息)。若未找到合适的块,进入S5。

S5:扩展堆。若堆空间不足,通过系统调用(如 Linux 的brk或mmap)向操作系统申请更多内存,将新内存添加到堆的尾部,并标记为空闲块。

3)GC

GC是一种自动管理计算机内存的机制,用于检测和释放程序不再使用的内存——“垃圾”,避免内存泄漏和手动管理内存的复杂性。

GC 通过不同的算法判断对象是否为“垃圾”并回收内存,常见算法包括:

(1)标记-清除(Mark and Sweep),操作包含:

S1:标记——从根对象(如全局变量、栈中的变量)出发,标记所有可达对象。

S2:清除——遍历堆内存,回收未被标记的对象,但可能产生内存碎片。

(2)引用计数(Reference Counting),记录每个对象被引用的变量的个数,引用数为 0 时立即回收。

(3)分代收集(Generational Collection),将对象按存活时间分为“年轻代”和“老年代”,优先回收年轻代对象。因为它们被引用的机会少。此算法应用于Java、C#、Go 等语言中。

(4)复制算法(Copying)。这种算法是将内存分为两块,每次只使用一块,将存活对象复制到另一块,清空原内存。

3.2 Java引用型变量及其对操作对象的角色化

引用型变量直接存储的是操作对象存储内存地址,而不是操作对象本身。这种对操作对象存储地址的抽象称为引用(reference), 如 C++ 的引用、Java 的对象引用。通过引用,程序可以间接访问并操作存储在内存中的对象。因此,可以说引用型变量是通过引用间接角色化操作对象。Java是一种典型的采用引用型变量的语言。

1)Java的引用型变量

Java 中的引用型变量(如Object obj)是对象的句柄(Handle)或地址的抽象表示,而非对象本身。Java 程序运行时的内存主要分为如下4块:

(1)堆:存储对象实例以及类的成员变量(非静态)和引用类型数组(元素存储的是对象引用),并由垃圾回收器(GC)自动管理。

(2)栈:存储局部变量、方法调用帧等。

(3)方法区(Method Area):存储类信息、静态变量、常量池等。

(4)本地方法栈(Native Method Stack):用于本地方法调用。

2)Java引用型变量对操作对象的角色化过程

(1)对象创建,如

在堆中分配内存创建 Person 对象,并用变量 p 存储该对象的引用(地址)。

(2)引用传递与共享,形成角色化效果,如

使p 和 p2 成为同一对象的两个访问入口,并将对象“角色化”为共享实体。

(3)引用解除与垃圾回收,角色终结,如

说明:应用型变量对于操作对象的角色化,多数程序设计语言采用的操作符与原生型变量相同,即多数语言也是采用“=”进行操作。不过操作的内容并不相同。仍然使用术语assignment是因为assignment本身仅有“赋予”“分配”之义,并无“赋值”之义。目前国内仍以“赋值”相称是不准确的。

3.3 C++引用型变量及其对操作对象的角色化

在 C++ 中,引用(Reference)和对象(Object)的存储方式既有相似又有区别。

1)C++对象的存储

对象的存储位置取决于创建方式和作用域,可分为以下三类:

(1)栈存储:内存连续,效率高,用于存储局部变量、函数参数、临时对象。这些变量的栈帧(Stack Frame)中,按后进先出(LIFO)顺序由编译器自动分配和释放。

(2)堆存储:用于存储由程序员手动分配(new)和释放(delete)的动态创建的对象(如new int)和需跨作用域共享的对象;内存不连续,可能碎片化。堆区由操作系统管理,通过内存池分配,对象地址通常不连续。

(3)静态存储区(Static Storage):用于存储全局变量、静态变量(static修饰);程序启动时分配,生命周期贯穿整个程序。

2)C++引用的性质

(1)引用的本质是对象的别名,代码中表现为直接操作对象,无需显式解引用(如ref而非*ref)。

(2)C++采用隐式解引用(Implicit Dereference)机制,用户无需显式使用解引用操作符(如*或->),编译器会在底层自动将引用转换为实际对象。

(3)引用在创建时必须绑定到对象,且不可重新绑定。初期,需要先声明一个变量,然后通过变量间接绑定到对象。

例3  C++的别名性质示例

自C++11(2011年)起,C++引入了右值引用(Rvalue Reference)和 lambda 表达式,允许直接绑定到临时对象(右值),无需显式命名变量。

(4)引用经编译器优化后,本身一般不占内存。

(5)必须初始化:不能为nullptr:引用必须始终指向有效对象。Java引用型变量无此限制。

4. 符号型变量

符号型变量就是直接标识操作对象的角色名字。与原生型变量和引用型变量的最大不同是它们没有自己的存储空间,仅以将操作对象角色化为“天职”。资料显示,Lisp(1958)和Python(1989)是采用典型的符号型变量的两种程序设计语言。

4.1 Lisp的Symbol-Value映射机制

1958年问世的Lisp采用Symbol-Value机制对操作对象进行角色化。二者通过绑定操作形成映射关系,绑定分为全局绑定、局部绑定和特殊处理的闭包。

(1)全局绑定也称静态绑定,特点是直接修改符号的值单元。这种绑定关系程序运行期间持续存在,并存储在以哈希表方式实现的符号表中。符号表的基本结构如下。

符号名(Symbol)名:全局唯一的操作对象角色化名称,即变量名;

值单元(Value Cell):存储全局值(若存在)的引用;

函数单元(Function Cell):存储关联函数,如(setf (symbol-function 'double) #'(lambda (x) (* x 2));

属性列表(Plist):存储元数据(如 (setf (get 'x 'color) 'red))。

简而言之,符号表负责全局变量的管理,全局绑定的值对象通常在堆上分配。

(2)局部绑定也称词法绑定,通过独立的“环境(Environment)”实现。环境是一个动态链表结构,每个节点(环境帧)存储局部符号到值对象的映射,运行时局部绑定会遮蔽符号表中的全局值,但不修改符号表本身。这样,就形成环境与符号表协同工作管理局部变量的机制。当访问变量时,Lisp 按环境链从内向外查找,直到符号表。

(3)闭包的特殊处理:闭包捕获的变量存储在堆的独立环境中,与调用栈无关。

4.2 Python的对象赋名机制

1)Python对象

Python是一种“一切皆对象”的程序设计语言。它的“一切皆对象”的特征是把对象作为了程序主角,让对象拥有自己的存储资源——一个对象一经创建,就会被分配有自己的存储资源,同时在头部会有一个唯一的身份标识码ID(identity,在CPython中可认为是内存地址标记)、类型码、应用标识码和引用计数器[1]09-10,[2]12-16。

在Python中面向开发的存储资源是小对象池(Small Object Pool)和堆。小对象池用于以单值缓存频繁使用的小对象,避免值的重复存储,优化存储机制。按照对象类型可分为:

(1)小整数池(Small Integer Cache),默认缓存了 [-5, 256] 之间的整数对象)、

(2)短字符串驻留(String Interning),缓存变量名、函数名、类名、长度 ≤ 20的字符串。

(3)空元组缓存(Empty Tuple Singleton)和单例对象缓存。

除了这些小对象,Python其他对象(包括浮点数对象)都存在堆中。

Python 的栈由解释器管理,不向程序设计开放,无显式“栈分配”。。当函数嵌套调用时,每个函数都会在栈上创建自己的栈帧,形成调用栈(Call Stack),记录函数执行的上下文信息,如局部变量、返回地址、调用者信息、寄存器状态、异常处理信息等,以确保函数能正确地暂停、恢复和返回。

2)对象命名与与Python变量的创建

在Python中,对象是主角,变量是配角。变量是用“=”操作符按照assignment的语义创建的。语法格式为:名字 = 对象表达式。这种语句的作用是将一个名字与右侧的对象绑定,在当前名字空间中增加一个“名字:对象”键-值对,使名字就可以引用(代表)所绑定的对象了[1]18-22,[2]19-20。

例4 Python变量(名字)与对象绑定示例。

3)Python变量的作用[2]23-25

(1)角色化,增强程序可读性。

(2)引用对象。

(3)管理对象的可访问性。

(4)配合引用计数器和GC,管理对象生命周期,优化堆存储管理。

4.3  符号型变量不再有“变量赋值”

1)从不变性原则谈起

大千世界变幻莫测,并且变化是绝对的。但是,系统设计常追求不变性带来的稳定。1939 年,苏联学者 Г.B.谢巴诺夫在控制系统研究中提出:若系统的扰动可测,就有可能通过控制消除其对输出的影响,实现完全不变性或稳态(规范)不变性。1958 年,美国社会心理学家弗里茨· 海德(Fritz Heider,1896—1988)将变与不变的关系引入归因理论中,提出:若特定原因在许多情境下总是与某种结果相伴,且原因不存在时相应的结果也不出现,则可以把特定结果归因于那个特定的原因。人们将这一观点称为归因理论中的不变性原则(IAP)[3]13。

2)变量赋值:程序中的扰动根源

20 世纪 60 年代中期之后,软件危机推动不变性原则被引入计算机科学,其中一个内容就是关于“变量赋值”副作用的研究[4]13。人们发现变量赋值的核心是修改程序状态,使变量从一个确定状态跃迁到另一个状态,导致程序行为不再是纯粹的 “输入-输出” 映射,形成时间维度的状态依赖。在大型程序中,状态修改还会被链式传播,形成难以梳理的状态污染;在多线程环境中,可能引发竞态条件(Race Condition);在面向对象程序中,属性的赋值可能破坏对象封装性。这些都会大幅度地增加程序的复杂性、风险性。Harold Abelson等人在其《计算机程序的构造和解释》一书中写道:赋值使得一个变量从“一个简单的名字”变为了一个“值也是可以改变的”的“存储位置索引”,导致“事情复杂化了”[4]158-159,易引发牵一发而动全身的错误蔓延,[3],[2]26-27。

3)变量赋值副作用的应对[1]23,87,[3]14-15,[2]27,68

“变量赋值”作为程序的最大扰动因素,引起了程序设计界的高度重视。人们为应对它所形成的扰动,也采取了一些措施,主要措施有以下一些。

(1)用命名空间和作用于限制变量的作用范围。

(2)使用关键字标记不可变变量,如,C(还有 C++、PHP5、C#.net、HC08C)中的const,Java中用 final 等。

但是,这些均为治标之策。真正从根本上去除变量赋值的措施就是符号型变量。因为它无独立的存储空间——皮之不存,毛将焉附,也就无法赋值了。因此,在符号型变量程序设计语言中,assignment再译为 “赋名”或“绑定”更为严谨。

4.4  将操作对象的可变性分离:可变对象与不可变对象

符号型变量虽不可赋值,但它所绑定的对象还是可以被修改的。为了堵塞这一漏洞,一些高级程序设计语言中,引入了不可变对象(Immutable)机制——将对象分为可变(Mutable)与不可变两类:

1960S,Lisp 分离数据与操作:,将原子类型定义为不可变,将列表定义为可变;

1970s,Smalltalk显式区分对象可变性;

1972s,C将表达式分为左值(可赋值操作)和右值(不可赋值操作);

1990s,Java通过类设计体现不可变性;

1990s,Python定义动态类型下系统化实现可变/不可变类型;

2004s,Scala显式区分可变与不可变变量。

可变性分离是高级语言中的一种重要的设计思想,带来了多方面的优势。

1)提高存储效率

不可变对象在内存中有更好的复用性,例如,当多个角色使用同一个值(如多个学生共由一个老师指导)时,无需重复存储,且其中一个角色值修改(一个学生的指导老师变化)不会影响其他,这样就可以节省大量内存。而可变对象则需要更多的拷贝操作。

2)增强程序的稳定性与可靠性

(1)不可变对象创建后不可修改,配合符号型变量减少程序中的可变性因素,使开发者仅需关注可变对象,降低维护成本;

(2)保证状态一致性:多变量引用同一对象时,修改一个引用不会影响其他,避免错误传递;

(3)可作为哈希表键:因其哈希值不变,而可变对象若作为键,修改后可能导致哈希表查找失败;

(4)简化函数依赖分析:函数行为仅由参数决定,无需追踪外部状态变化,便于理解与测试。

3)支持多线程开发、函数式编程

(1)不可变对象在多线程环境中更安全,因为它们不会被修改,所以不需要加锁,减少了同步的开销。

(2)契合函数式编程 “无副作用” 特性:如 Lisp/Scala 中,列表通常不可变,修改操作返回新列表,确保函数行为确定性。

5. 变量概念的偏移与回归

从源头上说,程序中的变量就是用于表达计算逻辑的抽象符号,通过对操作对象的角色化命名,可以实现算法逻辑的泛化与复用,使程序能够以参数化的方式适配不同场景下的同类计算。这是变量最本质的概念。但是,由于变量实现机制的变化以及人们的理解角度等差异,使得变量概念随着程序设计语言的发展发生一定程度的偏移。

5.1 程序设计语言中的“可变化”因素与变量概念偏移

本来,变量的“变”指的是在求解一类问题的不同实例时,所使用的每个角色值可以不同。但是,这一不言而喻的共识概念,在程序设计语言中,往往被具体的语法规所掩盖,人们的注意力被放到了在程序执行过程中角色实例是否可以修改上,只是在给变量命名时,才被提示了一下:“见名知义”,但也没有说明为什么。这一现象在不同的编程模式中表现不同。

1)过程式编程模式中的默认“变”

过程式编程(Procedural Programming)把问题求解描述为:将问题的原始状态通过一系列操作逐步变为目标状态的过程。因此,它强调角色值的动态修改、动态更新。相应地,它的变量概念是“过程状态的记录者”。现在一些书中出现的“变量是存储可变化数据的容器”就是基于这一概念的描述。代表性的程序设计语言有:C、Pascal、Fortran、BASIC等,所使用的变量是原生型变量。因为原生型变量有自己的存储空间,其存储空间被角色化为变量名,便于用变量名直接映射到所存值,默认所有的值都是可变的,可以通过“赋值”操作直接修改;除非用const等声明为不可修改。正是这种语法掩盖了变量的本质概念。

2)面向对象编程模式中的可变性分层

面向对象编程(Object-Oriented Programming, OOP)的核心思想是“一切皆对象”。对象是属性和方法的封装体。代表性的程序设计语言是Java、C#等。它们的变量基本采用引用型,以提高存储分配的效率。引用型变量使得变量的概念变为“存储对象的内存地址(引用)”,从而将对象可变性分为如下两层:

(1)引用可变:可重新指向其他对象(如StringBuilder sb = new StringBuilder(); sb = null;)。

(2)对象状态可变:引用不变,但对象内部状态可修改(如sb.append("abc");)。

同时,可通过final关键字限制引用可变(如final StringBuilder sb = new StringBuilder();,sb不能指向其他对象,但可修改sb指向的对象)。

3)函数式编程模式的“无可变性”目标

函数式编程(Functional Programming,FP)是一种编程范式,其核心思想围绕 “将计算视为数学函数的求值” 展开,强调避免可变状态和副作用。简单地说,函数式编程是避免使用任何赋值操作的编程[4]157。因此,函数式语言中的变量是符号型变量,并且以默认变量为不可变变量(Immutable Variable) ——变量声明后不可修改(如 Scala 的val x = 5;,相当于 Java 的final int x = 5;)。这样,也就实现了变量概念的回归——变量是 “不可变的绑定”,是用于操作对象的角色符号——在程序运行中,所绑定的操作对象不可修改。因此,在函数式编程语言中没有“赋值”,只有“赋名”;没有额外的常量概念,因为默认操作对象本身就是不可变的。

函数式代表语言:Haskell、Scala(函数式风格)。在通常情况下,Python也被当作是一种支持函数式编程的语言。

5.2 术语assignment滥用引起的变量概念偏移

如前所述,在程序中使用变量,最重要的概念是它与所引用的操作对象之间的绑定关系。如表1所示,不同的编程语言中用于描述这一绑定操作的术语和符号有所不同。其中多数程序设计语言是使用了术语assignment,对应操作符是“=”。而这些语言中,应用最广、影响最大的是C和Java。

表1  不同编程语言中的绑定操作的术语和符号的简明对比

这些程序设计语言引入中国后,中国的计算机前辈们,将assignment翻译为“赋值”。这个翻译非常好,既准确地表达了这个操作的特征,又带有汉语特色。遗憾的是其涵盖面比较窄,只适合原生型变量,用于引用型变量也还讲究,却很不适合像Python这样的符号型变量。本来应当重新审视,给Python中的assignment翻译一个符合其操作特征的术语,但人们却简单地搬用了前辈们为过程式程序设计语言定制的“赋值”二字。到了2016年,这个术语便以讹传讹地出现在几乎所有Python著作中了。

到了2017年,这种术语使用不当的现象愈演愈烈,不仅中国的作者们这样写,甚至把外国的著作也这么翻译了。于是在清华大学出版社的支持下,作者写了一本书《Python大学教程》,试图先把Python中这个绑定操作与C和Java的不同解释清楚[5]20。后来,经过一番深思熟虑,又写了一本《新概念Python教程》为Python的assignment 操作造了一个与C 语言“赋值”相对应的术语——“赋名”[1]19-23。这个术语的创造,除了受“赋值”二字的启发外,还受PEP 572中提出的Named Expressions的启发[6]。但是,这样的努力,事与愿违。

有人辩称:那么多的语言都用assignment——过程式的、面向对象的、函数式的,我们当然也可以把“变量赋值”用于各种语言。这种说法实际上是没有厘清二者的区别:一个是变量与对象绑定在过程式语言与在函数式语言中语义不同;另一个是汉语“赋值”与英语assignment的词义分为不同:英语的assignment,要比汉语“赋值”宽泛得多。在这方面,日本计算机界的做法与我们不同,他们多是将assignment翻译为“代入”或直接使用其片假名アサインメント(或缩写アサイン)——与英语assignment的词义范围相当,不管哪种编程范式语言中使用都不会有错。因此,如果我国的前辈们当初在C和Java中将assignment翻译成了“赋予”“绑定”,那现在自然也就可以用于函数式编程了。不过,这样用的缺点是,虽然语义上可能不会有错误,但在区分不同语义时会显得够精细。可以想象,Python高层们之所以在发布运算符“:=”时,在已经有的“the walrus operator”和“Assignment Expressions”之外,又推出一个“Named Expressions” [6]。看来也有觉得通用assignment并没有充分反映Python的该操作特点之意。

还有人辩称,“只要会应用就行,不必强调概念”。持此说法者多是自己还没有真正理解而被追问时的一种搪塞。不过多数有责任心者,首先选择的是道歉,然后立即学习、纠,使正确的变量概念回归。

当然,正确的变量概念,也不是只要本质,应当是"本质优先、特色适配"。这样,既有语义的抽象性,又有实现的具体性,才是比较严谨的。

6. 结 语

在高级程序设计语言体系中,变量作为最基础的编程元素,其核心语义在于为操作实体赋予抽象命名标识。然而在程序设计语言演进过程中,变量的核心语义常被作用域规则、存储模型等具体实现机制所遮蔽,甚至以偏概全,用一种模式解释所有的模式。纠正这种偏向,不仅关系到当今程序设计教学和开发的质量,也影响到将来程序设计以及程序设计语言能不能顺利发展。更重要的是,这种不严谨之风,对于学风的侵害。这不是小事,希望有关部门也重视起来。

参考文献

[1] 张基温. 新概念Python教程[M]. 北京:清华大学出版社,2023.11.

[2] 张基温. Python新思维教程[M[. 北京:化学工业出版社, 2025.5.

[3] 张基温.不变性原则与Pythond的亮点[J].计算机教育,2024,第4期,12-16.

[4] Harold Abelson, Gerald Jay Sussman,Julie Sussman.,计算机程序的构造和解释[M].裘宗燕译. 北京:机械工业出版社,中信出版社,2017.

[5] 张基温. Python大学教程教程[M]. 北京:清华大学出版社,2018.9.

[6] PEP 572——Assignment Expressions [S]. https://www.python.org/dev/peps/pep-0572/ .