读书人

函数/方法的局部作用域与for的作用域有

发布时间: 2012-11-23 22:54:33 作者: rapoo

函数/方法的局部作用域与for的作用域问题
前天跟axx大聊起那个do..while(0)的宏的时候顺带聊到了别的一些语法结构的诡异地方。
觉得在C或者C-like语言里很麻烦的一个语法结构是for语句。比较常见的定义方式会是:

ForStatement -> "for" "(" ForInitialize ";" ForCondition ";" ForIncrement ")" ForBody              ;ForInitialize -> VariableDeclarationList               | ExpressionList               ;ForCondition -> Expression              ;ForIncrement -> ExpressionList              ;ForBody -> Statement         ;

也就是,一般来说for语句头部的括号里,第二部分是一个表达式,第三部分是一个表达式列表,而第一部分可能是一个变量声明列表或者一个表达式列表。按照局部作用域的规则,一般来说在这第一部分里声明的变量都是局部与for语句内的;如果与外部作用域已定义的变量重名,则可能:
1、不允许这样重定义(Java、C#、D等);
2、在for语句的局部作用域内创建一个新的局部变量,遮盖外部作用域原本的同名变量(C99/C++98);
3、不允许在for语句的头部定义新变量——所有局部变量都必须在局部作用域的一开头定义。(C99以前的C);
4、由于同一个局部作用域允许同一个名字的变量多次声明,所以实际上声明与不声明都没啥区别;for的头部里声明的变量与外部作用域的同名变量可以看成是“同一个”(ECMAScript 3)。

让我们看看C-like语言里具体是怎么定义的。关键要留意一下for头部的第一部分的规定。

------------------------------------------

C99:ISO/IEC 9899:1999, 6.8.5.3
for ( clause-1 ; expression-2 ; expression-3 ) statement
behaves as follows: The expression expression-2 is the controlling expression that is evaluated before each execution of the loop body. The expression expression-3 is evaluated as a void expression after each execution of the loop body. If clause-1 is a declaration, the scope of any variables it declares is the remainder of the declaration and the entire loop, including the other two expressions; it is reached in the order of execution before the first evaluation of the controlling expression. If clause-1 is an expression, it is evaluated as a void expression before the first evaluation of the controlling expression.134)
2 Both clause-1 and expression-3 can be omitted. An omitted expression-2 is replaced by a nonzero constant.
C99里的for语句与前面说的“一般情况”吻合。第一部分的子句可以是变量声明或者表达式,但不能是语句。

演示代码:
testCScope.c:
#include <stdio.h>int main( ) {    int c = 0;    for ( int i = 0; i < 2; ++i ) {        // ... do something    }    printf( "%d\n", c ); // 0}/*rednaxela@META-FX /d/experiment$ gcc -std=c99 testCScope.c -o testCScope.exe*/

用GCC 3.4.5编译出来的结果。跟预期一样,for里创建了一个新的局部变量c,遮蔽了main()里的c。

------------------------------------------

C++98:ISO/IEC 14882:1998, 6.5.3
(这PDF复制不了……懒得打字,截图代替)

可以看到,C++98里对for语句头部第一部分的定义与C99的写法不一样——第一部分是一个语句,而那个分号是语句的一部分。
不过还得结合另外一部分的规定来看:
for-init-statement: expression-statement simple-declaration
结合这个来看,其实它与C99的规定并没有多少区别。只是写法上的差异而已。

演示代码:
testCppScope.cpp:
#include <iostream>int main( ) {    int c = 0;    for ( int i = 0, c = 1; i < 2; ++i ) {        // ... do something    }    std::cout <<  c << std::endl; // 0}/*D:\experiment>cl testCppScope.cppMicrosoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.21022.08 for 80x86Copyright (C) Microsoft Corporation.  All rights reserved.testCppScope.cppC:\Program Files\Microsoft Visual Studio 9.0\VC\INCLUDE\xlocale(342) : warning C4530: C++ exception handler used, but unwind semantics are not enabled. Specify/EHscMicrosoft (R) Incremental Linker Version 9.00.21022.08Copyright (C) Microsoft Corporation.  All rights reserved./out:testCppScope.exetestCppScope.obj*/

用GCC 3.4.5和VC++2008编译都一样。运行结果是0,没问题,跟预期一样,与C99也吻合。

------------------------------------------

Java:Java Language Specification, 3rd Edition
BasicForStatement: for ( ForInitopt ; Expressionopt ; ForUpdateopt ) StatementForStatementNoShortIf: for ( ForInitopt ; Expressionopt ; ForUpdateopt ) StatementNoShortIfForInit: StatementExpressionList LocalVariableDeclarationForUpdate: StatementExpressionListStatementExpressionList: StatementExpression StatementExpressionList , StatementExpression
Java的语法也与“一般情况”吻合。但是它不允许在for的头部对方法的局部变量进行再次声明,所以下面的代码在编译时会出现错误。

