「k + = c + = k + = c;」にインライン演算子の説明はありますか?


89

次の操作の結果の説明は何ですか?

k += c += k += c;

次のコードからの出力結果を理解しようとしました:

int k = 10;
int c = 30;
k += c += k += c;
//k=80 instead of 110
//c=70

現在、私は "k"の結果が80である理由を理解するのに苦労しています。なぜk = 40を割り当てても機能しないのですか(実際、Visual Studioはその値が他で使用されていないことを通知しています)。

なぜkは110ではなく80なのですか?

操作を次のように分割した場合:

k+=c;
c+=k;
k+=c;

結果はk = 110です。

私はCILを調べようとしましたが、生成されたCILの解釈はそれほど深くなく、いくつかの詳細を取得できません。

 // [11 13 - 11 24]
IL_0001: ldc.i4.s     10
IL_0003: stloc.0      // k

// [12 13 - 12 24]
IL_0004: ldc.i4.s     30
IL_0006: stloc.1      // c

// [13 13 - 13 30]
IL_0007: ldloc.0      // k expect to be 10
IL_0008: ldloc.1      // c
IL_0009: ldloc.0      // k why do we need the second load?
IL_000a: ldloc.1      // c
IL_000b: add          // I expect it to be 40
IL_000c: dup          // What for?
IL_000d: stloc.0      // k - expected to be 40
IL_000e: add
IL_000f: dup          // I presume the "magic" happens here
IL_0010: stloc.1      // c = 70
IL_0011: add
IL_0012: stloc.0      // k = 80??????

3
関数を分割したため、異なる結果が得られました。k+ = c + = k + = c = 80の理由は、kとcの値がすべての合計で同じであるため、k + = c + = k + = cが等しいためです。まで10 + 30 + 10 + 30
ジョアンパウロアモリ

78
興味深い演習ですが、実際には、同僚に嫌われたくない限り、このようなコードチェーンを作成しないでください。:)
UnhandledExcepSean

3
@AndriiKotliarov K + = C + = K + = C 10 + 30 + 10 + 30であるので、したがって、Kは、すべての値を受け取り、そしてCは、最後の3つの引数= 70 30 + 10 + 30を取得
ジョアン・パウロアモリン

6
また、読む価値-エリックリペットの答え私の違い++と++ iは何ですか?
Wai Ha Lee

34
「医者、医者、私がこれをやると痛い!」「それをしないでください。」
David Conrad

回答:


104

のような操作a op= b;はと同等a = a op b;です。割り当てはステートメントまたは式として使用できますが、式としては割り当てられた値を生成します。あなたの声明...

k += c += k += c;

...代入演算子は右結合なので、次のように書くこともできます

k += (c += (k += c));

または(拡張)

k =  k +  (c = c +  (k = k  + c));
     10       30       10  30   // operand evaluation order is from left to right
      |         |            
      |            40  10 + 30   // operator evaluation
         70  30 + 40
80  10 + 70

評価全体で、関連する変数の古い値が使用されます。これは特にの値に当てはまりますk(以下のILのレビューとWai Ha Lee のリンクを参照してください)。したがって、70 + 40(の新しい値k)= 110ではなく、70 + 10(の古い値k)= 80になります。

