C# ref参数和赋值在同一行中

C# ref参数和赋值在同一行中,c#,.net,ref-parameters,C#,.net,Ref Parameters,我最近遇到了一个讨厌的bug,简化后的代码如下所示: int x = 0; x += Increment(ref x); 增量调用后的x值为1!一旦我发现发生了什么,这是一个简单的解决办法。我将返回值赋给一个临时变量,然后更新了x。我想知道是什么解释了这个问题。我忽略的是规范中的某些内容还是C#的某些方面。+=先读取左参数,然后读取右参数,因此它读取变量,执行递增的方法,对结果求和,并分配给变量。在这种情况下,它读取0,计算1,将变量更改为1,求和为1,并为变量赋值1。IL确认了这一点,因为

我最近遇到了一个讨厌的bug,简化后的代码如下所示:

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


增量调用后的x值为1!一旦我发现发生了什么,这是一个简单的解决办法。我将返回值赋给一个临时变量,然后更新了x。我想知道是什么解释了这个问题。我忽略的是规范中的某些内容还是C#的某些方面。

+=先读取左参数,然后读取右参数,因此它读取变量,执行递增的方法,对结果求和,并分配给变量。在这种情况下,它读取0,计算1,将变量更改为1,求和为1,并为变量赋值1。IL确认了这一点,因为它按顺序显示加载、调用、添加和存储

将返回值更改为2以查看结果为2,这将确认该方法的返回值是“粘滞”的部分

有人问过,下面是完整的IL via LINQPad及其注释:

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的值。

此:

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
将返回值添加到寄存器中。这将导致调用
Increment()
之前的
x

实际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()

所更改的值。C#spec介绍了复合运算符:(7.17.2)

该操作的计算结果为
x=x op y
,但x仅计算一次


<> p(x)是(0),然后由方法的结果递增。

< P>这是其他答案所暗示的,我赞同C++的建议,把它当作“坏事要做”,但是“简单”的修正是:

因为C#确保表达式*从左到右求值,所以这符合您的预期

*引用C规范第7.3节“操作员”:

表达式中的操作数从左到右求值。例如,在
F(i)+G(i++)*H(i)
中,使用
i
的旧值调用方法
F
,然后使用
i
的旧值调用方法
G
,最后,使用
i
的新值调用方法
H
。这与运算符优先级无关,也独立于运算符优先级

请注意,最后一句的意思是:

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

最后一行可以“信任”为前两个表达式中使用的相同值。也就是说,即使加法是在乘法之前执行的,但由于括号的原因,操作数已经计算过了

请注意,“重构”是为了:

仍然完全一样:

正确
5 != 9
1 * 5 != 3*3


但我还是不想依赖它:-)

你到底想做什么?你说的“这个代码的输出是1”是什么意思?伙计们,忘了目标吧。他已经处理好了。这里有意义的目标是,他试图理解为什么输出是这样的。这是一个很好的问题!要添加到观察值中,请替换
x+=增量(参考x)带有
x=增量(参考x)+x的行
x==2
!这可能是一只虫子吗?当我写入
x=x+增量(ref x)时我仍然得到<代码> x==1 < /代码>。嗯,我不认为它是一个bug,可能是一个特性,但对我来说太低了:-或者说,如果我自己做了,当我们访问变量两次i相同的语句时,我会认为它是潜在的问题,我会问自己,系统在做什么,按照什么顺序……在一条语句中修改同一个变量两次,这是一个非常糟糕的主意。在C++中,这是不确定的。基本上,它扩展到
x=x+Increment(x)
,而ref部分被完全忽略。
int x = 0;
x = Increment(ref x) + x;
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}");
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;
}