KT.*_*KT. 27 javascript c puzzle semantics
如果变量的值x最初为0,则表达式x += x += 1将在C中计算为2,在Javascript中计算为1.
C的语义对我来说似乎很明显:x += x += 1被解释为x += (x += 1)反过来相当于
x += 1
x += x // where x is 1 at this point
Run Code Online (Sandbox Code Playgroud)
Javascript解释背后的逻辑是什么?什么规范强制执行这种行为?(顺便说一句,应该注意Java在这里与Javascript一致).
更新:
事实证明,x += x += 1根据C标准,表达式具有未定义的行为(感谢ouah,John Bode,DarkDust,Drew Dormann),这似乎破坏了一些读者的问题的全部要点.通过在其中插入标识函数,可以使表达式符合标准:x += id(x += 1).可以对Javascript代码进行相同的修改,问题仍然如所述.假设大多数读者能够理解"非标准兼容"制定背后的观点,我会保留它,因为它更简洁.
更新2:事实证明,根据C99,身份函数的引入可能无法解决模糊性.在这种情况下,亲爱的读者,请将原始问题视为与C++而不是C99有关,其中"+ ="现在可能最安全地被视为具有唯一定义的操作序列的可重载运算符.也就是说,x += x += 1现在相当于operator+=(x, operator+=(x, 1)).对于通向标准的漫长道路感到抱歉.
oua*_*uah 27
x += x += 1; 在C中是未定义的行为
表达式语句违反了序列点规则.
(C99,6.5p2)"在前一个和下一个序列点之间,一个对象的存储值最多只能通过表达式的评估来修改一次."
Mic*_*urr 15
JavaScript和Java对此表达式具有非常严格的从左到右的评估规则.C不会(即使在您提供的具有身份功能干预的版本中).
我有ECMAScript规范(第3版,我承认它已经很老了 - 目前的版本可以在这里找到:http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262. pdf)表示复合赋值运算符的计算如下:
11.13.2复合赋值(op =)
生产AssignmentExpression:LeftHandSideExpression @ = AssignmentExpression,其中@代表上面指出的一个运算符,评估如下:
- 评估LeftHandSideExpression.
- 调用GetValue(Result(1)).
- 评估AssignmentExpression.
- 调用GetValue(Result(3)).
- 将operator @应用于Result(2)和Result(4).
- 调用PutValue(结果(1),结果(5)).
- 返回结果(5)
您注意到Java具有与JavaScript相同的行为 - 我认为其规范更具可读性,因此我将在此处发布一些片段(http://java.sun.com/docs/books/jls/third_edition/html/expressions. html#15.7):
15.7评估订单
Java编程语言保证运算符的操作数似乎以特定的评估顺序进行评估,即从左到右.
建议代码不要严格依赖于此规范.当每个表达式最多包含一个副作用时,代码通常更清晰,作为其最外层的操作,并且当代码不依赖于由于从左到右的表达式评估而出现的确切异常时.
15.7.1首先评估左侧操作数在评估右侧操作数的任何部分之前,似乎完全评估了二元运算符的左侧操作数.例如,如果左侧操作数包含对变量的赋值而右侧操作数包含对该同一变量的引用,则引用生成的值将反映赋值首先发生的事实.
...
如果运算符是复合赋值运算符(第15.26.2节),则对左侧操作数的计算包括记住左侧操作数表示的变量并获取并保存该变量的值以用于隐含的组合操作.
另一方面,在提供中间身份函数的not-undefined-behavior示例中:
x += id(x += 1);
Run Code Online (Sandbox Code Playgroud)
虽然它不是未定义的行为(因为函数调用提供了一个序列点),但是x在函数调用之前或之后评估最左边的行为仍然是未指定的行为.因此,虽然它不是'任何事情'未定义的行为,但仍然允许C编译器x在调用id()函数之前评估这两个变量,在这种情况下,存储到变量的最终值将是1:
例如,如果x == 0要开始,评估可能如下所示:
tmp = x; // tmp == 0
x = tmp + id( x = tmp + 1)
// x == 1 at this point
Run Code Online (Sandbox Code Playgroud)
或者它可以这样评价:
tmp = id( x = x + 1); // tmp == 1, x == 1
x = x + tmp;
// x == 2 at this point
Run Code Online (Sandbox Code Playgroud)
请注意,未指定的行为与未定义的行为略有不同,但它仍然不是理想的行为.
在C中,x += x += 1是未定义的行为.
您不能指望任何一致的结果,因为尝试在序列点之间两次更新同一对象是未定义的.
这里有几个问题.
首先也是最重要的是C语言规范的这一部分:
6.5表达式
...
2在前一个和下一个序列点之间,一个对象的存储值最多只能通过表达式的计算修改一次.72)此外,先前值应只读以确定要存储的值.73)
...
72)浮点状态标志不是对象,可以在表达式中多次设置.
73)此段落呈现未定义的语句表达式,如i = ++i + 1; a[i++] = i;允许的同时i = i + 1; a[i] = i;
强调我的.
表达式x += 1修改x(副作用).表达式x += x += 1修改x两次没有插入序列点,并且它不是只读取先前值来确定要存储的新值; 因此,行为是未定义的(意味着任何结果都同样正确).现在,为什么地球会成为一个问题呢?毕竟,+=是正确联想的,一切都是从左到右评估的,对吧?
错误.
3语法指示运算符和操作数的分组.74)除如后指定(对于函数调用(),&&,||,?:,和逗号运营商),子表达式的求值的顺序,并且其中的副作用发生均为未指定的顺序.
...
74)语法指定运算符在表达式求值中的优先级,与本子条款的主要子条款的顺序相同,首先是最高优先级.因此,例如,允许作为二元+运算符(6.5.6)的操作数的表达式是6.5.1到6.5.6中定义的表达式.例外情况是强制转换表达式(6.5.4)作为一元运算符的操作数(6.5.3),以及下列任何运算符对之间包含的操作数:分组括号()(6.5.1),下标括号[](6.5.2.1),函数调用括号()(6.5.2.2)和条件运算符?:(6.5.15).
强调我的.
通常,优先级和关联性不会影响评估顺序或应用副作用的顺序.这是一个可能的评估顺序:
t0 = x + 1
t1 = x + t0
x = t1
x = t0
Run Code Online (Sandbox Code Playgroud)
哎呀.不是我们想要的.
现在,其他语言,如Java和C#(我假设是Javascript)确实指定操作数总是从左到右进行评估,因此始终有一个明确定义的评估顺序.