59

最近碰到一个问题:Assignment operator chain理解

在回答这个问题时,我开始怀疑自己对加法赋值运算符+=或任何其他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' 的值会反映在表达式的其他地方,什么时候不应该?

4

3 回答 3

87

记住那是a += x真的意思a = a + x。要理解的关键点是加法是从左到右计算的——也就是说,aina + x是在之前计算的x

所以让我们弄清楚是什么b = (a += (a += a))。首先我们使用规则a += xmean a = a + x,然后我们开始以正确的顺序仔细评估表达式:

  • b = (a = a + (a = a + a))因为a += x意味着a = a + x
  • b = (a = 1 + (a = a + a))因为a目前是1a请记住,我们在右项之前评估左项(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 = 4and b = 6。这可以通过在 Java/JavaScript 中打印出来来验证(两者在此处具有相同的行为)ab


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

   +
  / \
 a   +
    / \
   b   c

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

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

请参阅Java 语言规范

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。

于 2018-06-15T06:37:04.300 回答
7

以下是需要注意的规则

  • 运算符优先级
  • 变量赋值
  • 表达式评估

    表达式 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)
    
于 2018-06-15T06:39:27.577 回答
1

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

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

佩达斯:

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 += 1whichb = 3

于 2018-06-15T05:57:36.347 回答