มีคำอธิบายสำหรับตัวดำเนินการแบบอินไลน์ใน“ k + = c + = k + = c;” หรือไม่


89

คำอธิบายสำหรับผลลัพธ์จากการดำเนินการต่อไปนี้คืออะไร?

k += c += k += c;

ฉันพยายามทำความเข้าใจผลลัพธ์ผลลัพธ์จากรหัสต่อไปนี้:

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

และตอนนี้ฉันกำลังพยายามทำความเข้าใจว่าทำไมผลลัพธ์ของ "k" จึงเป็น 80 ทำไมการกำหนด k = 40 จึงไม่ทำงาน (จริงๆแล้ว Visual Studio บอกฉันว่าไม่มีการใช้ค่านั้นที่อื่น)

ทำไม k 80 ถึงไม่ใช่ 110?

ถ้าฉันแยกการดำเนินการเป็น:

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

ผลลัพธ์คือ k = 110

ฉันพยายามมองผ่านCILแต่ฉันไม่ค่อยลึกซึ้งในการตีความ CIL ที่สร้างขึ้นและไม่สามารถรับรายละเอียดบางอย่างได้:

 // [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
คุณได้ผลลัพธ์ที่แตกต่างกันเนื่องจากคุณแยกฟังก์ชัน k + = c + = k + = c = 80 เนื่องจากค่าของ k และ c ยังคงเหมือนกันในผลรวมทั้งหมดดังนั้น k + = c + = k + = c จึงเท่ากัน ถึง 10 + 30 + 10 + 30
João Paulo Amorim

78
แบบฝึกหัดที่น่าสนใจ แต่ในทางปฏิบัติอย่าเขียนโค้ดผูกมัดแบบนั้นเว้นแต่คุณต้องการให้เพื่อนร่วมงานเกลียดคุณ :)
UnhandledExcepSean

3
@AndriiKotliarov เพราะ k + = c + = k + = c คือ 10 + 30 + 10 + 30 ดังนั้น K จะได้รับค่าทั้งหมดและ C จะได้รับ 3 อาร์กิวเมนต์สุดท้ายเท่านั้น 30 + 10 + 30 = 70
João Paulo Amorim

6
นอกจากนี้มูลค่าการอ่าน - เอริค Lippert ของคำตอบเพื่ออะไรคือความแตกต่างระหว่างฉันและ ++ ++ ฉัน?
Wai Ha Lee

34
"หมอหมอเจ็บเมื่อฉันทำแบบนี้!" "อย่าทำอย่างนั้น"
David Conrad

คำตอบ:


104

การดำเนินการเช่น a op= b;a = a op b;เทียบเท่ากับ การกำหนดสามารถใช้เป็นคำสั่งหรือเป็นนิพจน์ในขณะที่นิพจน์จะให้ค่าที่กำหนด คำชี้แจงของคุณ ...

k += c += k += c;

... ได้เนื่องจากตัวดำเนินการมอบหมายมีความเชื่อมโยงอย่างถูกต้องจึงเขียนเป็น

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

หรือ (ขยาย)

k =  k +  (c = c +  (k = k  + c));
     10301030   // operand evaluation order is from left to right
      |         |        ↓    ↓
      |         ↓   4010 + 30   // operator evaluation7030 + 40
8010 + 70

โดยที่ในระหว่างการประเมินทั้งหมดจะใช้ค่าเก่าของตัวแปรที่เกี่ยวข้อง โดยเฉพาะอย่างยิ่งสำหรับมูลค่าของk(ดูบทวิจารณ์ IL ด้านล่างและลิงค์ Wai Ha Lee ให้มา) ดังนั้นคุณจะไม่ได้รับ 70 + 40 (ค่าใหม่ของk) = 110 แต่ 70 + 10 (ค่าเก่าของk) = 80

