มีเหตุผลสำหรับการใช้ตัวแปร C # ซ้ำใน foreach หรือไม่?


1684

เมื่อใช้แลมบ์ดานิพจน์หรือวิธีที่ไม่ระบุตัวตนใน C # เราต้องระวังการเข้าถึงหลุมพรางปิดที่มีการแก้ไข ตัวอย่างเช่น:

foreach (var s in strings)
{
   query = query.Where(i => i.Prop == s); // access to modified closure
   ...
}

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

ดังที่อธิบายไว้ที่นี่สิ่งนี้เกิดขึ้นเพราะsตัวแปรที่ประกาศในforeachลูปด้านบนถูกแปลเช่นนี้ในคอมไพเลอร์:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}

แทนที่จะเป็นเช่นนี้:

while (enumerator.MoveNext())
{
   string s;
   s = enumerator.Current;
   ...
}

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

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}
var finalString = s;

อย่างไรก็ตามตัวแปรที่กำหนดในforeachลูปไม่สามารถใช้ภายนอกลูปได้:

foreach(string s in strings)
{
}
var finalString = s; // won't work: you're outside the scope.

ดังนั้นคอมไพเลอร์จึงประกาศตัวแปรในลักษณะที่ทำให้เกิดข้อผิดพลาดสูงซึ่งมักจะยากที่จะค้นหาและตรวจแก้จุดบกพร่องในขณะที่ไม่มีประโยชน์ที่รับรู้ได้

มีบางสิ่งที่คุณสามารถทำกับforeachลูปได้ด้วยวิธีนี้ถ้าคุณไม่ได้คอมไพล์ด้วยตัวแปรภายในหรือนี่เป็นเพียงตัวเลือกโดยพลการที่ทำขึ้นก่อนวิธีที่ไม่ระบุชื่อและการแสดงออกแลมบ์ดานั้นมีอยู่หรือทั่วไป ไม่ได้รับการแก้ไขตั้งแต่นั้นมา?


4
มีอะไรผิดปกติกับString s; foreach (s in strings) { ... }?
แบรดคริสตี้

5
@BradChristie OP ไม่ได้จริงๆพูดคุยเกี่ยวforeachแต่เกี่ยวกับนิพจน์ Lamda ผลในรหัสที่คล้ายกันที่แสดงโดยสหกรณ์ ...
Yahia

22
@BradChristie: รวบรวมหรือไม่ ( ข้อผิดพลาด: ประเภทและระบุมีทั้งที่จำเป็นต้องใช้ในคำสั่ง foreachสำหรับฉัน)
ออสติน Salonen

32
@JakobBotschNielsen: มันเป็นแบบปิดด้านนอกของแลมบ์ดา ทำไมคุณคิดว่ามันจะอยู่ในกองซ้อน? อายุการใช้งานนานกว่ากรอบสแต็ก !
Eric Lippert

3
@EricLippert: ฉันสับสน ผมเข้าใจว่าแลมบ์ดาจับอ้างอิงกับตัวแปร foreach (ซึ่งจะมีการประกาศภายในนอกวง) และดังนั้นคุณจะจบลงเมื่อเทียบกับค่าสุดท้ายของตน ที่ฉันได้รับ สิ่งที่ฉันไม่เข้าใจคือการประกาศตัวแปรภายในลูปจะสร้างความแตกต่างได้อย่างไร จากมุมมองของคอมไพเลอร์ - ผู้เขียนฉันจัดสรรการอ้างอิงสตริงเพียงหนึ่งรายการ (var 's') บนสแต็กโดยไม่คำนึงว่าการประกาศอยู่ภายในหรือภายนอกลูป แน่นอนว่าฉันไม่ต้องการผลักดันการอ้างอิงใหม่ลงบนสแต็กทุก ๆ การวนซ้ำ!
Anthony

คำตอบ:


1407

คอมไพเลอร์ประกาศตัวแปรในลักษณะที่ทำให้มีแนวโน้มสูงที่จะเกิดข้อผิดพลาดซึ่งมักจะยากที่จะค้นหาและตรวจแก้จุดบกพร่องในขณะที่ไม่มีประโยชน์ที่รับรู้ได้

