11

最近厄介なバグに遭遇しました。簡略化されたコードは次のようになります。

int x = 0;
x += Increment(ref x);

...

private int Increment(ref int parameter) {
    parameter += 1;
    return 1;
}

Increment 呼び出し後の x の値は 1 です! 何が起こっているのかがわかれば、これは簡単な修正でした。戻り値を一時変数に代入し、x を更新しました。この問題を説明するものは何だろうと思っていました。私が見落としているのは、仕様またはC#の一部の側面の何かですか。

4

4 に答える 4

7

+= は左の引数を読み取り、次に右の引数を読み取るため、変数を読み取り、インクリメントするメソッドを実行し、結果を合計して、変数に代入します。この場合、0 を読み取り、変数を 1 に変更する副作用を伴う 1 を計算し、合計して 1 にし、変数に 1 を割り当てます。IL は、ロード、呼び出し、追加、ストアをこの順序で示しているため、これを確認します。

return を 2 に変更して結果が 2 であることを確認すると、メソッドの戻り値が「固執」する部分であることが確認されます。

誰かが尋ねたので、注釈付きの LINQPad を介した完全な IL を次に示します。

IL_0000:  ldc.i4.0
IL_0001:  stloc.0     // x
IL_0002:  ldloc.0     // x
IL_0003:  ldloca.s    00 // x
IL_0005:  call        UserQuery.Increment
IL_000A:  add
IL_000B:  stloc.0     // x
IL_000C:  ldloc.0     // x
IL_000D:  call        LINQPad.Extensions.Dump

Increment:
IL_0000:  ldarg.0
IL_0001:  dup
IL_0002:  ldind.i4
IL_0003:  ldc.i4.1
IL_0004:  add
IL_0005:  stind.i4
IL_0006:  ldc.i4.2
IL_0007:  ret

行 IL_000A で、スタックには x のロード (ロードされたときは 0) と Increment の戻り値 (2) が含まれていることに注意してください。その後、x の値をさらに検査することなく実行addされます。stloc.0

于 2013-04-08T14:36:49.263 に答える
1

それは他の回答によって暗示されています.C ++からの提案を支持して、これを「悪いこと」として扱いますが、「簡単な」修正は次のとおりです。

int x = 0;
x = Increment(ref x) + x;

C# では式の左から右への評価が保証されるため*、これは期待どおりの動作をします。

* C# 仕様のセクション「7.3 Operators」を引用:

式のオペランドは左から右に評価されます。たとえば、 では、の古い値を使用してF(i) + G(i++) * H(i)メソッドが呼び出され、次にの古い値でメソッドが呼び出され、最後にの新しい値でメソッドが呼び出されます。これは、演算子の優先順位とは別のものであり、無関係です。FiGiHi

最後の文はこれを意味することに注意してください。

int i=0, j=0;
Console.WriteLine(++j * (++j + ++j) != (++i + ++i) * ++i);
i = 0; j = 0;
Console.WriteLine($"{++j * (++j + ++j)} != {(++i + ++i) * ++i}");
i = 0; j = 0;
Console.WriteLine($"{++j} * ({++j} + {++j}) != ({++i} + {++i}) * {++i}");

これを出力します:


5 != 9
1 * (2 + 3) != (1 + 2) * 3

その最後の行は、前の 2 つの式で使用されたのと同じ値であると「信頼」できます。IE、加算は乗算の前に実行されますが、角かっこのため、オペランドはすでに評価されています。

これを次のように「リファクタリング」することに注意してください。

i = 0; j = 0;
Console.WriteLine(++j * TwoIncSum(ref j) !=  TwoIncSum(ref i) * ++i);
i = 0; j = 0;
Console.WriteLine($"{++j * TwoIncSum(ref j)} != { TwoIncSum(ref i) * ++i}");
i = 0; j = 0;
Console.WriteLine($"{++j} * {TwoIncSum(ref j)} != {TwoIncSum(ref i)} * {++i}");

private int TwoIncSum(ref int parameter)
{
    return ++parameter + ++parameter;
}

まったく同じように動作します:


5 != 9
1 * 5 != 3 * 3

しかし、私はまだそれに依存したくない:-)

于 2016-09-01T04:47:09.610 に答える