ประเด็นก็คือว่า (ตาม C # ข้อมูลจำเพาะ ) "ถูกดำเนินการในการแสดงออกได้รับการประเมินจากซ้ายไปขวา" (ตัวถูกดำเนินการที่มีตัวแปรcและkในกรณีของเรา) สิ่งนี้ไม่ขึ้นอยู่กับลำดับความสำคัญของตัวดำเนินการและการเชื่อมโยงซึ่งในกรณีนี้จะกำหนดลำดับการดำเนินการจากขวาไปซ้าย (ดูความคิดเห็นสำหรับคำตอบของ Eric Lippert ในหน้านี้)


ทีนี้มาดูที่ IL IL ถือว่าเครื่องเสมือนที่ใช้สแต็กกล่าวคือไม่ใช้รีจิสเตอร์

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)

ตอนนี้สแต็กมีลักษณะดังนี้ (จากซ้ายไปขวาด้านบนของสแต็กอยู่ทางขวา)

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

โปรดทราบว่าIL_000c: dup,IL_000d: stloc.0เช่นการกำหนดคนแรกที่k จะได้รับการปรับให้เหมาะสมออกไป สิ่งนี้อาจทำได้สำหรับตัวแปรโดย jitter เมื่อแปลง IL เป็นรหัสเครื่อง

โปรดทราบด้วยว่าค่าทั้งหมดที่จำเป็นในการคำนวณจะถูกพุชไปที่สแต็กก่อนที่จะมีการกำหนดหรือคำนวณจากค่าเหล่านี้ ค่าที่กำหนด (โดยstloc) จะไม่ถูกนำมาใช้ซ้ำในระหว่างการประเมินนี้ stlocปรากฏด้านบนของสแต็ก


ผลลัพธ์ของการทดสอบคอนโซลต่อไปนี้คือ (Releaseโหมดที่เปิดใช้การปรับให้เหมาะสม)

ประเมิน k (10)
ประเมิน c (30)
ประเมิน k (10)
ประเมิน c (30)
40 กำหนดให้ k
70 กำหนดให้ c
80 มอบหมายให้ 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;
}

คุณสามารถเพิ่มผลสุดท้ายกับตัวเลขในสูตรสำหรับแม้แต่สมบูรณ์มากขึ้น: สุดท้ายคือk = 10 + (30 + (10 + 30)) = 80และค่าสุดท้ายตั้งอยู่ในวงเล็บแรกซึ่งเป็นc c = 30 + (10 + 30) = 70
Franck

2
แน่นอนว่าถ้าkเป็นคนท้องถิ่นร้านค้าที่ตายแล้วจะถูกลบออกไปอย่างแน่นอนหากเปิดการเพิ่มประสิทธิภาพไว้และเก็บรักษาไว้หากไม่ คำถามที่น่าสนใจคือว่าตัวกระวนกระวายใจได้รับอนุญาตให้เข้าไปในร้านค้าที่ตายแล้วหรือไม่ถ้าkเป็นฟิลด์คุณสมบัติสล็อตอาร์เรย์และอื่น ๆ ในทางปฏิบัติฉันเชื่อว่ามันไม่
Eric Lippert

การทดสอบคอนโซลในโหมดรีลีสแสดงให้เห็นว่าkถูกกำหนดสองครั้งหากเป็นคุณสมบัติ
Olivier Jacot-Descombes

26

ก่อนอื่นคำตอบของ Henk และ Olivier นั้นถูกต้อง ฉันต้องการอธิบายด้วยวิธีที่แตกต่างออกไปเล็กน้อย โดยเฉพาะอย่างยิ่งฉันต้องการพูดถึงประเด็นนี้ที่คุณทำ คุณมีชุดคำสั่งนี้:

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

จากนั้นคุณก็สรุปไม่ถูกต้องว่าสิ่งนี้ควรให้ผลลัพธ์เช่นเดียวกับชุดคำสั่งนี้:

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

เป็นข้อมูลเพื่อดูว่าคุณทำผิดอย่างไรและทำอย่างไรให้ถูกต้อง วิธีที่ถูกที่จะทำลายมันลงเป็นเช่นนี้

ขั้นแรกเขียนด้านนอกสุดใหม่ + =

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

ประการที่สองเขียนเครื่องหมาย + ด้านนอกสุดใหม่ ฉันหวังว่าคุณยอมรับว่า x = Y + Z มักจะต้องเป็นเช่นเดียวกับ "ประเมิน Y เพื่อชั่วคราวประเมิน z เพื่อชั่วคราวสรุปชั่วคราวที่กำหนดจำนวนเงินที่จะ x" ลองทำให้ชัดเจนมาก:

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

