1. ホーム
  2. c#

[解決済み] k += c += k += c;」のインライン演算子について教えてください。

2023-03-31 10:20:35

質問

次の操作の結果はどのように説明されますか?

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 は 80 であり、110 ではないのですか?

に分割して操作すると。

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??????

どのように解決するのですか?

以下のような操作で 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#によれば スペック ) 式中のオペランドは左から右へ評価されます。 (オペランドは変数 ck のようになる)。これは、この場合、右から左への実行順序を決定する演算子の優先順位や連想性とは無関係です。(Eric Lippertの 答え へのコメントを参照)。


さて、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: dup , IL_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;
}