Có giải thích cho các toán tử nội dòng trong “k + = c + = k + = c;” không?


89

Giải thích cho kết quả từ hoạt động sau đây là gì?

k += c += k += c;

Tôi đang cố gắng hiểu kết quả đầu ra từ mã sau:

int k = 10;
int c = 30;
k += c += k += c;
//k=80 instead of 110
//c=70

và hiện tại tôi đang đấu tranh với việc hiểu tại sao kết quả cho "k" là 80. Tại sao việc gán k = 40 không hoạt động (thực sự Visual Studio nói với tôi rằng giá trị đó không được sử dụng ở nơi khác)?

Tại sao lại là k 80 mà không phải 110?

Nếu tôi tách hoạt động thành:

k+=c;
c+=k;
k+=c;

kết quả là k = 110.

Tôi đang cố gắng xem qua CIL , nhưng tôi không hiểu sâu về CIL được tạo và không thể biết được một vài chi tiết:

 // [11 13 - 11 24]
IL_0001: ldc.i4.s     10
IL_0003: stloc.0      // k

// [12 13 - 12 24]
IL_0004: ldc.i4.s     30
IL_0006: stloc.1      // c

// [13 13 - 13 30]
IL_0007: ldloc.0      // k expect to be 10
IL_0008: ldloc.1      // c
IL_0009: ldloc.0      // k why do we need the second load?
IL_000a: ldloc.1      // c
IL_000b: add          // I expect it to be 40
IL_000c: dup          // What for?
IL_000d: stloc.0      // k - expected to be 40
IL_000e: add
IL_000f: dup          // I presume the "magic" happens here
IL_0010: stloc.1      // c = 70
IL_0011: add
IL_0012: stloc.0      // k = 80??????

3
Bạn nhận được kết quả khác vì bạn chia hàm, k + = c + = k + = c = 80 vì giá trị của k và c không đổi trong tất cả các tổng, do đó k + = c + = k + = c bằng nhau đến 10 + 30 + 10 + 30
João Paulo Amorim

78
Bài tập thú vị, nhưng trên thực tế, đừng bao giờ viết mã chuỗi như vậy trừ khi bạn muốn đồng nghiệp ghét mình. :)
UnhandledExcep Sean

3
@AndriiKotliarov vì k + = c + = k + = c là 10 + 30 + 10 + 30, do đó, K nhận tất cả các giá trị và C chỉ nhận 3 đối số cuối cùng là 30 + 10 + 30 = 70
João Paulo Amorim

6
Cũng đáng đọc - Eric Lippert của câu trả lời cho sự khác biệt giữa i ++ và ++ i là gì?
Wai Ha Lee

34
"Bác sĩ, bác sĩ, tôi rất đau khi làm thế này!" "Vì vậy, đừng LÀM điều đó."
David Conrad

Câu trả lời:


104

Một hoạt động như a op= b;tương đương với a = a op b;. Một phép gán có thể được sử dụng dưới dạng câu lệnh hoặc dưới dạng biểu thức, trong khi dưới dạng biểu thức, nó mang lại giá trị được gán. Tuyên bố của bạn ...

k += c += k += c;

... có thể, vì toán tử gán là liên kết phải, cũng có thể được viết là

k += (c += (k += c));

hoặc (mở rộng)

k =  k +  (c = c +  (k = k  + c));
     10       30       10  30   // operand evaluation order is from left to right
      |         |            
      |            40  10 + 30   // operator evaluation
         70  30 + 40
80  10 + 70

Trong quá trình đánh giá toàn bộ, các giá trị cũ của các biến liên quan được sử dụng. Điều này đặc biệt đúng với giá trị của k(xem đánh giá của tôi về IL bên dưới và liên kết Wai Ha Lee đã cung cấp). Do đó, bạn không nhận được 70 + 40 (giá trị mới của k) = 110, mà là 70 + 10 (giá trị cũ của k) = 80.

