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

前天跟axx大聊起那个do..while(0)的宏的时候顺带聊到了别的一些语法结构的诡异地方。
觉得在C或者C-like语言里很麻烦的一个语法结构是for语句。比较常见的定义方式会是:
[code]ForStatement -> "for" "(" ForInitialize ";" ForCondition ";" ForIncrement ")" ForBody
;

ForInitialize -> VariableDeclarationList
| ExpressionList
;

ForCondition -> Expression
;

ForIncrement -> ExpressionList
;

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

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

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

[url=http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf]C99[/url]:ISO/IEC 9899:1999, 6.8.5.3
[quote]1 The statement
[code]for ( clause-1 ; expression-2 ; expression-3 ) statement[/code]
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.[/quote]
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。

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

[url=http://plumber.gnu-darwin.org/home/pub/Documents/isoiec14882-c%2B%2B-standard.pdf]C++98[/url]:ISO/IEC 14882:1998, 6.5.3
(这PDF复制不了……懒得打字,截图代替)
[img]http://rednaxelafx.iteye.com/upload/picture/pic/13787/f0b8ae41-6b7c-3b02-a03d-1d3f59be6108.gif[/img]
可以看到,C++98里对for语句头部第一部分的定义与C99的写法不一样——第一部分是一个语句,而那个分号是语句的一部分。
不过还得结合另外一部分的规定来看:
[quote][code]for-init-statement:
expression-statement
simple-declaration[/code][/quote]
结合这个来看,其实它与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也吻合。

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

[url=http://java.sun.com/docs/books/jls/third_edition/html/statements.html#14.14]Java[/url]:Java Language Specification, 3rd Edition
[quote][code]BasicForStatement:
for ( ForInitopt ; Expressionopt ; ForUpdateopt ) Statement

ForStatementNoShortIf:
for ( ForInitopt ; Expressionopt ; ForUpdateopt )
StatementNoShortIf

ForInit:
StatementExpressionList
LocalVariableDeclaration

ForUpdate:
StatementExpressionList

StatementExpressionList:
StatementExpression
StatementExpressionList , StatementExpression[/code][/quote]
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 错误
*/


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

[url=http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf]C#[/url]:ECMA-334 4th Edition, A.2.5
[quote][code]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[/code][/quote]
于是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的[url=http://devdoc.kikyou.info/tvp/docs/tjs2doc/contents/]TJS2[/url]
[quote]2.28,\kirikiri2\src\core\tjs2\syntax\tjs.y,第298行开始
[code]/* 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); }
;[/code][/quote]
语法上也属于“一般情况。看看运行时如何?

演示代码:
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语言在这里比较诡异。
[url=http://www.digitalmars.com/d/1.0/statement.html#ForStatement]D 1.0[/url]
[url=http://www.digitalmars.com/d/2.0/statement.html#ForStatement]D 2.0[/url]:
[quote][code]ForStatement:
for (Initialize Test; Increment) ScopeStatement

Initialize:
;
NoScopeNonEmptyStatement

Test:
empty
Expression

Increment:
empty
Expression[/code][/quote]

演示代码1:
testDScope.d:
[code]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
*/[/code]
OK,编译时出现错误。跟前面Java和C#的行为差不多。但是……

演示代码2:
testDScope.d:
[code]void main(char[][] args) {
int c = 0;
for ({int i = 0; c = 1;} i < 2; ++i) {
// ...do something
}
printf("%d", c); // 1
}[/code]
这段代码可以顺利通过编译(DMD 2.012),而且运行的结果与C/C++不一样……
诡异吧?

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

[url=http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf]ECMAScript[/url]:ECMA-262 3rd Edition, 12.6
[quote][code]for (ExpressionNoInopt; Expressionopt ; Expressionopt ) Statement
for ( var VariableDeclarationListNoIn; Expressionopt ; Expressionopt ) Statement[/code][/quote]
看上去语法与“一般情况”吻合。但这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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值