ตรวจสอบให้แน่ใจว่าเป็นที่ชัดเจนเพราะนี้เป็นขั้นตอนที่คุณได้ผิด เมื่อหมดสภาพการดำเนินงานที่มีความซับซ้อนในการดำเนินงานที่เรียบง่ายที่คุณต้องให้แน่ใจว่าคุณทำเช่นนั้นอย่างช้าๆและระมัดระวังและไม่ข้ามขั้นตอน การข้ามขั้นตอนคือการที่เราทำผิดพลาด

ตกลงตอนนี้แบ่งงานเป็น t2 อีกครั้งอย่างช้าๆและระมัดระวัง

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

การกำหนดจะกำหนดค่าเดียวกันให้กับ t2 ตามที่กำหนดให้กับ c ดังนั้นสมมติว่า:

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

เยี่ยมมาก ตอนนี้แยกบรรทัดที่สอง:

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

เยี่ยมมากเรากำลังก้าวหน้า แบ่งงานเป็น t4:

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

ตอนนี้แบ่งบรรทัดที่สาม:

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

และตอนนี้เราสามารถดูสิ่งทั้งหมด:

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

เมื่อเราทำเสร็จแล้ว k คือ 80 และ c คือ 70

ตอนนี้เรามาดูวิธีการใช้งานใน IL:

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

ตอนนี้ค่อนข้างยุ่งยาก:

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

เราสามารถดำเนินการข้างต้นเป็น

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

แต่เราใช้เคล็ดลับ "dup" เพราะมันทำให้โค้ดสั้นลงและทำให้กระตุกง่ายขึ้นและเราก็ได้ผลลัพธ์เหมือนกัน โดยทั่วไปตัวสร้างรหัส C # จะพยายามรักษาจังหวะ "ชั่วคราว" บนสแต็กให้มากที่สุด หากคุณพบว่ามันง่ายที่จะปฏิบัติตาม IL กับ ephemerals น้อยกว่าการเพิ่มประสิทธิภาพเปิดปิดและรหัสเครื่องกำเนิดไฟฟ้าจะก้าวร้าวน้อย

ตอนนี้เราต้องทำเคล็ดลับเดียวกันเพื่อรับ 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.

และในที่สุดก็:

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

เนื่องจากเราไม่ต้องการผลรวมสำหรับสิ่งอื่นใดเราจึงไม่ทำซ้ำ ตอนนี้สแต็กว่างเปล่าและเราอยู่ที่ส่วนท้ายของคำสั่ง

คุณธรรมของเรื่องนี้คือ: เมื่อคุณพยายามทำความเข้าใจโปรแกรมที่ซับซ้อนให้แยกย่อยการดำเนินการทีละรายการเสมอ อย่าตัดสั้น พวกเขาจะทำให้คุณหลงทาง


3
@ OlivierJacot-Descombes: บรรทัดที่เกี่ยวข้องของสเปคที่อยู่ในส่วน "ผู้ประกอบการ" และกล่าวว่า "ตัวถูกดำเนินการในการแสดงออกได้รับการประเมินจากซ้ายไปขวาตัวอย่างเช่นใน. F(i) + G(i++) * H(i), วิธี F เรียกว่าใช้ค่าเดิมของฉันวิธีแล้ว G ถูกเรียกด้วยค่าเก่าของ i และสุดท้ายเมธอด H ถูกเรียกด้วยค่าใหม่ของ iซึ่งแยกจากและไม่เกี่ยวข้องกับลำดับความสำคัญของตัวดำเนินการ " (เน้นเสียงเพิ่มเติม) ดังนั้นฉันเดาว่าฉันคิดผิดเมื่อฉันพูดว่าไม่มีที่ไหนที่ "ใช้ค่าเก่า" เกิดขึ้น! มันเกิดขึ้นในตัวอย่าง แต่บิตเชิงบรรทัดฐานคือ "ซ้ายไปขวา"
Eric Lippert

