我最近遇到了一个令人讨厌的错误,简化的代码如下所示:
int x = 0;
x += Increment(ref x);
...
private int Increment(ref int parameter) {
parameter += 1;
return 1;
}
增量调用后的x值为1!一旦我发现发生了什么,这是一个简单的解决方案。我将返回值分配给临时变量,然后更新x。我想知道是什么解释了这个问题。是我在俯视的规范或C#的某些方面。
答案 0 :(得分:7)
+ =读取左参数然后读取右参数,因此它读取变量,执行递增的方法,对结果求和,并赋值给变量。在这种情况下,它读取0,计算1,副作用是将变量更改为1,总和为1,并为变量赋值1。 IL确认了这一点,因为它按顺序显示了加载,调用,添加和存储。
将返回值改为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)。然后它会运行add
和stloc.0
而无需进一步检查x的值。
答案 1 :(得分:6)
此:
static void Main()
{
int x = 0;
x += Increment(ref x);
Console.WriteLine(x);
}
获取编译为:
.method private hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 2
.locals init (
[0] int32 x)
L_0000: nop
L_0001: ldc.i4.0
L_0002: stloc.0
L_0003: ldloc.0
L_0004: ldloca.s x
L_0006: call int32 Demo.Program::Increment(int32&)
L_000b: add
L_000c: stloc.0
L_000d: ldloc.0
L_000e: call void [mscorlib]System.Console::WriteLine(int32)
L_0013: nop
L_0014: ret
}
编译器正在使用ldloca.s x
将当前值x
放入本地寄存器,然后调用Increment()
并使用add
将返回值添加到登记册。这会导致在使用x
的调用之前Increment()
的值。
实际C#语言规范的相关部分是:
通过应用二元运算符重载决策(第7.3.4节)来处理x op = y形式的运算,就像操作是用x op y编写的一样。然后,
如果所选运算符的返回类型可隐式转换为x的类型,则操作将计算为x = x op y,但x仅计算一次。
这意味着:
x += Increment(ref x);
将被改写为:
x = x + Increment(ref x);
由于这将从左到右进行评估,因此将捕获并使用x
的旧值,而不是通过调用Increment()
更改的值
答案 2 :(得分:1)
C#规范说复合算子:(7.17.2)
操作被评估为
x = x op y
,但x仅被评估一次。
因此评估x(为0),然后通过方法的结果递增。
答案 3 :(得分:1)
其他答案暗示了这一点,我赞同C ++的建议,将其视为"做坏事",但简单"""简单"修复是:
int x = 0;
x = Increment(ref x) + x;
因为C#确保了表达式*的从左到右的评估,所以这符合您的预期。
*报价部分" 7.3运营商" C#规范:
表达式中的操作数从左到右进行计算。例如,在
F(i) + G(i++) * H(i)
中,使用F
的旧值调用方法i
,然后使用旧值G
调用方法i
,并且最后,使用新值H
调用方法i
。这与运算符优先级分开,无关。
请注意,最后一句是指:
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
并且最后一行可以是"信任"与前两个表达式中使用的值相同。 I.E.即使在乘法之前执行加法,由于括号,操作数已经被评估过了。
请注意"重构"这个:
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
但我仍然宁愿不依赖它: - )