การวิจารณ์ของคุณนั้นสมเหตุสมผล

ฉันพูดถึงปัญหานี้โดยละเอียดที่นี่:

การปิดตัวแปรลูปถือว่าเป็นอันตราย

มีบางสิ่งที่คุณสามารถทำกับลูป foreach ในแบบที่คุณทำไม่ได้ถ้าพวกมันถูกคอมไพล์ด้วยตัวแปรภายใน หรือนี่เป็นเพียงตัวเลือกโดยพลการที่ทำขึ้นก่อนวิธีการแบบไม่ระบุชื่อและการแสดงออกแลมบ์ดามีให้บริการหรือพบเห็นทั่วไปและสิ่งใดที่ไม่ได้รับการแก้ไขตั้งแต่นั้นมา?

หลัง. ข้อกำหนด C # 1.0 ไม่ได้บอกว่าตัวแปรลูปอยู่ภายในหรือภายนอกตัวลูปเนื่องจากมันไม่มีความแตกต่างที่สังเกตได้ เมื่อซีแมนทิกส์ปิดถูกแนะนำใน C # 2.0 ทางเลือกถูกสร้างขึ้นเพื่อใส่ตัวแปรลูปนอกลูป

ฉันคิดว่ามันยุติธรรมที่จะพูดว่าการตัดสินใจทั้งหมดนั้นเสียใจ นี่เป็นหนึ่งใน "gotchas" ที่เลวร้ายที่สุดใน C # และเราจะทำการเปลี่ยนแปลงครั้งใหญ่เพื่อแก้ไข ใน C # 5 ตัวแปรลูป foreach จะมีเหตุผลอยู่ภายในเนื้อความของลูปดังนั้นการปิดจะได้รับสำเนาใหม่ทุกครั้ง

การforวนซ้ำจะไม่ถูกเปลี่ยนแปลงและการเปลี่ยนแปลงจะไม่ "ย้อนกลับพอร์ต" เป็น C # รุ่นก่อนหน้า คุณควรใช้ความระมัดระวังต่อไปเมื่อใช้สำนวนนี้


177
ในความเป็นจริงเราได้ผลักดันการเปลี่ยนแปลงนี้ใน C # 3 และ C # 4 เมื่อเราออกแบบ C # 3 เราได้ตระหนักว่าปัญหา (ซึ่งมีอยู่แล้วใน C # 2) จะแย่ลงเพราะจะมี lambdas จำนวนมาก (และแบบสอบถาม comprehensions ซึ่งเป็น lambdas ปลอมตัว) ในห่วง foreach ขอบคุณ LINQ ฉันเสียใจที่เรารอคอยสำหรับปัญหาที่จะได้รับเพียงพอที่ดีที่จะแก้ไขมันใบสำคัญแสดงสิทธิเพื่อให้ปลายมากกว่าการแก้ไขมันใน C # 3
เอริค Lippert

75
และตอนนี้เราจะต้องจำไว้ว่าforeach'ปลอดภัย' แต่forไม่ใช่
leppie

22
@michielvoo: การเปลี่ยนแปลงที่เกิดขึ้นในแง่ที่ว่ามันไม่เข้ากันได้ย้อนหลัง รหัสใหม่จะทำงานไม่ถูกต้องเมื่อใช้คอมไพเลอร์รุ่นเก่า
leppie

41
@Benjol: ไม่ซึ่งเป็นเหตุผลว่าทำไมเรายินดีที่จะรับมัน Jon Skeet ชี้ให้ฉันเห็นถึงสถานการณ์การเปลี่ยนแปลงที่สำคัญซึ่งก็คือว่ามีคนเขียนรหัสใน C # 5 ทดสอบและแบ่งปันกับผู้ที่ยังใช้ C # 4 อยู่ซึ่งไร้เดียงสาเชื่อว่าถูกต้อง หวังว่าจำนวนคนที่ได้รับผลกระทบจากสถานการณ์นี้จะน้อย
Eric Lippert