重要なのは、(C#仕様に従って)「式内のオペランドは左から右に評価される」ということです(オペランドは変数でcありk、この場合)。これは、この場合は右から左への実行順序を決定する演算子の優先順位と結合性には依存しません。(このページのEric Lippertの回答へのコメントを参照してください)。


次に、ILを見てみましょう。ILはスタックベースの仮想マシンを想定しています。つまり、ILはレジスターを使用しません。

IL_0007: ldloc.0      // k (is 10)
IL_0008: ldloc.1      // c (is 30)
IL_0009: ldloc.0      // k (is 10)
IL_000a: ldloc.1      // c (is 30)

スタックは次のようになります(左から右、スタックの上部が右)

10 30 10 30

IL_000b: add          // pops the 2 top (right) positions, adds them and pushes the sum back

10 30 40

IL_000c: dup

10 30 40 40

IL_000d: stloc.0      // k <-- 40

10 30 40

IL_000e: add

10 70

IL_000f: dup

10 70 70

IL_0010: stloc.1      // c <-- 70

10 70

IL_0011: add

80

IL_0012: stloc.0      // k <-- 80

なおIL_000c: dupIL_000d: stloc.0すなわちへの最初の割り当てk 、離れて最適化することができます。おそらくこれは、ILをマシンコードに変換するときに、ジッターによって変数に対して行われます。

また、計算に必要なすべての値は、割り当てが行われる前にスタックにプッシュされるか、これらの値から計算されることに注意してください。(によってstloc)割り当てられた値は、この評価中に再利用されることはありません。stlocスタックの一番上をポップします。


次のコンソールテストの出力は(Release最適化をオンにしたモード)

kの評価(10)
cの評​​価(30)
kの評価(10)
cの評​​価(30)
40をkに
割り当て70をcに
割り当て80をkに割り当て

private static int _k = 10;
public static int k
{
    get { Console.WriteLine($"evaluating k ({_k})"); return _k; }
    set { Console.WriteLine($"{value} assigned to k"); _k = value; }
}

private static int _c = 30;
public static int c
{
    get { Console.WriteLine($"evaluating c ({_c})"); return _c; }
    set { Console.WriteLine($"{value} assigned to c"); _c = value; }
}

public static void Test()
{
    k += c += k += c;
}

さらに完全にするために、式の数値を使用して最終結果を追加することができます。最終is k = 10 + (30 + (10 + 30)) = 80とそのc最終値は、最初の括弧であるに設定されc = 30 + (10 + 30) = 70ます。
フランク

2
実際k、ローカルの場合、最適化がオンであればデッドストアはほぼ確実に削除され、そうでない場合は保持されます。興味深い質問は、フィールド、プロパティ、配列スロットなどの場合、ジッターがデッドストアを回避できるかどうかkです。実際にはそうではないと思います。
Eric Lippert

実際に、リリースモードでのコンソールテストでkは、それがプロパティの場合、2回割り当てられることが示されています。
Olivier Jacot-Descombes

26

まず、ヘンクとオリビエの答えは正しいです。少し違う方法で説明したいと思います。具体的には、この点についてお話ししたいと思います。次のステートメントセットがあります。

int k = 10;
int c = 30;
k += c += k += c;

そして、次のステートメントのセットと同じ結果が得られるはずであると誤って結論付けます。

int k = 10;
int c = 30;
k += c;
c += k;
k += c;

どのようにしてそれを間違ったのか、そしてそれを正しく行う方法を知ることは有益です。それを分解する正しい方法はこのようなものです。

まず、最も外側の+ =を書き換えます

k = k + (c += k += c);

次に、最も外側の+を書き換えます。 x = y + zは常に「yを一時的に評価し、zを一時的に評価し、一時を合計し、合計をxに割り当てる」と同じでなければならないことに同意してください。だからそれを非常に明示的にしましょう:

int t1 = k;
int t2 = (c += k += c);
k = t1 + t2;

これがあなたが間違ったステップなので、それが明確であることを確認してください。複雑な操作をより単純な操作に分解する場合は、ゆっくりと慎重行う必要があり、手順をスキップしないください。手順をスキップすると、間違いが発生します。

OK、今度はt2への割り当てを、ゆっくりと慎重に分解します。

int t1 = k;
int t2 = (c = c + (k += c));
k = t1 + t2;

割り当ては、cに割り当てられているのと同じ値をt2に割り当てるので、次のようにしましょう。

int t1 = k;
int t2 = c + (k += c);
c = t2;
k = t1 + t2;

すごい。次に、2行目を分解します。

int t1 = k;
int t3 = c;
int t4 = (k += c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

すばらしい、私たちは進歩しています。t4への割り当てを分解します。

int t1 = k;
int t3 = c;
int t4 = (k = k + c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

次に3行目を分解します。

int t1 = k;
int t3 = c;
int t4 = k + c;
k = t4;
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

そして今、全体を見ることができます:

int k = 10;  // 10
int c = 30;  // 30
int t1 = k;  // 10
int t3 = c;  // 30
int t4 = k + c; // 40
k = t4;         // 40
int t2 = t3 + t4; // 70
c = t2;           // 70
k = t1 + t2;      // 80

完了したら、kは80、cは70です。

次に、これがILにどのように実装されているかを見てみましょう。

int t1 = k;
int t3 = c;  
  is implemented as
ldloc.0      // stack slot 1 is t1
ldloc.1      // stack slot 2 is t3

これは少しトリッキーです:

int t4 = k + c; 
k = t4;         
  is implemented as
ldloc.0      // load k
ldloc.1      // load c
add          // sum them to stack slot 3
dup          // t4 is stack slot 3, and is now equal to the sum
stloc.0      // k is now also equal to the sum

上記を次のように実装することもできます

ldloc.0      // load k
ldloc.1      // load c
add          // sum them
stloc.0      // k is now equal to the sum
ldloc.0      // t4 is now equal to k

"dup"トリックを使用しているのは、コードが短くなり、ジッターが軽減され、同じ結果が得られるためです。 一般に、C#コードジェネレーターは、一時的なものをできるだけ「スタック上」に保持しようとします。エフェメラルが少ないILを追跡する方が簡単である場合は、最適化をオフにすると、コードジェネレーターの攻撃性が低下します。

cを取得するために、同じトリックを実行する必要があります。

int t2 = t3 + t4; // 70
c = t2;           // 70
  is implemented as:
add          // t3 and t4 are the top of the stack.
dup          
stloc.1      // again, we do the dup trick to get the sum in 
             // both c and t2, which is stack slot 2.

そして最後に:

k = t1 + t2;
  is implemented as
add          // stack slots 1 and 2 are t1 and t2.
stloc.0      // Store the sum to k.

それ以外には合計が必要ないため、重複はしません。これでスタックは空になり、ステートメントの終わりです。

この話の教訓は、複雑なプログラムを理解しようとするときは、常に1つずつ操作を分解することです。近道をしないでください。彼らはあなたを迷わせます。


3
@ OlivierJacot-Descombes:仕様の関連する行はセクション「演算子」にあり、「式のオペランドは左から右に評価されます。たとえば、ではF(i) + G(i++) * H(i)メソッドFはiの古い値を使用して呼び出され、次にメソッドGは古い値のiで呼び出され、最後にメソッドHは新しい値のiで呼び出されます。これは、演算子の優先順位とは無関係であり、無関係です。(強調が追加されています。)「古い値が使用されている」ことが発生する場所はどこにもないと言ったとき、私は間違っていたと思います!例で発生します。しかし、規範的なビットは「左から右」です。
エリックリッペルト

1
これはミッシングリンクでした。本質は、オペランドの評価順序と演算子の優先順位を区別する必要があるということです。オペランドの評価は左から右に行われ、OPの場合、オペレーターの実行は右から左に行われます。
Olivier Jacot-Descombes

4
@ OlivierJacot-Descombes:その通りです。優先順位と結合性は、部分式の境界がどこにあるかを決定するという事実を除いて、部分式が評価される順序とは何の関係もありません。部分式は左から右に評価されます。
エリックリッパート

1
Ooopsは代入演算子をオーバーロードできないようです:/
ジョニー5

1
@ johnny5:その通りです。ただし、をオーバーロードすると+、except が1回だけ評価されると定義されている+=ため、無料で取得できます。組み込みかユーザー定義かに関係なく、これは当てはまります。だから:参照型のオーバーロードを試して、何が起こるかを見てください。x += yx = x + yx++
Eric Lippert

14

つまり、最初に+=適用されたのは元kの値ですか、それとも、より右側で計算された値ですか?

答えは、割り当ては右から左にバインドされますが、操作は依然として左から右に進むということです。

左端+=が実行中10 += 70です。


1
これは、それをナッツ殻にうまく入れます。
Aganju

実際には、左から右に評価されるオペランドです。
Olivier Jacot-Descombes

0

私はgccとpgccで例を試し、110を取得しました。それらが生成したIRを確認したところ、コンパイラはexprを次のように拡張しました。

k = 10;
c = 30;
k = c+k;
c = c+k;
k = c+k;

それは私には合理的に見えます。


-1

この種のチェーン割り当てでは、最も右側から値を割り当てる必要があります。あなたはそれを左側に割り当てて計算し、割り当てなければなりません、そしてこれを最後の(一番左の割り当て)まで進めてください、確かにそれはk = 80として計算されます。


他の多数の回答がすでに述べていることを単に再説明する回答を投稿しないでください。
Eric Lippert

-1

簡単な答え:varsを値に置き換えて、得た値:

int k = 10;
int c = 30;
k += c += k += c;
10 += 30 += 10 += 30
= 10 + 30 + 10 + 30
= 80 !!!

この答えは間違っています。この手法はこの特定のケースで機能しますが、そのアルゴリズムは一般的には機能しません。たとえば、k = 10; m = (k += k) + k;は意味しませんm = (10 + 10) + 10変化する式を持つ言語は、熱心な値の置換があるかのように分析できません。値の置換は、ミューテーションに関して特定の順序で行われ、ユーザーはそれを考慮する必要があります。
Eric Lippert

-1

これを数えることで解決できます。

a = k += c += k += c

2がありますcsおよび2 kのように

a = 2c + 2k

そして、言語の演算子の結果として、kまた等しく2c + 2k

これは、このスタイルのチェーンの変数の任意の組み合わせで機能します。

a = r += r += r += m += n += m

そう

a = 2m + n + 3r

そしてr、同じになります。

左端の割り当てまで計算するだけで、他の数値の値を計算できます。だから、m等しい2m + nn等しいですn + m

これは、それk += c += k += c;がとは異なりk += c; c += k; k += c;、そのために異なる回答が得られる理由を示しています。

コメントの一部の人々は、このショートカットから可能なすべてのタイプの追加に一般化しすぎるのではないかと心配しているようです。したがって、このショートカットはこの状況にのみ適用できることを明確にします。つまり、組み込みの数値型の加算割り当てを連鎖させます。()またはに他の演算子を追加したり+、関数を呼び出したり、をオーバーライド+=したり、基本的な数値タイプ以外のものを使用している場合は、(必ずしも)機能しません。質問の特定の状況を支援することのみを目的としています


これは質問の答えにはなりません
ジョニー5

@ johnny5それはあなたがあなたが得る結果を得る理由を説明します、すなわちそれが数学がどのように機能するかという理由です。
マットエレン

2
数学と、コンパイラがステートメントを評価する操作の順序は、2つの異なるものです。あなたの論理の下でk + = c; c + = k; k + = cは同じ結果に評価されるはずです。
ジョニー

いいえ、ジョニー5、それはそれが意味することではありません。数学的には、それらは異なるものです。3つの個別の演算は3c + 2kに評価されます。
マットエレン

2
残念ながら、「代数的」解は偶然にも正しいだけです。あなたのテクニックは一般的に機能しません。考えてみましょうx = 1;y = (x += x) + x;「3つのXのとyが等しくなるようにがあること、それあなたの競合ですか3 * x」?この場合yはと等しいためです4。今はどうy = x + (x += x);代数法則は「+ B = B +」満たされ、これも4であること?それはあなたの競合でありますか これは3であるため、残念ながら、式に副作用がある場合C#は高校代数の規則に従いません。C#は、副作用のある代数の規則に従います。
Eric Lippert
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.