为什么(x + = x + = 1)在C和Javascript中的评价方式不同?

时间:2012-01-23 22:52:03

标签: javascript c semantics puzzle

如果变量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

Javascript解释背后的逻辑是什么?什么规范强制执行这种行为? (顺便说一句,应该注意Java在这里与Javascript一致)。

更新 事实证明,x += x += 1表达式根据C标准具有未定义的行为(感谢ouahJohn BodeDarkDustDrew Dormann),这似乎破坏了一些读者的问题的全部要点。通过在其中插入标识函数,可以使表达式符合标准:x += id(x += 1)。可以对Javascript代码进行相同的修改,问题仍然如所述。假设大多数读者都能理解“非标准兼容”制定背后的观点,我会保留它,因为它更简洁。

更新2:事实证明,根据C99,身份功能的引入可能无法解决歧义。在这种情况下,亲爱的读者,请将原始问题视为与C ++而不是C99有关,其中“+ =”现在可能最安全地被视为具有唯一定义的操作序列的可重载运算符。也就是说,x += x += 1现在等同于operator+=(x, operator+=(x, 1))。对于通向标准的漫长道路感到抱歉。

6 个答案:

答案 0 :(得分:27)

x += x += 1;是C中未定义的行为。

表达式语句违反了序列点规则。

  

(C99,6.5p2)“在上一个和下一个序列点之间,对象的存储值最多只能通过表达式的一次修改一次。”

答案 1 :(得分: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,其中@表示指示的运算符之一   以上评估如下:

     
      
  1. 评估LeftHandSideExpression。
  2.   
  3. 调用GetValue(Result(1))。
  4.   
  5. 评估AssignmentExpression。
  6.   
  7. 调用GetValue(Result(3))。
  8.   
  9. 将operator @应用于Result(2)和Result(4)。
  10.   
  11. 调用PutValue(结果(1),结果(5))。
  12.   
  13. 返回结果(5)
  14.   

您注意到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节),那么   左手操作数的评估包括记住   左侧操作数表示并获取和保存的变量   该隐含组合操作中使用的变量值。

另一方面,在提供中间身份函数的非未定义行为示例中:

x += id(x += 1);

虽然它不是未定义的行为(因为函数调用提供了一个序列点),但是在函数调用之前或之后是否评估最左边的x仍然是未指定的行为。因此,虽然它不是'任何事情'未定义的行为,但仍然允许C编译器在调用x函数之前评估两个id()变量,在这种情况下,存储到变量的最终值将为{ {1}}:

例如,如果要启动1,评估可能如下所示:

x == 0

或者它可以这样评价:

tmp = x;    // tmp == 0
x = tmp  +  id( x = tmp + 1)
// x == 1 at this point

请注意,未指定的行为与未定义的行为略有不同,但它仍然不是理想行为。

答案 2 :(得分:9)

在C中,x += x += 1未定义的行为

您不能指望任何一致的结果,因为尝试在sequence points之间两次更新同一对象是未定义的。

答案 3 :(得分:5)

至少在C中,这是未定义的行为。表达式x += x+= 1;有两个sequence points:在表达式开始之前是隐式的(即:前一个序列点),然后是;。在这两个序列点之间x被修改两次,这明确表示为C99标准的未定义行为。编译器可以自由地做任何它喜欢的事情,包括让守护进程飞出你的鼻子。如果你很幸运,它只是做你期望的,但根本无法保证。

这与在C中未定义x = x++ + x++;的原因相同。有关此更多示例和解释,请参阅C-FAQ或StackOverflow C ++ FAQ条目Undefined Behavior and Sequence Points(AFAIK C ++规则)因为这与C)相同。

答案 4 :(得分:5)

这里有几个问题。

首先也是最重要的是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

糟糕。不是我们想要的。

现在,其他语言,如Java和C#(我假设是Javascript)指定操作数总是从左到右进行评估,因此总是有一个明确定义的评估顺序。

答案 5 :(得分:4)

从左到右评估所有JavaScript表达式。

......的相关性。

var x = 0;
x += x += 1

将......

var x = 0;
x = (x + (x = (x + 1)))

因此,由于从左到右的评估,x的当前值将在任何其他操作发生之前进行评估。

结果可以这样看......

var x = 0;
x = (0 + (x = (0 + 1)))

......明显等于1

因此...

   var x = 0;
   x = (x + (x = (x + 1)));
// x = (0 + (x = (0 + 1)));  // 1


   x = (x + (x = (x + 1)));
// x = (1 + (x = (1 + 1)));  // 3


   x = (x + (x = (x + 1)));
// x = (3 + (x = (3 + 1)));  // 7