演示代码:
testJavaScope.java:
public class testJavaScope {    public static void main( String[ ] args ) {        int c = 0;        for ( int i = 0, c = 1; i < 2; ++i ) { // error            // ... do something        }        System.out.println( c );    }}/*D:\experiment>javac testJavaScope.javatestJavaScope.java:4: 已在 main(java.lang.String[]) 中定义 c        for ( int i = 0, c = 1; i < 2; ++i ) {                         ^1 错误*/


------------------------------------------

C#:ECMA-334 4th Edition, A.2.5
for-statement: for ( for-initializeropt ; for-conditionopt ; for-iteratoropt ) embedded-statementfor-initializer: local-variable-declaration statement-expression-listfor-condition: boolean-expressionfor-iterator: statement-expression-liststatement-expression-list: statement-expression statement-expression-list , statement-expression
于是C#的for语句在语法上也跟C99、C++98、Java等相似,属于“一般情况”。

演示代码:
testCSharpScope.cs:
sealed class Test {    public static void Main( string[ ] args ) {        int c = 0;        for ( int i = 0, c = 1; i < 2; ++i ) { // error            // ... do something        }        System.Console.WriteLine( c );    }}/*D:\experiment>csc testCSharpScope.cs适用于 Microsoft(R) .NET Framework 3.5 版的 Microsoft(R) Visual C# 2008 编译器 3.5.21022.8 版版权所有 (C) Microsoft Corporation。保留所有权利。testCSharpScope.cs(4,26): error CS0136:       不能在此范围内声明名为“c”的局部变量,因为这样会使“c”具有不同的含义,       而它已在“父级或当前”范围中表示其他内容了*/

这段代码编译出错了。但是出错的原因与Java的版本并不完全相同,因为Java与C#的作用域规则并不完全一样。这里我们暂时不关心那个问题,至少在for语句头部的第一部分表现相似就是了。

------------------------------------------

吉里吉里2的TJS2
/* a for loop */for : "for" "(" for_first_clause ";" for_second_clause ";" for_third_clause ")" block_or_statement { cc->ExitForCode(); };/* the first clause of a for statement */for_first_clause : /* empty */ { cc->EnterForCode(false); } | { cc->EnterForCode(true); } variable_def_inner | expr { cc->EnterForCode(false); cc->CreateExprCode($1); };/* the second clause of a for statement */for_second_clause : /* empty */ { cc->CreateForExprCode(NULL); } | expr { cc->CreateForExprCode($1); };/* the third clause of a for statement */for_third_clause : /* empty */ { cc->SetForThirdExprCode(NULL); } | expr { cc->SetForThirdExprCode($1); };
语法上也属于“一般情况。看看运行时如何?

演示代码:
startup.tjs:
function foo() {    var c = 0;    for ( var i = 0, c = 1; i < 2; ++i ) {        // ... do something        // System.inform( c );    }    System.inform( c );}foo();

运行结果是c == 0。去掉中间的注释的话,可以看到for循环中c是1,没问题。
于是TJS2在这个地方的行为与C99/C++98更相似。

------------------------------------------

D语言在这里比较诡异。
D 1.0
D 2.0:
ForStatement:for (Initialize Test; Increment) ScopeStatementInitialize:;NoScopeNonEmptyStatementTest:emptyExpressionIncrement:emptyExpression

演示代码1:
testDScope.d:
void main(char[][] args) {    int c = 0;    for (int i = 0, c = 1; i < 2; ++i) { // error        // ...do something    }    printf("%d", c);}/*D:\experiment>dmd testDScope.dtestDScope.d(3): Error: shadowing declaration testDScope.main.c is deprecated*/

OK,编译时出现错误。跟前面Java和C#的行为差不多。但是……

演示代码2:
testDScope.d:
void main(char[][] args) {    int c = 0;    for ({int i = 0; c = 1;} i < 2; ++i) {        // ...do something    }    printf("%d", c); // 1}

这段代码可以顺利通过编译—MD 2.012),而且运行的结果与C/C++不一样……
诡异吧?

------------------------------------------

ECMAScript:ECMA-262 3rd Edition, 12.6
for (ExpressionNoInopt; Expressionopt ; Expressionopt ) Statementfor ( var VariableDeclarationListNoIn; Expressionopt ; Expressionopt ) Statement
看上去语法与“一般情况”吻合。但这ECMAScript实际上也不乖……

让我们用Rhino 1.7R1来测试一下:
Rhino 1.7 release 1 2008 03 06js> var c = 0js> for ( var i = 0, c = 1; i < 2; ++i ) { /* ... */ }js> c1js> i2

看到了吧,c的值变为1了。这跟ECMAScript对作用域的规定相关:同一个作用域内同一个名字的变量可以多次声明;多次声明的同名变量还是“同一个”;var关键字声明的变量拥有的是function scoping。所以……要是按照Java或者C#的习惯来写JavaScript代码,这里就危险了……
从JavaScript 1.7开始增加了let关键字,相应增加了let语句、let表达式和let声明。以let关键字而不是var关键字声明的变量的作用域就是局部于最小的语句块的,而不是函数的。但是for循环的初始化部分却无法用let关键字声明循环变量……

===========================================================================

真的是自己不写语言的语法都不觉得,真到要自己写语法的时候就会注意到很多这种诡异的地方 T T

读书人网 >Web前端

热点推荐