Vấn đề là (theo C # spec ) "Các toán hạng trong một biểu thức được đánh giá từ trái sang phải" (các toán hạng là các biến cktrong trường hợp của chúng ta). Điều này độc lập với sự ưu tiên của toán tử và tính liên kết, trong trường hợp này chỉ ra một thứ tự thực hiện từ phải sang trái. (Xem bình luận cho câu trả lời của Eric Lippert trên trang này).


Bây giờ chúng ta hãy nhìn vào IL. IL giả định một máy ảo dựa trên ngăn xếp, tức là nó không sử dụng các thanh ghi.

IL_0007: ldloc.0      // k (is 10)
IL_0008: ldloc.1      // c (is 30)
IL_0009: ldloc.0      // k (is 10)
IL_000a: ldloc.1      // c (is 30)

Ngăn xếp bây giờ trông như thế này (từ trái sang phải; trên cùng của ngăn xếp là bên phải)

10 30 10 30

IL_000b: add          // pops the 2 top (right) positions, adds them and pushes the sum back

10 30 40

IL_000c: dup

10 30 40 40

IL_000d: stloc.0      // k <-- 40

10 30 40

IL_000e: add

10 70

IL_000f: dup

10 70 70

IL_0010: stloc.1      // c <-- 70

10 70

IL_0011: add

80

IL_0012: stloc.0      // k <-- 80

Lưu ý rằng IL_000c: dup, IL_000d: stloc.0tức là nhiệm vụ đầu tiên k , có thể được tối ưu hóa. Có thể điều này được thực hiện cho các biến bởi jitter khi chuyển IL sang mã máy.

Cũng lưu ý rằng tất cả các giá trị được yêu cầu bởi phép tính hoặc được đẩy vào ngăn xếp trước khi thực hiện bất kỳ phép gán nào hoặc được tính toán từ các giá trị này. Các giá trị đã ấn định (bởi stloc) không bao giờ được sử dụng lại trong quá trình đánh giá này. stlocbật lên trên cùng của ngăn xếp.


Đầu ra của kiểm tra bảng điều khiển sau là ( Releasechế độ có bật tối ưu hóa)

đánh giá k (10)
đánh giá c (30)
đánh giá k (10)
đánh giá c (30)
40 được giao cho k
70 được giao cho c
80 được giao cho k

private static int _k = 10;
public static int k
{
    get { Console.WriteLine($"evaluating k ({_k})"); return _k; }
    set { Console.WriteLine($"{value} assigned to k"); _k = value; }
}

private static int _c = 30;
public static int c
{
    get { Console.WriteLine($"evaluating c ({_c})"); return _c; }
    set { Console.WriteLine($"{value} assigned to c"); _c = value; }
}

public static void Test()
{
    k += c += k += c;
}

Bạn có thể thêm kết quả cuối cùng với các số trong công thức để hoàn chỉnh hơn: cuối cùng là k = 10 + (30 + (10 + 30)) = 80cgiá trị cuối cùng được đặt trong dấu ngoặc đơn đầu tiên là c = 30 + (10 + 30) = 70.
Franck

2
Thật vậy, nếu klà cục bộ thì cửa hàng chết gần như chắc chắn bị xóa nếu bật tối ưu hóa và được giữ nguyên nếu không. Một câu hỏi thú vị là liệu jitter có được phép lướt qua cửa hàng chết nếu klà một trường, thuộc tính, vị trí mảng, v.v. hay không; trong thực tế, tôi tin rằng nó không.
Eric Lippert

Thử nghiệm bảng điều khiển ở chế độ Phát hành thực sự cho thấy điều đó kđược chỉ định hai lần nếu đó là một thuộc tính.
Olivier Jacot-Descombes

26

Trước hết, câu trả lời của Henk và Olivier là đúng; Tôi muốn giải thích nó theo một cách hơi khác. Cụ thể, tôi muốn giải quyết điểm này mà bạn đã thực hiện. Bạn có bộ câu lệnh sau:

int k = 10;
int c = 30;
k += c += k += c;

Và sau đó bạn kết luận sai rằng điều này sẽ cho kết quả giống như tập hợp các câu lệnh này:

int k = 10;
int c = 30;
k += c;
c += k;
k += c;

Đó là thông tin để xem bạn đã sai như thế nào và làm thế nào để làm điều đó đúng. Cách đúng để phá vỡ nó là như thế này.

Đầu tiên, hãy viết lại dấu + = ngoài cùng

k = k + (c += k += c);

Thứ hai, viết lại dấu + ngoài cùng. Tôi hy vọng bạn đồng ý rằng x = y + z phải luôn giống như "đánh giá y thành tạm thời, đánh giá z thành tạm thời, tính tổng các thời gian tạm thời, gán tổng cho x" . Vì vậy, hãy làm cho điều đó thật rõ ràng:

int t1 = k;
int t2 = (c += k += c);
k = t1 + t2;

Hãy chắc chắn rằng điều đó rõ ràng, bởi vì đây là bước bạn đã sai . Khi chia nhỏ các thao tác phức tạp thành thao tác đơn giản hơn, bạn phải đảm bảo rằng mình thực hiện từ từ, cẩn thậnkhông bỏ qua các bước . Bỏ qua các bước là nơi chúng ta mắc sai lầm.

OK, bây giờ hãy chia nhỏ bài tập sang t2, một lần nữa, chậm rãi và cẩn thận.

int t1 = k;
int t2 = (c = c + (k += c));
k = t1 + t2;

Phép gán sẽ gán cùng một giá trị cho t2 như được gán cho c, vì vậy giả sử rằng:

int t1 = k;
int t2 = c + (k += c);
c = t2;
k = t1 + t2;

Tuyệt quá. Bây giờ hãy chia nhỏ dòng thứ hai:

int t1 = k;
int t3 = c;
int t4 = (k += c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

Tuyệt vời, chúng tôi đang tiến bộ. Chia nhỏ nhiệm vụ sang t4:

int t1 = k;
int t3 = c;
int t4 = (k = k + c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

Bây giờ hãy chia nhỏ dòng thứ ba:

int t1 = k;
int t3 = c;
int t4 = k + c;
k = t4;
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

Và bây giờ chúng ta có thể xem xét toàn bộ:

int k = 10;  // 10
int c = 30;  // 30
int t1 = k;  // 10
int t3 = c;  // 30
int t4 = k + c; // 40
k = t4;         // 40
int t2 = t3 + t4; // 70
c = t2;           // 70
k = t1 + t2;      // 80

Vì vậy, khi chúng ta hoàn thành, k là 80 và c là 70.

Bây giờ chúng ta hãy xem cách điều này được thực hiện trong IL:

int t1 = k;
int t3 = c;  
  is implemented as
ldloc.0      // stack slot 1 is t1
ldloc.1      // stack slot 2 is t3

Bây giờ điều này là một chút khó khăn:

int t4 = k + c; 
k = t4;         
  is implemented as
ldloc.0      // load k
ldloc.1      // load c
add          // sum them to stack slot 3
dup          // t4 is stack slot 3, and is now equal to the sum
stloc.0      // k is now also equal to the sum

Chúng tôi có thể đã thực hiện những điều trên như

ldloc.0      // load k
ldloc.1      // load c
add          // sum them
stloc.0      // k is now equal to the sum
ldloc.0      // t4 is now equal to k

nhưng chúng tôi sử dụng thủ thuật "lặp lại" bởi vì nó làm cho mã ngắn hơn và làm cho nó dễ bị chập chờn hơn, và chúng tôi nhận được kết quả tương tự. Nói chung, trình tạo mã C # cố gắng giữ các khoảng thời gian tạm thời "phù du" trên ngăn xếp càng nhiều càng tốt. Nếu bạn thấy việc làm theo IL dễ dàng hơn với ít khoảng thời gian hơn, hãy tắt tối ưu hóa và trình tạo mã sẽ ít hoạt động hơn.

Bây giờ chúng ta phải làm cùng một thủ thuật để có được c:

int t2 = t3 + t4; // 70
c = t2;           // 70
  is implemented as:
add          // t3 and t4 are the top of the stack.
dup          
stloc.1      // again, we do the dup trick to get the sum in 
             // both c and t2, which is stack slot 2.

và cuối cùng:

k = t1 + t2;
  is implemented as
add          // stack slots 1 and 2 are t1 and t2.
stloc.0      // Store the sum to k.

Vì chúng tôi không cần tổng cho bất kỳ điều gì khác, chúng tôi không trùng lặp nó. Ngăn xếp hiện đang trống và chúng ta đang ở cuối câu lệnh.

Đạo lý của câu chuyện là: khi bạn đang cố gắng hiểu một chương trình phức tạp, hãy luôn chia nhỏ các thao tác tại một thời điểm . Đừng đi đường tắt; chúng sẽ khiến bạn lạc lối.


3
@ OlivierJacot-Descombes: Dòng liên quan của thông số nằm trong phần "Toán tử" và cho biết "Các toán hạng 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 pháp F được gọi bằng cách sử dụng giá trị cũ của i, sau đó là phương thức G được gọi với giá trị cũ của i và cuối cùng, phương thức H được gọi với giá trị mới của i . Điều này tách biệt và không liên quan đến ưu tiên toán tử. " (Nhấn mạnh thêm.) Vì vậy, tôi đoán tôi đã sai khi tôi nói rằng không có nơi nào xảy ra "giá trị cũ được sử dụng"! Nó xảy ra trong một ví dụ. Nhưng bit quy chuẩn là "trái sang phải".
Eric Lippert

1
Đây là liên kết bị thiếu. Điều cốt lõi là chúng ta phải phân biệt giữa thứ tự đánh giá toán hạng và ưu tiên toán tử . Đánh giá toán hạng đi từ trái sang phải và trong trường hợp của OP, toán tử thực hiện từ phải sang trái.
Olivier Jacot-Descombes

4
@ OlivierJacot-Descombes: Chính xác là như vậy. Mức độ ưu tiên và tính liên kết không liên quan đến thứ tự mà các biểu thức phụ được đánh giá, ngoài thực tế là mức độ ưu tiên và tính liên kết xác định vị trí của ranh giới biểu thức phụ . Subexpressions được đánh giá từ trái sang phải.
Eric Lippert

1
Rất tiếc, có vẻ như bạn không thể quá tải các toán tử gán: /
johnny 5

1
@ johnny5: Đúng vậy. Nhưng bạn có thể quá tải +, và sau đó bạn sẽ nhận được +=miễn phí vì x += yđược định nghĩa là x = x + yngoại trừ xđược đánh giá chỉ một lần. Điều đó đúng bất kể +nó được tích hợp sẵn hay do người dùng xác định. Vì vậy: hãy thử quá tải +trên một loại tham chiếu và xem điều gì sẽ xảy ra.
Eric Lippert

14

Nó tóm tắt là: giá trị đầu tiên +=được áp dụng cho kgiá trị gốc hay giá trị được tính nhiều hơn ở bên phải?

Câu trả lời là mặc dù các phép gán ràng buộc từ phải sang trái, các hoạt động vẫn tiến hành từ trái sang phải.

Vì vậy, ngoài cùng bên trái +=đang thực thi 10 += 70.


1
Điều này đặt nó độc đáo trong một vỏ hạt.
Aganju

Nó thực sự là các toán hạng được đánh giá từ trái sang phải.
Olivier Jacot-Descombes

0

Tôi đã thử ví dụ với gcc và pgcc và nhận được 110. Tôi đã kiểm tra IR mà chúng tạo ra và trình biên dịch đã mở rộng expr thành:

k = 10;
c = 30;
k = c+k;
c = c+k;
k = c+k;

mà có vẻ hợp lý với tôi.


-1

đối với loại phép gán chuỗi này, bạn phải gán các giá trị bắt đầu từ phía bên phải nhất. Bạn phải gán và tính toán và gán nó cho phía bên trái, và tiếp tục như vậy cho đến phần cuối cùng (phép toán ngoài cùng bên trái), Chắc chắn nó được tính là k = 80.


Vui lòng không đăng các câu trả lời chỉ trình bày lại những gì nhiều câu trả lời khác đã nêu.
Eric Lippert

-1

Câu trả lời đơn giản: Thay thế các vars bằng các giá trị mà bạn đã hiểu:

int k = 10;
int c = 30;
k += c += k += c;
10 += 30 += 10 += 30
= 10 + 30 + 10 + 30
= 80 !!!

Câu trả lời này là sai. Mặc dù kỹ thuật này hoạt động trong trường hợp cụ thể này, nhưng thuật toán đó không hoạt động nói chung. Ví dụ, k = 10; m = (k += k) + k;không có nghĩa là m = (10 + 10) + 10. Các ngôn ngữ có biểu thức thay đổi không thể được phân tích như thể chúng có sự thay thế giá trị mong muốn . Sự thay thế giá trị xảy ra theo một thứ tự cụ thể đối với các đột biến và bạn phải tính đến điều đó.
Eric Lippert

-1

Bạn có thể giải quyết điều này bằng cách đếm.

a = k += c += k += c

Có hai cs và hai ks như vậy

a = 2c + 2k

Và, như một hệ quả của các toán tử của ngôn ngữ, kcũng bằng2c + 2k

Điều này sẽ hoạt động đối với bất kỳ sự kết hợp nào của các biến trong kiểu chuỗi này:

a = r += r += r += m += n += m

Vì thế

a = 2m + n + 3r

rsẽ bằng nhau.

Bạn có thể tính ra các giá trị của các số khác bằng cách chỉ tính đến phần gán ngoài cùng bên trái của chúng. Vậy mbằng 2m + nnbằng n + m.

Điều này chứng tỏ điều đó k += c += k += c;là khác nhau k += c; c += k; k += c;và do đó tại sao bạn nhận được các câu trả lời khác nhau.

Một số người trong nhận xét có vẻ lo lắng rằng bạn có thể cố gắng tổng quát hóa quá mức từ phím tắt này thành tất cả các kiểu bổ sung có thể có. Vì vậy, tôi sẽ nói rõ rằng phím tắt này chỉ áp dụng cho trường hợp này, tức là xâu chuỗi các phép gán cộng với nhau cho các kiểu số được tạo sẵn. Nó không (nhất thiết) hoạt động nếu bạn thêm các toán tử khác vào, ví dụ: ()hoặc +, hoặc nếu bạn gọi các hàm hoặc nếu bạn đã ghi đè +=hoặc nếu bạn đang sử dụng thứ gì đó khác với các loại số cơ bản. Nó chỉ nhằm mục đích trợ giúp cho tình huống cụ thể trong câu hỏi .


Điều này không trả lời câu hỏi
johnny 5

@ johnny5 nó giải thích tại sao bạn nhận được kết quả bạn nhận được, tức là vì đó là cách hoạt động của toán học.
Matt Ellen

2
Toán học và thứ tự của các hoạt động mà một trình biên dịch tạo ra một câu lệnh là hai thứ khác nhau. Theo logic của bạn k + = c; c + = k; k + = c nên đánh giá cùng một kết quả.
johnny 5

Không, johnny 5, đó không phải là ý nghĩa của nó. Về mặt toán học, chúng là những thứ khác nhau. Ba hoạt động riêng biệt đánh giá đến 3c + 2k.
Matt Ellen

2
Thật không may, lời giải "đại số" của bạn chỉ tình cờ đúng. Kỹ thuật của bạn nói chung không hoạt động . Hãy xem xét x = 1;y = (x += x) + x;bạn có tranh cãi rằng "có ba x và y như vậy bằng 3 * x" không? Bởi vì ybằng 4trong trường hợp này. Bây giờ y = x + (x += x);bạn tranh luận về việc luật đại số "a + b = b + a" được hoàn thành và đây cũng là 4? Vì đây là 3. Thật không may, C # không tuân theo các quy tắc của đại số trung học nếu có các phản ứng phụ trong biểu thức . C # tuân theo các quy tắc của đại số có hiệu ứng phụ.
Eric Lippert
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.