29
นอกจากนี้ ReSharper ยังได้จับสิ่งนี้อยู่เสมอและรายงานว่าเป็น "การเข้าถึงการปิดการแก้ไข" จากนั้นกดปุ่ม Alt + Enter จะเป็นการแก้ไขรหัสของคุณโดยอัตโนมัติหรือไม่ jetbrains.com/resharper
Mike Chamberlain

191

สิ่งที่คุณถามนั้นครอบคลุมโดย Eric Lippert ในบล็อกโพสต์ของเขาการปิดตัวแปรลูปถือว่าเป็นอันตรายและผลสืบเนื่อง

สำหรับฉันข้อโต้แย้งที่น่าเชื่อที่สุดคือการมีตัวแปรใหม่ในการทำซ้ำแต่ละครั้งจะไม่สอดคล้องกับfor(;;)ลักษณะวนรอบ คุณคาดหวังว่าจะมีสิ่งใหม่int iในการทำซ้ำแต่ละครั้งfor (int i = 0; i < 10; i++)หรือไม่

ปัญหาที่พบบ่อยที่สุดกับพฤติกรรมนี้คือการปิดตัวแปรการทำซ้ำและมีวิธีแก้ไขปัญหาง่าย ๆ :

foreach (var s in strings)
{
    var s_for_closure = s;
    query = query.Where(i => i.Prop == s_for_closure); // access to modified closure

โพสต์ของฉันบล็อกเกี่ยวกับปัญหานี้: ปิดมากกว่าตัวแปร foreach ใน C #


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

1
ใช่มันเป็นไปไม่ได้ที่จะปิดโดยค่า แต่มีวิธีแก้ปัญหาง่ายมากฉันเพิ่งแก้ไขคำตอบของฉันที่จะรวม
Krizz

6
การปิดที่เลวร้ายเกินไปใน C # ใกล้เคียงกับการอ้างอิง refหากพวกเขาปิดมากกว่าค่าตามค่าเริ่มต้นเราได้อย่างง่ายดายสามารถระบุปิดตัวแปรแทนด้วย
Sean U

2
@ Krizz นี่เป็นกรณีที่ความมั่นคงที่ถูกบังคับนั้นมีอันตรายมากกว่าการที่ไม่สอดคล้องกัน ควร "เพียงแค่ทำงาน" ตามที่ผู้คนคาดหวังและชัดเจนว่าคนคาดหวังสิ่งที่แตกต่างเมื่อใช้ foreach ซึ่งตรงข้ามกับ for for loop เนื่องจากจำนวนคนที่มีปัญหาก่อนที่เราจะรู้เรื่องการเข้าถึงปัญหาการปิดปรับปรุง (เช่นตัวเอง) .
Andy

2
@ Random832 ไม่ทราบเกี่ยวกับ C # แต่ใน LISP ทั่วไปมีไวยากรณ์สำหรับสิ่งนั้นและมันแสดงให้เห็นว่าภาษาใด ๆ ที่มีตัวแปรที่ไม่แน่นอนและการปิดจะต้อง (ไม่จำเป็นต้อง ) ด้วยเช่นกัน เราปิดการอ้างอิงไปยังสถานที่ที่เปลี่ยนแปลงหรือค่าที่มีในช่วงเวลาที่กำหนด (สร้างการปิด) สิ่งนี้จะกล่าวถึงสิ่งที่คล้ายกันใน Python และ Scheme ( cutสำหรับ refs / vars และcuteสำหรับการรักษาค่าที่ประเมินไว้ในการปิดที่ประเมินบางส่วน)
Will Ness

103

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

foreach (var s in strings)
    query = query.Where(i => i.Prop == s); // access to modified closure

ฉันทำ:

foreach (var s in strings)
{
    string search = s;
    query = query.Where(i => i.Prop == search); // New definition ensures unique per iteration.
}        

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


24
นั่นเป็นวิธีแก้ปัญหาทั่วไปขอบคุณสำหรับการสนับสนุน Resharper นั้นฉลาดพอที่จะจดจำรูปแบบนี้และนำมันมาสู่ความสนใจของคุณด้วยซึ่งก็ดี ฉันยังไม่ได้ bit โดยรูปแบบนี้ในขณะที่ แต่เพราะมันเป็นในคำพูดของเอริค Lippert ของ "ที่พบมากที่สุดรายงานข้อผิดพลาดเพียงครั้งเดียวที่ไม่ถูกต้องที่เราได้รับ" ฉันอยากรู้ว่าทำไมมากกว่าวิธีการที่จะหลีกเลี่ยงได้
StriplingWarrior

62

ใน C # 5.0 ปัญหานี้ได้รับการแก้ไขและคุณสามารถปิดตัวแปรลูปและรับผลลัพธ์ที่คุณคาดหวัง

ข้อกำหนดภาษาพูดว่า:

8.8.4 คำสั่ง foreach

( ... )

คำสั่ง foreach ของแบบฟอร์ม

foreach (V v in x) embedded-statement

จะถูกขยายเป็น:

{
  E e = ((C)(x)).GetEnumerator();
  try {
      while (e.MoveNext()) {
          V v = (V)(T)e.Current;
          embedded-statement
      }
  }
  finally {
       // Dispose e
  }
}

( ... )

การจัดวางvด้านในของ while while มีความสำคัญสำหรับวิธีการจับโดยฟังก์ชันที่ไม่ระบุชื่อใด ๆ ที่เกิดขึ้นในคำสั่งฝังตัว ตัวอย่างเช่น:

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null) f = () => Console.WriteLine("First value: " + value);
}
f();