1
นี่คือลิงค์ที่หายไป แก่นแท้คือเราต้องแยกความแตกต่างระหว่างลำดับการประเมินตัวถูกดำเนินการและลำดับความสำคัญของตัวดำเนินการเราต้องแยกความแตกต่างระหว่างการสั่งซื้อการประเมินผลการดำเนินและผู้ประกอบการมีความสำคัญการประเมินตัวดำเนินการไปจากซ้ายไปขวาและในกรณีของ OP การดำเนินการตัวดำเนินการจากขวาไปซ้าย
Olivier Jacot-Descombes

4
@ OlivierJacot-Descombes: ถูกต้อง ลำดับความสำคัญและการเชื่อมโยงไม่มีอะไรเกี่ยวข้องกับลำดับในการประเมินนิพจน์ย่อยนอกเหนือจากข้อเท็จจริงที่ว่าลำดับความสำคัญและการเชื่อมโยงเป็นตัวกำหนดว่าขอบเขตของนิพจน์ย่อยอยู่ที่ใด นิพจน์ย่อยจะถูกประเมินจากซ้ายไปขวา
Eric Lippert

1
อ๊ะดูเหมือนว่าคุณจะโอเวอร์โหลดตัวดำเนินการกำหนดไม่ได้: /
5

1
@ johnny5: ถูกต้อง แต่คุณสามารถโอเวอร์โหลดได้+จากนั้นคุณจะได้รับ+=ฟรีเนื่องจากx += yถูกกำหนดให้x = x + yยกเว้นxได้รับการประเมินเพียงครั้งเดียว นั่นเป็นความจริงไม่ว่า+จะสร้างขึ้นในหรือผู้ใช้กำหนดเอง ดังนั้น: ลอง+ใช้ประเภทการอ้างอิงมากเกินไปและดูว่าเกิดอะไรขึ้น
Eric Lippert

14

มันลดลงเป็น: ครั้งแรกที่+=นำไปใช้กับต้นฉบับkหรือกับค่าที่คำนวณทางขวามากขึ้น?

คำตอบก็คือแม้ว่างานจะผูกจากขวาไปซ้าย แต่การดำเนินการยังคงดำเนินจากซ้ายไปขวา

ดังนั้นซ้ายสุดกำลังดำเนินการ+=10 += 70


1
สิ่งนี้ทำให้อยู่ในเปลือกถั่วได้ดี
Aganju

จริงๆแล้วมันคือตัวถูกดำเนินการซึ่งประเมินจากซ้ายไปขวา
Olivier Jacot-Descombes

0

ฉันลองใช้ตัวอย่างด้วย gcc และ pgcc และได้ 110 ฉันตรวจสอบ IR ที่สร้างขึ้นและคอมไพเลอร์ได้ขยาย expr เป็น:

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

ซึ่งดูสมเหตุสมผลสำหรับฉัน


-1

สำหรับการกำหนดลูกโซ่ประเภทนี้คุณต้องกำหนดค่าจากการเริ่มจากด้านขวาสุด คุณต้องกำหนดและคำนวณและกำหนดให้ทางด้านซ้ายและดำเนินการต่อไปจนถึงขั้นสุดท้าย (การมอบหมายซ้ายสุด) แน่นอนว่าคำนวณเป็น k = 80


โปรดอย่าโพสต์คำตอบที่ระบุคำตอบอื่น ๆ อีกมากมายที่ระบุไว้แล้ว
Eric Lippert

-1

คำตอบง่ายๆ: แทนที่ vars ด้วยค่าที่คุณไม่ได้รับ:

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

คำตอบนี้ผิด แม้ว่าเทคนิคนี้จะใช้ได้ผลในกรณีนี้ แต่อัลกอริทึมนั้นก็ใช้ไม่ได้โดยทั่วไป ยกตัวอย่างเช่นไม่ได้หมายความว่าk = 10; m = (k += k) + k; ไม่สามารถวิเคราะห์ภาษาที่มีนิพจน์กลายพันธุ์ได้ราวกับว่ามีการทดแทนค่าที่ต้องการ การทดแทนมูลค่าเกิดขึ้นตามลำดับเฉพาะที่เกี่ยวข้องกับการกลายพันธุ์และคุณต้องคำนึงถึงสิ่งนั้นด้วย m = (10 + 10) + 10
Eric Lippert

