2013-04-08 22 views
11

tôi chạy vào một lỗi khó chịu thời gian gần đây và mã đơn giản trông giống như dưới đây:ref Parameter và Phân trong cùng một dòng

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

...

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

Giá trị của x sau khi cuộc gọi Tăng là 1! Đây là một sửa chữa dễ dàng một khi tôi phát hiện ra những gì đang xảy ra. Tôi đã gán giá trị trả về cho một biến tạm thời và sau đó được cập nhật x. Tôi đã tự hỏi điều gì giải thích vấn đề này. Có một cái gì đó trong spec hoặc một số khía cạnh của C# mà tôi nhìn.

+0

Bạn chính xác đang cố gắng làm gì? Và ý bạn là gì bởi "đầu ra của mã này là 1"? –

+0

@Bartdude, x là 1 sau '+ ='. –

+0

Vì vậy, bạn đang cố gắng tăng x x 2? Mục tiêu là gì? – alan

Trả lời

7

+ = đọc đối số bên trái rồi đối số bên phải, vì vậy nó đọc biến, thực hiện phương thức tăng, tổng kết quả và gán cho biến. Trong trường hợp này, nó đọc 0, tính 1 với tác dụng phụ của việc thay đổi biến thành 1, tổng thành 1 và gán 1 cho biến. IL xác nhận điều này, vì nó cho thấy tải, một cuộc gọi, thêm và một cửa hàng theo thứ tự đó.

Thay đổi trở lại thành 2 để xem kết quả là 2 xác nhận rằng giá trị trả về của phương thức là phần "gậy".

Kể từ khi có người hỏi, đây là toàn bộ IL qua LINQPad với các chú thích của nó:

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 

Lưu ý rằng trên dòng IL_000A, ngăn xếp chứa tải của x (mà là 0 khi nó đã được nạp) và sự trở lại giá trị gia tăng (là 2). Sau đó, nó chạy addstloc.0 mà không kiểm tra thêm giá trị của x.

+0

Bạn có thể hiển thị mã IL không? – MarcinJuraszek

+0

Điều đó có ý nghĩa. Về cơ bản nó mở rộng thành 'x = x + Increment (x)' và phần ref bị bỏ qua hoàn toàn. – Michael

6

này:

static void Main() 
{ 
    int x = 0; 
    x += Increment(ref x); 
    Console.WriteLine(x); 
} 

Gets biên soạn như sau:

.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 
} 

Trình biên dịch được sử dụng ldloca.s x để đưa giá trị hiện tại của x vào một thanh ghi địa phương, và sau đó nó gọi Increment() và sử dụng add để thêm giá trị trả về cho thanh ghi. Điều này dẫn đến giá trị của x từ trước cuộc gọi đến Increment() đang được sử dụng.

Phần liên quan từ spec ngôn ngữ C# thực tế là thế này:

Một hoạt động có dạng x op = y được xử lý bằng cách áp dụng hệ nhị phân có độ phân giải toán tử quá tải (§7.3.4) như thể cuộc phẫu thuật đã viết x op y. Sau đó,

Nếu kiểu trả về của toán tử đã chọn chuyển đổi hoàn toàn thành loại x, thao tác được đánh giá là x = x op y, ngoại trừ x được đánh giá chỉ một lần.

Có nghĩa là:

x += Increment(ref x); 

sẽ được viết lại như sau:

x = x + Increment(ref x); 

Vì đây sẽ được đánh giá từ trái sang phải, các giá trị cũ của x sẽ bị bắt và sử dụng thay cho giá trị bị thay đổi bởi cuộc gọi đến Increment().

1

C# spec nói về khai thác hợp chất: (7.17.2)

hoạt động được đánh giá là x = x op y, trừ x mà được đánh giá chỉ một lần.

Vì vậy x được đánh giá (bằng 0) và sau đó được tăng lên theo kết quả của phương pháp.

1

Nó ngụ ý của câu trả lời khác, và tôi ủng hộ đề nghị từ C++ để điều trị này là "một điều xấu để làm", nhưng "đơn giản" sửa chữa là:

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

Bởi vì C# đảm bảo trái sang phải đánh giá các biểu thức *, điều này làm những gì bạn mong đợi.

* phần Trích dẫn "7.3 Các nhà khai thác" của C# Spec:

Phép toán trong một biểu thức được đánh giá từ trái sang phải. Ví dụ, trong F(i) + G(i++) * H(i), phương thức F được gọi là sử dụng giá trị cũ i, sau đó phương thức G được gọi với giá trị cũ là i và cuối cùng, phương thức H được gọi với giá trị mới i. Điều này tách biệt và không liên quan đến quyền ưu tiên của nhà điều hành.

Lưu ý rằng câu cuối cùng nghĩa này:

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}"); 

đầu ra này:!

Đúng
5 = 9
1 * (2 + 3) = (1 + 2) * 3

và dòng cuối cùng có thể là "tr usted "là các giá trị giống như được sử dụng trong hai biểu thức trước. I E. mặc dù việc bổ sung được thực hiện trước phép nhân, vì các dấu ngoặc đơn, các toán hạng đã được đánh giá.

Lưu ý rằng "refactoring" này để:

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; 
} 

vẫn hoạt động hoàn toàn giống nhau:

Đúng
5 = 9
1 * 5 = 3 * 3

!

Nhưng tôi vẫn muốn không dựa vào nó :-)

Các vấn đề liên quan