块 (编程)
在计算机编程中,块(block)或代码块是将源代码组织在一起的词法结构。块构成自一个或多个声明和语句。编程语言允许创建块,包括嵌入其他块之内的块,就叫做块结构编程语言。块和子程序是结构化编程的基础,结构化所强调的控制结构可以用块来形成的。
在编程中块的功能,是确使成组的语句被当作如同就是一个语句,限定在一个块中声明的对象如变量、过程和函数的词法作用域,使得它们不冲突于在其他地方用到的同名者。在块结构编程语言中,在块外部的对象名字在块内部是可见的,除非它们被声明了相同名字的对象所遮掩。
历史
块结构的想法是在1950年代开发最初的Autocode期间发展出来的,并形式化于ALGOL 60报告中。ALGOL 58介入了“复合”(compound)语句的概念,它只与控制流程有关[1]。在“ALGOL 60报告”中,介入了块和作用域的概念[2]。最终在“修订报告”中,复合语句被定义为:包围在语句括号begin
和end
之间的成序列的语句,形成一个复合语句。块被定义为:成序列的声明,跟随着成序列的语句,并被包围在begin
和end
之间,形成一个块;所有声明以这种方式出现在一个块中,并只在这个块中有效[3]。块与复合语句的主要差异是不能从块外跳转到块内的标签[4]。
语法
块在不同语言家族中使用不同的语法:
- ALGOL语言家族,ALGOL 60及其后继者比如Simula,使用关键字
begin
和end
来界定复合语句和块。ALGOL 68成为了面向表达式编程语言,偏好使用与begin
和end
等价的圆括号(
和)
[5]。 - Lisp语言家族,Lisp 1.5使用具有语法关键字
prog
的S-表达式表示块[9],而Maclisp和Scheme使用let
形式的S-表达式来表示块[10],S-表达式是圆括号(
和)
包围的前缀表示法。 - Smalltalk语言家族,Smalltalk-80和Self使用方括号
[
和]
来界定块。
此外,复合语句界定还可以采用:
建立控制结构,除了将所控制的语句序列,包围入复合语句或匿名块之外,还可以采用其他语法机制:
- 在ALGOL 68中,条件和迭代语句,使用块首保留字的反写保留字来终止,比如:
if ~ then ~ elif ~ then ~ else ~ fi
,case ~ in ~ out ~ esac
和for ~ while ~ do ~ od
。继承此风格的有:Dijkstra的守卫命令语言和Bourne的Bourne shell等。 - 一些结构化编程语言,如FORTRAN 77、Modula-2、Ada和Visual Basic等,对控制结构加结束关键字,比如:
IF ~ THEN ~ ELSIF ~ THEN ~ ELSE ~ END
。
限制
受ALGOL影响的一些语言支持块,但有着各自的限制:
基本语义
块的语义是双重的。首先,它向编程者提供了建立任意大和复杂的结构,并把它当作一个单元的一种途径。其次,它确使编程者能限制变量的作用域,有时可以限制已经被声明了的其他对象的作用域。
在早期语言比如FORTRAN和BASIC中,没有语句块或控制结构。直到1978年标准化FORTRAN 77之前,都没有“块状IF
”语句,要实现按条件选择,必须诉诸GOTO
语句。例如下述FORTRAN代码片段,从雇员工资中分别扣除超出正税阈值部分的税款,和超出附加税阈值部分的附加税款:
C 语言:ANSI标准FORTRAN 66C 初始化要计算的值 PAYSTX = .FALSE. PAYSST = .FALSE. TAX = 0.0 SUPTAX = 0.0C 如果雇员挣钱小于等于正税阈值则跃过税款扣除 IF (WAGES .LE. TAXTHR) GOTO 100 PAYSTX = .TRUE. TAX = (WAGES - TAXTHR) * BASCRTC 如果雇员挣钱小于等于附加税阈值则跃过附加税扣除 IF (WAGES .LE. SUPTHR) GOTO 100 PAYSST = .TRUE. SUPTAX = (WAGES - SUPTHR) * SUPRAT 100 TAXED = WAGES - TAX - SUPTAX
由于程序的逻辑结构不反映在语言中,分析出给定语句在何时执行可能会有困难。
块允许编程者把一组语句当作一个单元。例如,在与上述FORTRAN代码相对应的Pascal代码片段:
{ 语言:Jensen与Wirth版标准Pascal }if wages > tax_threshold thenbegin paystax := true; tax := (wages - tax_threshold) * tax_rate { 附加税处理代码不再嵌套在这里 }endelse begin paystax := false; tax := 0end;if wages > supertax_threshold thenbegin pays_supertax := true; supertax := (wages - supertax_threshold) * supertax_rateendelse begin pays_supertax := false; supertax := 0end;taxed := wages - tax - supertax;
与上述FORTRAN代码相比,上例中出现在初始化中的那些缺省值,通过复合语句即不带声明的块结构,被分别放置作出有关判断的地方。此外,处理附加税代码不再嵌入到处理正税代码之中,去除了附加税阈值要大于正税阈值,才能处理附加税的隐含条件。使用块结构,能明晰编程者的意图,使代码的结构更加密切反映出编程者的思考;再凭借某种风格的缩进增进可读性,可使代码更加容易理解和修改。
在早期语言中,在子例程中变量的作用域遍及整个子例程。假想在一个Fortran子例程中,完成了与管理者有关的任务,这里可能用到叫做IEMPNO
的一个整数变量,指示作为管理者的雇员的社会安全号码(SSN);后来在这个子例程的维护工作中,又增加与下属们有关的任务,此时编程者可能不经意间使用同名变量IEMPNO
,指示了作为这个管理者的下属的雇员的SSN,这就会导致一个难于跟踪的缺陷。
块结构使得编程者能够容易地将作用域控制到细微级别。例如完成有关雇员任务的Scheme代码片段:
;; 语言:R5RS标准Scheme(let ((empno (ssn-of employee-name))) (when (is-manager? empno) ;; when已列入R7RS-small标准 (let ((employee-list (underlings-of empno))) (display ;; format是SRFI-28和SRFI-48规定的字符串格式化过程 (format "~a has ~a employees working under him:~%" employee-name (length employee-list))) (for-each (lambda (empno) (display (format "Name: ~a, role: ~a~%" (name-of empno) (role-of empno)))) employee-list))))
这里在外层通过绑定宏let
将管理者的SSN绑定到了局部变量empno
,在其形成的块的作用域中列出管理者的雇员名字和他的下属数目;随后通过for-each
高阶函数,将他所有下属的SSN逐个绑定到匿名函数lambda
的形式参数empno
上,执行此匿名函数列出这个下属的名字和角色;这个形式参数的作用域是此匿名函数的主体,它与其外层的局部变量,标识符重名但不相互影响。在实践中,出于清晰性的考虑,编程者更可能选取明显不同的变量名字,但是即使名字选取存在重复,也难以在不经意间介入一个缺陷。在基于S-表达式的语言中,经常见到大量的嵌套圆括号,故而其代码必须采用良好的缩进。
提升
在一些语言中,变量可以声明为有函数作用域即使它位于函数的内嵌块之中。例如在JavaScript中,变量应当总是在使用之前被声明,它曾经允许赋值到未声明变量,会为此建立为未声明的全局变量,这在strict
模态下是个错误。以var
声明的变量有函数作用域,而非以let
或const
声明的变量可从属的块作用域。以var
声明的变量会被提升(hoist),这意味着可以在这个函数的作用域内任何地方提及这个变量,即使还未触及到它的声明,从而可以将var
声明视为被提举(lift)到它所在函数的顶部或全局作用域。但是如果在其声明之前访问了一个变量,这个变量的值总是未指定的。
参见
- 基本块
- 作用域
- 控制流程
- 悬摆else
- 闭包 (计算机科学)