最近、この質問に出会いました: 割り当て演算子チェーンの理解 。
この質問に答えている間、私は加算代入演算子+=
または他のoperator=
(&=
、*=
、/=
など)の動作についての自分自身の理解を疑い始めました。
私の質問は、評価中に変更された値が式の他の場所に反映されるように、以下の式の変数a
がいつ更新されるのか、そしてその背後にあるロジックは何ですか?次の2つの式を見てください。
式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
として出力されます。
ただし、2番目の式では、a
の値が更新されるため、結果は6です。
いつa
の値が式の他の場所に反映されると仮定すべきですか?いつすべきではありませんか?
a += x
は本当にa = a + x
を意味することに注意してください。理解すべき重要な点は、加算が左から右に評価されることです-つまり、a + x
のa
はx
の前に評価されます。
それでは、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 + 1
は2
であるためb = (a = 1 + 2)
a
が2
になったためb = (a = 3)
1 + 2
は3
であるためb = 3
a
が3
になったためこれにより、上記の理由でa = 3
とb = 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
およびa
は4
になりましたb = 6
これにより、a = 4
とb = 6
が残ります。これは、Java/JavaScriptでa
とb
の両方を印刷することで確認できます(両方とも同じ動作をします)。
これらの式を解析ツリーと考えることも役立ちます。 a + (b + c)
を評価すると、RHS (b + c)
の前にLHS a
が評価されます。これはツリー構造でエンコードされます:
+
/ \
a +
/ \
b c
括弧はもうないことに注意してください-操作の順序はツリー構造にエンコードされます。ツリー内のノードを評価するとき、ノードの子を固定順序で処理します(つまり、+
の左から右へ) )。たとえば、ルートノード+
を処理するとき、右サブツリーが括弧で囲まれているかどうかに関係なく、右サブツリー(b + c)
の前に左サブツリーa
を評価します。解析ツリーにも括弧は存在しません)。
このため、Java/JavaScript donotは、最初に「最もネストされた括弧」を最初に評価します。これは、算術について教えられたルールとは対照的です。
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
*演算子の評価で、9ではなく6を生成することは許可されていません。
以下は、注意が必要なルールです
式の評価
式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)
Opsの順序のバリエーションを使用するだけです。
Opsの順序のリマインダーが必要な場合:
PEMDAS:
[〜#〜] p [〜#〜] =括弧
[〜#〜] e [〜#〜] =指数
[〜#〜] md [〜#〜] =乗算/除算
[〜#〜] as [〜#〜] =加算/減算
残りは左から右。
このバリエーションは左から右に読み取られますが、括弧が表示されている場合は、その中のすべてを実行し、定数に置き換えてから先に進みます。
最初の例:
var b = (a+=(a+=a))
var b = (1+=(1+=1))
var b = (1+=2)
_var b = 3
_
2番目の例:
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
_です。