表达式中的加法赋值 += 行为

59

最近我看到了这个问题:Assignment operator chain understanding

回答这个问题的时候,我开始怀疑我对加法赋值运算符+=或任何其他operator=&=*=/=等)行为的理解。

我的问题是,在以下表达式中,变量a何时在原地更新,以便其更改的值在表达式在评估过程中反映在其他位置,并且背后的逻辑是什么? 请查看以下两个表达式:

表达式1

a = 1
b = (a += (a += a))
//b = 3 is the result, but if a were updated in place then it should've been 4

表达式2

a = 1
b = (a += a) + (a += a)
//b = 6 is the result, but if a is not updated in place then it should've been 4
在第一个表达式中,当最内层的表达式 (a += a) 被计算时,似乎它没有更新 a 的值,因此结果是 3 而不是 4
然而,在第二个表达式中,a 的值被更新了,因此结果为6。
我们应该在什么情况下假设 a 的值会反映在表达式的其他地方,而在什么情况下不应该呢?

19
既然尘埃已经落定 - 永远不要在生产中这样做! - Adelin
13
这是Java还是JavaScript? - muru
5
请不要编写像生产环境中的代码那样,这可能看起来很有趣,但是调试会非常麻烦。 - Eugene
4
@pkpnd 这一定是这样吗?尽管人们可能期望Java、JavaScript或者甚至C++的规则是相同的,但它们并不保证是相同的。 - Uyghur Lives Matter
2
只是为了澄清,我的意图是让Java/Javascript程序员都阅读这个问题,因为在这些语言中,数学表达式的赋值操作并没有太多区别。 - 11thdimension
显示剩余3条评论
3个回答

87
记住,a += x实际上意味着a = a + x。需要理解的关键点是加法从左到右进行计算--也就是说,在x之前会先计算a + x中的a
那么我们来看看b = (a += (a += a))的含义。首先我们使用规则a += x等同于a = a + x,然后按照正确的顺序仔细评估表达式。
  • b = (a = a + (a = a + a)),因为a += x意味着a = a + x
  • b = (a = 1 + (a = a + a)),因为a当前是1。记住我们在计算左边的项a之前计算右边的项(a = a + a)
  • b = (a = 1 + (a = 1 + a)),因为a仍然是1
  • b = (a = 1 + (a = 1 + 1)),因为a仍然是1
  • b = (a = 1 + (a = 2)),因为1 + 12
  • b = (a = 1 + 2),因为a现在是2
  • b = (a = 3),因为1 + 23
  • b = 3,因为a现在是3

这样我们就得到了a = 3b = 3,如上所述。

现在让我们用另一个表达式b = (a += a) + (a += a)来尝试一下:

  • b = (a = a + a) + (a = a + a)
  • b = (a = 1 + 1) + (a = a + a),请记住我们在评估右侧项之前评估左侧项
  • b = (a = 2) + (a = a + a)
  • b = 2 + (a = a + a),此时a为2。开始评估右侧项
  • b = 2 + (a = 2 + 2)
  • b = 2 + (a = 4)
  • b = 2 + 4,此时a4
  • b = 6

这样我们就得到了a = 4b = 6。可以通过在Java / JavaScript中打印出ab来验证这一点(两者在这里具有相同的行为)。


将这些表达式视为解析树可能有助于理解。当我们评估a + (b + c)时,左侧的a在右侧的(b + c)之前被评估。这在树结构中得到编码:

   +
  / \
 a   +
    / \
   b   c

请注意,我们不再有任何括号——运算顺序被编码到树结构中。当我们评估树中的节点时,按照固定的顺序(即对于+从左到右)处理节点的子级。例如,当我们处理根节点+时,我们在评估右子树(b + c)之前评估左子树a,无论右子树是否被括在括号中(因为括号甚至不存在于解析树中)。
因此,Java/JavaScript并不总是首先评估“最嵌套的括号”,与你可能在算术规则中学到的规则相反。
请参阅Java Language Specification
15.7.评估顺序 Java编程语言保证操作数按特定的评估顺序从左到右出现。
... 15.7.1.先评估左手操作数 二元运算符的左手操作数在任何右手操作数的部分被评估之前似乎已经完全被评估。 如果运算符是复合赋值运算符(§15.26.2),则左手操作数的评估包括记住左手操作数表示的变量并获取和保存该变量的值以用于隐含的二元操作。
类似于您的问题的更多示例可以在JLS的链接部分中找到。

Example 15.7.1-1. Left-Hand Operand Is Evaluated First

In the following program, the * operator has a left-hand operand that contains an assignment to a variable and a right-hand operand that contains a reference to the same variable. The value produced by the reference will reflect the fact that the assignment occurred first.

class Test1 {
    public static void main(String[] args) {
        int i = 2;
        int j = (i=3) * i;
        System.out.println(j);
    }
}

This program produces the output:

9

It is not permitted for evaluation of the * operator to produce 6 instead of 9.


