当前位置: 代码迷 >> Web前端 >> 函数/方法的局部作用域与for的作用域有关问题
  详细解决方案

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

热度:334   发布时间:2012-11-23 22:54:33.0
函数/方法的局部作用域与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
引用
1 The statement
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.cpp
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.21022.08 for 80x86
Copyright (C) Microsoft Corporation.  All rights reserved.

testCppScope.cpp
C:\Program Files\Microsoft Visual Studio 9.0\VC\INCLUDE\xlocale(342) : warning C
4530: C++ exception handler used, but unwind semantics are not enabled. Specify
/EHsc
Microsoft (R) Incremental Linker Version 9.00.21022.08
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:testCppScope.exe
testCppScope.obj
*/

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

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

Java:Java Language Specification, 3rd Edition
引用
BasicForStatement:
        for ( ForInitopt ; Expressionopt ; ForUpdateopt ) Statement

ForStatementNoShortIf:
        for ( ForInitopt ; Expressionopt ; ForUpdateopt )
         StatementNoShortIf

ForInit:
        StatementExpressionList
        LocalVariableDeclaration

ForUpdate:
        StatementExpressionList

StatementExpressionList:
        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.java
testJavaScope.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-statement

for-initializer:
    local-variable-declaration
    statement-expression-list

for-condition:
    boolean-expression

for-iterator:
    statement-expression-list

statement-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
引用
2.28,\kirikiri2\src\core\tjs2\syntax\tjs.y,第298行开始
/* 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) ScopeStatement

Initialize:
	;
	NoScopeNonEmptyStatement

Test:
	empty
	Expression

Increment:
	empty
	Expression


演示代码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.d
testDScope.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
}

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

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

ECMAScript:ECMA-262 3rd Edition, 12.6
引用
for (ExpressionNoInopt; Expressionopt ; Expressionopt ) Statement
for ( var VariableDeclarationListNoIn; Expressionopt ; Expressionopt ) Statement

看上去语法与“一般情况”吻合。但这ECMAScript实际上也不乖……

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

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

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

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