หากvมีการประกาศนอกวงขณะที่มันจะถูกใช้ร่วมกันในการทำซ้ำทั้งหมดและค่าของมันหลังจากห่วงสำหรับจะเป็นค่าสุดท้าย13ซึ่งเป็นสิ่งที่ภาวนาของfจะพิมพ์ แต่เนื่องจากการวนซ้ำแต่ละครั้งมีตัวแปรของตัวเองvการจับfซ้ำโดยในการทำซ้ำครั้งแรกจะยังคงเก็บค่า 7ซึ่งเป็นสิ่งที่จะพิมพ์ ( หมายเหตุ: รุ่นก่อนหน้าของ C # ประกาศvนอกห่วงขณะที่ )


1
เหตุใด C # รุ่นแรกนี้จึงประกาศ v ในขณะที่ลูป? msdn.microsoft.com/en-GB/library/aa664754.aspx
colinfang

4
@colinfang ให้แน่ใจว่าได้อ่านคำตอบของ Eric : ข้อกำหนด C # 1.0 ( ในลิงก์ของคุณเรากำลังพูดถึง VS 2003, C # 1.2 ) จริง ๆ แล้วไม่ได้บอกว่าตัวแปร loop อยู่ภายในหรือภายนอกร่างกาย loop เพราะมันไม่มีความแตกต่างที่สังเกตได้ . เมื่อซีแมนทิกส์ปิดถูกแนะนำใน C # 2.0 ทางเลือกถูกสร้างขึ้นเพื่อใส่ตัวแปรลูปนอกลูป
เปาโล Moretti

1
คุณกำลังบอกว่าตัวอย่างในลิงค์ไม่ใช่สเป็คที่ชัดเจนในขณะนั้น?
colinfang

4
@ colinfang พวกเขามีข้อกำหนดที่ชัดเจน ปัญหาคือว่าเรากำลังพูดถึงคุณสมบัติ (เช่นฟังก์ชั่นปิด) ที่ได้รับการแนะนำในภายหลัง (กับ C # 2.0) เมื่อ C # 2.0 ขึ้นมาพวกเขาตัดสินใจที่จะวางตัวแปรลูปนอกลูป และพวกเขาก็เปลี่ยนใจอีกครั้งด้วย C # 5.0 :)
เปาโล Moretti
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.