1
我不理解在存在优先级更高的运算符“()”的情况下从左到右进行评估的原因。此外,第一个表达式可以简单地写为b = a += a += a,应该从右到左进行评估,因为赋值运算符是右结合的。 - 11thdimension
2
@11thdimension 这就是语言的工作方式。假设我们想要评估 a + (b + c)。无论 RHS 是否被括号括起来,LHS a 都会首先被评估。然后评估 RHS (b + c)。这可能违反了算术中我们先计算“最内层”括号的“规则”。相反,将这些计算视为解析树是有帮助的,它消除了所有括号,并将操作顺序嵌入到树结构中。 - k_ssb
我认为在 a + (b + c) 的计算中,评估不会从左到右开始,因为后缀表示法将是 abc++,这意味着 b+c 将首先被评估。 - 11thdimension
1
在后缀表示法中,首先将a的值“推入”堆栈,然后是b的值,接着是c的值。然后我们将bc相加,最后将a加到该结果中。这与我的说法一致:当将其值推入堆栈时,“评估”a。如果a(x+y),那么后缀表示法就是xy+bc++,请注意如何首先计算总和x+y。无论a是单个变量还是像(x+y)这样的表达式,语言都与评估顺序一致。 - k_ssb
你说得对,现在答案似乎已经浮出水面了。缺失的部分是变量在被解析器处理后立即推回堆栈时就会被评估。其他所有答案都将其概念化为从左到右处理的语言特定保证。其次,最内层的括号将首先被评估,这是正确的,因为这个括号的运算符将首先出现在后缀表达式中,但这并不会阻止解析器在解析之后用它们的值替换先前处理过的变量。 - 11thdimension
@11thdimension:在操作符的结合性和优先级以及求值顺序之间不要混淆是很重要的。例如,在a() + b()*c()中,您可以有评估顺序a→b→c;乘法在加法之前执行是无关紧要的。 - 6502

7
以下是需要注意的规则:
  • Operator precedence
  • Variable assignment
  • expression evaluation

    Expression 1

    a = 1
    b = (a += (a += a))
    
    b = (1 += (a += a))  // a = 1
    b = (1 += (1 += a))  // a = 1
    b = (1 += (1 += 1))  // a = 1
    b = (1 += (2))  // a = 2 (here assignment is -> a = 1 + 1)
    b = (3)  // a = 3 (here assignment is -> a = 1 + 2)
    

    Expression 2

    a = 1
    b = (a += a) + (a += a)
    
    b = (1 += a) + (a += a) // a = 1
    b = (1 += 1) + (a += a) // a = 1
    b = (2) + (a += a) // a = 2 (here assignment is -> a = 1 + 1)
    b = (2) + (2 += a) // a = 2 (here here a = 2)
    b = (2) + (2 += 2) // a = 2
    b = (2) + (4) // a = 4 (here assignment is -> a = 2 + 2)
    b = 6 // a = 4
    

    Expression 3

    a = 1
    b = a += a += a += a += a
    
    b = 1 += 1 += 1 += 1 += 1 // a = 1
    b = 1 += 1 += 1 += 2 // a = 2 (here assignment is -> a = 1 + 1)
    b = 1 += 1 += 3 // a = 3 (here assignment is -> a = 1 + 2)
    b = 1 += 4 // a = 4 (here assignment is -> a = 1 + 3)
    b = 5 // a = 5 (here assignment is -> a = 1 + 4)
    

2
1 += 2或类似的东西真的没有意义,因为1不是可以赋值的变量。 - k_ssb
@pkpnd - 是的。然而,上述内容是为了理解而写的。 - Nikhil Aggarwal
6
对我来说,这使得你的回答变得不太容易理解,因为1 += 2的意思并不明显(因为它不是有效的代码)。 - k_ssb
内部括号不应该首先被评估吗?我认为它使用了一个堆栈,这意味着在存在未评估的括号的情况下从左侧开始评估是没有意义的。 - 11thdimension
仍然不明白 b = (1 += (2)) // a = 2 怎么会变成 b = (3) // a = 3 -- 如果第一个表达式中没有任何 a,那么 a 的值怎么突然改变了呢?@NikhilAggarwal 如果你能解决这个困惑的来源,你的答案将更容易理解。 - k_ssb
显示剩余3条评论

1
它只是使用了一种运算顺序的变化。
如果您需要提醒运算顺序:
PEMDAS:
P = 括号
E = 指数
MD = 乘法/除法
AS = 加法/减法
其余从左到右。
这种变化只是从左到右阅读,但如果您看到括号,请执行其中的所有操作,然后用一个常数替换它并继续进行。
第一个例子:
var b = (a += (a += a))
var b = (1 += (1 += 1))
var b = (1 += 2)
var b = 3
第二个例子:
var b = (a += a) + (a += a)
var b = (1 += 1) + (a += a)
var b = 2 + (2 += 2)
var b = 2 + 4

var b = 6

var a = 1
var b = (a += (a += a))
console.log(b);

a = 1
b = (a += a) + (a += a)
console.log(b);

a = 1
b = a += a += a;
console.log(b);

最后一个例子 b = a += a += a,因为没有括号,它自动变成了 b = 1 += 1 += 1,即 b = 3


在第一个表达式中,您同时替换了所有出现的a的值为1,然而在第二个表达式中,它是逐个进行的,并且每次使用上一次计算的值。这就是问题所在,做这件事的规则是什么,谢谢。 - 11thdimension
@11thdimension 在第一个表达式中,我在第一个括号集中用1替换了所有a的值。 - Sheshank S.
1
基本上,每当代码到达带有“a”的点时,它就会用a的当前值替换它。在括号中的情况下,在到达下一个位置之前,a会发生变化,因此当代码到达a时,它是不同的。这有意义吗? - Sheshank S.
例如使用b = a += (a +=a ),它从它看到的第一个a开始:b = 1 += (a += a)。然后它转移到下一个a,也就是b= 1 += (1+=a)。接着转移到下一个a,即b= 1+= (1+=1),然后求解。 - Sheshank S.
这是个问题,抱歉我也撞墙想了好几个小时。让我解释一下,(a += (a += a))是一个嵌套的括号表达式 ( () ),我期望几乎所有编译器都会使用堆栈来解决它,也就是说内部括号将位于堆栈的顶部,因此将首先解决内部括号中的表达式。这意味着外部括号中的表达式必须等待内部表达式被评估后才能求值。在内部表达式评估完成之后,a的值应该是2,但是旧值用于求解外部表达式,问题是这里有什么特殊规则。 - 11thdimension
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接