-1

คุณสามารถแก้ปัญหานี้ได้โดยการนับ

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

มีสองcวินาทีและสองkวินาทีดังนั้น

a = 2c + 2k

และเป็นผลมาจากตัวดำเนินการของภาษา kก็เท่ากับ2c + 2k

สิ่งนี้จะใช้ได้กับการรวมกันของตัวแปรในรูปแบบของห่วงโซ่:

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

ดังนั้น

a = 2m + n + 3r

และ rจะเท่ากัน.

คุณสามารถคำนวณค่าของตัวเลขอื่น ๆ ได้โดยคำนวณเฉพาะการกำหนดทางซ้ายสุดเท่านั้น ดังนั้นmเท่ากับ2m + nและnเท่าเทียมกันn + mเท่าเทียมกัน

นี่แสดงให้เห็นว่า k += c += k += c;แตกต่างกันk += c; c += k; k += c;และด้วยเหตุนี้คุณจึงได้รับคำตอบที่แตกต่างกัน

คนบางคนในความคิดเห็นดูเหมือนจะกังวลว่าคุณอาจพยายามสรุปจากทางลัดนี้ไปสู่การเพิ่มทุกประเภทที่เป็นไปได้ ดังนั้นฉันจะพูดให้ชัดเจนว่าทางลัดนี้ใช้ได้กับสถานการณ์นี้เท่านั้นนั่นคือการผูกโยงการกำหนดเพิ่มเติมเข้าด้วยกันสำหรับประเภทตัวเลขในตัว มันใช้ไม่ได้ (จำเป็น) ถ้าคุณเพิ่มตัวดำเนินการอื่น ๆ ในเช่น()หรือ+หรือถ้าคุณเรียกใช้ฟังก์ชันหรือถ้าคุณทับ+=หรือถ้าคุณใช้อย่างอื่นที่ไม่ใช่ประเภทตัวเลขพื้นฐาน มีไว้เพื่อช่วยในสถานการณ์เฉพาะในคำถามเท่านั้น


สิ่งนี้ไม่ตอบคำถาม
5

@ johnny5 มันอธิบายว่าทำไมคุณถึงได้ผลลัพธ์ที่ได้นั่นคือเพราะนั่นคือวิธีการทำงานของคณิตศาสตร์
Matt Ellen

2
คณิตศาสตร์และคำสั่งของการดำเนินการที่คอมไพเลอร์ประมวลผลคำสั่งเป็นสองสิ่งที่แตกต่างกัน ภายใต้ตรรกะของคุณ k + = c; c + = k; k + = c ควรประเมินเป็นผลลัพธ์เดียวกัน
5

ไม่จอห์นนี่ 5 นั่นไม่ใช่ความหมาย ในทางคณิตศาสตร์พวกเขาเป็นสิ่งที่แตกต่างกัน การดำเนินการสามอย่างแยกกันประเมินเป็น 3c + 2k
Matt Ellen

2
น่าเสียดายที่โซลูชัน "พีชคณิต" ของคุณถูกต้องโดยบังเอิญเท่านั้น เทคนิคของคุณไม่ทำงานโดยทั่วไป พิจารณาx = 1;และy = (x += x) + x;มันคือการต่อสู้ของคุณว่า "มีสาม X และ Y เพื่อให้เท่ากับ3 * x"? เพราะyเท่ากับ4ในกรณีนี้ ทีนี้y = x + (x += x);คุณจะโต้แย้งว่ากฎพีชคณิต "a + b = b + a" เป็นจริงได้อย่างไรและนี่ก็คือ 4 ด้วย? เพราะนี่คือ 3 แต่น่าเสียดายที่C # ไม่ปฏิบัติตามกฎของพีชคณิตโรงเรียนมัธยมถ้ามีผลข้างเคียงในการแสดงออก C # เป็นไปตามกฎของพีชคณิตที่มีผลข้างเคียง
Eric Lippert
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.