C# ref参数和赋值在同一行中
我最近遇到了一个讨厌的bug,简化后的代码如下所示: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确认了这一点,因为
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;
}