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

Posted

技术标签:

【中文标题】加法赋值 += 表达式中的行为【英文标题】:Addition assignment += behavior in expression 【发布时间】:2018-11-24 22:58:57 【问题描述】:

最近遇到一个问题: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的值会反映在表达式的其他地方,什么时候不应该?

【问题讨论】:

现在尘埃落定 - 永远不要在生产中这样做! 这是 Java 还是 javascript 只是请不要像在生产环境中那样编写代码,这可能看起来很有趣,但是男孩调试会很痛苦 @pkpnd 一定是这样吗? Java、JavaScript 甚至 C++ 的规则并不能保证是相同的,即使人们期望它们是一样的。 简单地说,我的目的是让 Java/Javascript 的程序员阅读这个问题,因为这些语言中数学表达式的赋值操作没有太大区别。跨度> 【参考方案1】:

请记住,a += x 的真正含义是 a = a + x。要理解的关键点是加法是从左到右计算的——即a + x中的ax之前计算。

所以让我们弄清楚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 + 4a 现在是 4 b = 6

这给我们留下了a = 4b = 6。这可以通过在 Java/JavaScript 中打印出 ab 来验证(此处两者的行为相同)。


将这些表达式视为解析树也可能会有所帮助。当我们评估 a + (b + c) 时,LHS a 在 RHS (b + c) 之前被评估。这是在树结构中编码的:

   +
  / \
 a   +
    / \
   b   c

请注意,我们不再有任何括号 - 操作顺序被编码到树结构中。当我们评估树中的节点时,我们以固定顺序处理节点的子节点(即,+ 从左到右)。例如,当我们处理根节点+ 时,我们在右子树(b + c) 之前评估左子树a,而不管右子树是否包含在括号中(因为括号甚至不存在在解析树中)。

因此,Java/JavaScript总是首先评估“嵌套最多的括号”,这与您可能学过的算术规则相反。

见Java Language Specification:

15.7。评估顺序

Java 编程语言保证运算符的操作数看起来是按照特定的求值顺序进行求值的,即从左到右。 ...

15.7.1。首先计算左手操作数

二元运算符的左侧操作数似乎在右侧操作数的任何部分被评估之前已被完全评估。

如果运算符是复合赋值运算符(第 15.26.2 节),则左侧操作数的评估包括记住左侧操作数表示的变量以及获取和保存该变量的值以用于隐含二元运算。

更多类似你问题的例子可以在JLS的链接部分找到,例如:

示例 15.7.1-1。首先计算左手操作数

在下面的程序中,* 运算符有一个左操作数 包含对变量的赋值和右侧操作数 包含对同一变量的引用。所产生的价值 引用将反映分配首先发生的事实。

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

这个程序产生输出:

9

不允许对 * 运算符的求值产生 6 而不是 9。

【讨论】:

我不明白在存在更高优先级运算符()的情况下从左到右求值的原因,第一个表达式可以简单地写成b = a += a += a,应该是由于赋值运算符的右结合性,从右到左求值。 @11thdimension 这就是语言的工作原理。假设我们要评估a + (b + c)。首先评估 LHS a,无论 RHS 是否包含在括号中。然后评估 RHS (b + c)。这可能违反算术中的“规则”,我们首先评估“最里面的”括号。相反,将这些计算视为解析树会很有帮助,它会消除所有括号并按操作顺序烘焙到树结构中。 我不认为a + (b + c) 中的评估会从左到右开始,因为后缀符号是abc++,这意味着b+c 将首先评估。 @11thdimension 在后缀表示法中,a 的值首先被压入堆栈,然后是b 的值,然后是c。然后我们将bc 相加,最后将a 添加到该结果中。这与我的主张一致:a 在其值被推入堆栈时被“评估”。如果a 改为(x + y),则后缀符号为xy+bc++,并注意如何首先计算和x+y。语言与评估顺序一致,无论a 只是单个变量还是(x + y) 之类的表达式。 你是对的,现在答案似乎正在形成。缺少的部分是变量在被解析器处理后被推回堆栈后立即被评估。所有其他答案都将其概念化为从左到右处理的语言特定保证。其次,最里面的括号将首先被评估这是正确的,因为这个括号的运算符将首先出现在后缀中,但这并不会阻止解析器在解析后用它们的值替换之前处理的变量。【参考方案2】:

以下是需要注意的规则

Operator precedence 变量赋值

表达式评估

表达式 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)

表达式 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

表达式 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)

【讨论】:

1 += 2 或类似的东西真的没有意义,因为1 不是一个可以赋值的变量。 @pkpnd - 是的。不过,以上是为了便于理解。 嗯,对我来说,这让你的答案不太容易理解,因为1 += 2 的意思并不明显(因为那不是有效的代码)。 不应该先评估内括号吗?我会假设它使用的是堆栈,这意味着在存在未评估的大括号的情况下从左侧开始评估是没有意义的。 @11thdimension 是的,它是一个堆栈,但在(a + (b + c)) 的情况下,首先评估a,然后将(b + c) 推入堆栈,然后评估b,最后评估c。看我的回答,让这个过程更清晰【参考方案3】:

它只是使用了操作顺序的变体。

如果您需要提醒操作顺序:

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 在第一个表达式中,我将第一个括号集中的所有 a 值替换为 1。 @11thdimension 基本上,只要代码到达带有“a”的点,它就会用 a 的当前值替换它。在带括号的情况下,在到达下一个位置之前,a 会发生变化,因此当代码到达 a 时会有所不同。这有意义吗? @11thdimension 例如 b = a += (a +=a ) 它以 `b = 1 += (a += a)` 开头,这是它看到的第一个 a。然后它转到下一个b= 1 += (1+=a) 然后下一个b= 1+= (1+=1) 然后解决。 为什么一个表达式会以这种方式发生而不是另一个,这意味着为什么在第一个表达式中的所有大括号中,a 的值都替换为当前值,而在第二个表达式中它被延迟了?跨度>

以上是关于加法赋值 += 表达式中的行为的主要内容,如果未能解决你的问题,请参考以下文章

java中的基本运算符

java中的基本运算符

Java中的基本运算符

Expression 常用操作符,表达式

Expression 常用操作符,表达式

Expression 核心操作符表达式操作方法