为什么 (x += x += 1) 在 C 和 Javascript 中的计算方式不同

Why does (x += x += 1) evaluate differently in C and Javascript?

本文关键字:计算 方式不 Javascript 为什么      更新时间:2023-09-26

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

更新:事实证明,根据 C 标准,表达式 x += x += 1 具有未定义的行为(感谢 ouah、John Bode、DarkDust、Drew Dormann(,这似乎破坏了某些读者的问题的全部意义。可以通过在其中插入标识函数来使表达式符合标准,如下所示:x += id(x += 1) .可以对Javascript代码进行相同的修改,问题仍然如前所述。假设大多数读者能够理解"不符合标准"的表述背后的要点,我将保留它,因为它更简洁。

更新2:事实证明,根据C99,引入恒等函数可能并没有解决歧义。在这种情况下,亲爱的读者,请将原始问题视为与C++有关,而不是 C99,其中 "+=" 现在很可能可以安全地被视为具有唯一定义的操作序列的可重载运算符。也就是说,x += x += 1现在相当于 operator+=(x, operator+=(x, 1)) .很抱歉,通往标准合规的道路漫长。

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

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

(C99,6.5p2("在前一个序列点和下一个序列点之间,对象应通过计算表达式最多修改一次其存储值。

JavaScript 和 Java 对这个表达式有非常严格的从左到右的求值规则。C 没有(即使在您提供的具有标识函数干预的版本中也是如此(。

我拥有的 ECMAScript 规范(第 3 版,我承认它很旧 - 当前版本可以在这里找到:http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf(说复合赋值运算符的计算方式如下:

11.13.2 复合赋值 ( op= (

生产赋值表达式:左边表达式@ = 赋值表达式,where@表示指示的运算符之一 以上,评估如下:

  1. 评估左侧表达式。
  2. 调用 GetValue(Result(1((。
  3. 计算赋值表达式。
  4. 调用 GetValue(Result(3((。
  5. 将运算符 @ 应用于结果 (2( 和结果 (4(。
  6. 调用 PutValue(Result(1(, Result(5((。
  7. 返回结果(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(,则 对左操作数的评估包括记住 左操作数表示的变量以及获取和保存 用于隐含组合操作的该变量的值。

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

x += id(x += 1);

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

例如,如果x == 0开始,评估可能如下所示:

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

或者它可以像这样评估它:

tmp = id( x = x + 1);   // tmp == 1, x == 1
x = x + tmp;
// x == 2 at this point
请注意,未

指定的行为与未定义的行为有细微的不同,但它仍然不是理想的行为。

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

您不能指望任何结果持续发生,因为尝试在序列点之间更新同一对象两次是未定义的。

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

这与 x = x++ + x++; 在 C 中未定义的原因相同。另请参阅 C-FAQ 以获取更多示例和解释,或 StackOverflow C++ FAQ 条目未定义的行为和序列点(AFAIK 对此C++规则与 C 相同(。

这里有几个问题在起作用。

首先也是最重要的一点是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(确实指定操作数总是从左到右计算,所以总是有一个明确定义的计算顺序。

所有 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