ref参数和赋值在同一行

Mic*_*ael 11 .net c# ref-parameters

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

int x = 0;
x += Increment(ref x);
Run Code Online (Sandbox Code Playgroud)

...

private int Increment(ref int parameter) {
    parameter += 1;
    return 1;
}
Run Code Online (Sandbox Code Playgroud)

增量调用后的x值为1!一旦我发现发生了什么,这是一个简单的解决方案.我将返回值分配给临时变量,然后更新x.我想知道是什么解释了这个问题.这是我在忽略的规范或C#的某些方面.

Jud*_*con 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
Run Code Online (Sandbox Code Playgroud)

请注意,在IL_000A行上,堆栈包含x的加载(加载时为0)和Increment的返回值(为2).然后运行add,stloc.0无需进一步检查x的值.


Mat*_*son 6

这个:

static void Main()
{
    int x = 0;
    x += Increment(ref x);
    Console.WriteLine(x);
}
Run Code Online (Sandbox Code Playgroud)

获取编译为:

.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 
}
Run Code Online (Sandbox Code Playgroud)

编译器ldloca.s x用于将当前值x放入本地寄存器,然后调用Increment()并使用add将返回值添加到寄存器.这导致在使用x调用之前的值Increment().

实际C#语言规范的相关部分是:

x op = y形式的操作通过应用二元运算符重载决策(第7.3.4节)来处理,就好像操作是用x op y编写的一样.然后,

如果所选运算符的返回类型可隐式转换为x的类型,则操作将计算为x = x op y,但x仅计算一次.

意思就是:

x += Increment(ref x);
Run Code Online (Sandbox Code Playgroud)

将被改写为:

x = x + Increment(ref x);
Run Code Online (Sandbox Code Playgroud)

由于这将从左到右进行评估,因此将捕获并使用值,x而不是通过调用更改的值Increment().