前天跟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
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.
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