2013-04-08 43 views
11

我最近碰到了一個討厭的錯誤,並簡化代碼如下所示:REF參數和分配在同一行

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

...

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

x的增量調用後的價值是1!一旦我發現發生了什麼,這是一個簡單的修復。我將返回值分配給一個臨時變量,然後更新x。我想知道什麼解釋了這個問題。是我在忽略的規範還是C#的某些方面?

+0

你到底在做什麼? 「這段代碼的輸出是1」是什麼意思? – 2013-04-08 14:28:34

+0

@Bartdude,在'+ ='後面x是1。 – 2013-04-08 14:29:04

+0

那麼你是不是想用x增加2?目標是什麼? – alan 2013-04-08 14:29:57

回答

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,當它被加載)和返回增量值(2)。然後運行addstloc.0,而無需進一步檢查x的值。

+0

請問您可以顯示IL代碼嗎? – MarcinJuraszek 2013-04-08 14:52:10

+0

這是有道理的。基本上它擴展到'x = x + Increment(x)',參考部分完全被忽略。 – Michael 2013-04-08 14:57:51

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將返回值添加到寄存器。這導致在使用調用Increment()之前的值x

相關部分從實際C#語言規範是這樣的:

形如x OP = Y的操作是通過將二元運算符重載分辨率處理(§7.3.4),爲如果操作書面x op y。然後,

如果所選運算符的返回類型可隱式轉換爲x的類型,則操作的求值爲x = x op y,但x只計算一次。

這意味着:

x += Increment(ref x); 

將被改寫爲:

x = x + Increment(ref x); 

因爲這將從左向右的x值會進行評估捕獲並使用,而不是通過調用Increment()更改的值。

1

C#的規範說約化合物運算符:(7.17.2)

操作被評價爲x = x op y,除了x只計算一次。

因此x被評估(爲0),然後由該方法的結果遞增。

1

它是由其他的答案暗示,我贊同從C++將此視爲「不好的事情」的建議,但「簡單」的解決方法是:

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

因爲C#確保了左至右表達式*的評估,這是你所期望的。

*引述部分「7.3運算符」 C#的規格的:

操作數中的表達從左至右進行評估。例如,在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

並且最後一行可以是「tr usted「與前面兩個表達式中使用的值相同。 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

但我仍然寧願不依靠它:-)