เข้าถึงตัวแปร foreach ในคำเตือนการปิด


86

ฉันได้รับคำเตือนต่อไปนี้:

เข้าถึงตัวแปร foreach ในการปิด อาจมีลักษณะการทำงานที่แตกต่างกันเมื่อคอมไพล์ด้วยคอมไพเลอร์เวอร์ชันต่างๆ

นี่คือสิ่งที่ดูเหมือนในตัวแก้ไขของฉัน:

ข้อความแสดงข้อผิดพลาดข้างต้นในป๊อปอัปแบบเลื่อน

ฉันรู้วิธีแก้ไขคำเตือนนี้ แต่ฉันอยากรู้ว่าทำไมฉันถึงได้รับคำเตือนนี้

นี่เกี่ยวกับเวอร์ชัน "CLR" หรือไม่ เกี่ยวข้องกับ "IL" หรือไม่?



1
TL; DR คำตอบ: เพิ่ม. ToList () หรือ. ToArray () ที่ท้ายนิพจน์การค้นหาของคุณและจะกำจัดคำเตือน
JoelFan

คำตอบ:


136

คำเตือนนี้มีสองส่วน อย่างแรกคือ ...

เข้าถึงตัวแปร foreach ในการปิด

... ซึ่งไม่ถูกต้องตามข้อใด แต่เป็นเรื่องที่ตอบโต้ได้ง่ายในตอนแรก นอกจากนี้ยังยากมากที่จะทำถูกต้อง (มากจนบทความที่ฉันลิงก์ด้านล่างอธิบายว่า "เป็นอันตราย")

รับคำถามของคุณโดยสังเกตว่าโค้ดที่คุณตัดตอนมานั้นโดยพื้นฐานแล้วเป็นรูปแบบขยายของสิ่งที่คอมไพเลอร์ C # (ก่อน C # 5) สร้างขึ้นสำหรับforeach1 :

ฉัน [ไม่] เข้าใจว่าเหตุใด [ต่อไปนี้] จึงไม่ถูกต้อง:

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

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

var countingActions = new List<Action>();

var numbers = from n in Enumerable.Range(1, 5)
              select n.ToString(CultureInfo.InvariantCulture);

using (var enumerator = numbers.GetEnumerator())
{
    string s;

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

        Console.WriteLine("Creating an action where s == {0}", s);
        Action action = () => Console.WriteLine("s == {0}", s);

        countingActions.Add(action);
    }
}

หากคุณเรียกใช้รหัสนี้คุณจะได้รับเอาต์พุตคอนโซลต่อไปนี้:

Creating an action where s == 1
Creating an action where s == 2
Creating an action where s == 3
Creating an action where s == 4
Creating an action where s == 5

นี่คือสิ่งที่คุณคาดหวัง

หากต้องการดูสิ่งที่คุณอาจไม่คาดคิดให้เรียกใช้รหัสต่อไปนี้ทันทีหลังจากรหัสด้านบน:

foreach (var action in countingActions)
    action();

คุณจะได้รับเอาต์พุตคอนโซลต่อไปนี้:

s == 5
s == 5
s == 5
s == 5
s == 5

ทำไม? เพราะเราสร้างฟังก์ชันขึ้นมาห้าฟังก์ชันที่ทำสิ่งเดียวกันทั้งหมด: พิมพ์ค่าของs(ซึ่งเราปิดไปแล้ว) ในความเป็นจริงฟังก์ชันเดียวกัน ("พิมพ์s" "พิมพ์s" "พิมพ์s" ... )

เมื่อมาถึงจุดที่เราไปใช้พวกเขาพวกเขาไม่ว่าสิ่งที่เราถาม: sพิมพ์ค่าของ ถ้าคุณดูที่มูลค่าที่รู้จักกันครั้งสุดท้ายของคุณจะเห็นว่ามันเป็นs 5ดังนั้นเราจึงได้รับการs == 5พิมพ์ห้าครั้งในคอนโซล

ซึ่งเป็นสิ่งที่เราขอ แต่อาจไม่ใช่สิ่งที่เราต้องการ

ส่วนที่สองของคำเตือน ...

อาจมีลักษณะการทำงานที่แตกต่างกันเมื่อคอมไพล์ด้วยคอมไพเลอร์เวอร์ชันต่างๆ

... มันคืออะไร. เริ่มต้นด้วย C # 5 คอมไพเลอร์สร้างรหัสที่แตกต่างกันที่ว่า "ป้องกัน" foreachนี้เกิดขึ้นผ่านทาง

ดังนั้นโค้ดต่อไปนี้จะให้ผลลัพธ์ที่แตกต่างกันภายใต้คอมไพเลอร์เวอร์ชันต่างๆ:

foreach (var n in numbers)
{
    Action action = () => Console.WriteLine("n == {0}", n);
    countingActions.Add(action);
}

ดังนั้นมันจะสร้างคำเตือน R # ด้วย :)

ข้อมูลโค้ดแรกของฉันด้านบนจะแสดงลักษณะการทำงานเหมือนกันในทุกเวอร์ชันของคอมไพเลอร์เนื่องจากฉันไม่ได้ใช้foreach(แต่ฉันได้ขยายออกไปตามที่คอมไพเลอร์ pre-C # 5 ทำ)

สำหรับรุ่น CLR หรือไม่

ฉันไม่ค่อยแน่ใจว่าคุณกำลังถามอะไรที่นี่

โพสต์ของ Eric Lippert ระบุว่าการเปลี่ยนแปลงเกิดขึ้น "ใน C # 5" ดังนั้นสันนิษฐานว่าคุณต้องกำหนดเป้าหมาย. NET 4.5 หรือใหม่กว่า ด้วยคอมไพเลอร์ C # 5 หรือใหม่กว่าเพื่อรับพฤติกรรมใหม่และทุกอย่างก่อนหน้านั้นจะได้รับพฤติกรรมเก่า

แต่เพื่อความชัดเจนมันเป็นหน้าที่ของคอมไพเลอร์ไม่ใช่เวอร์ชัน. NET Framework

มีความเกี่ยวข้องกับ IL หรือไม่?

รหัสที่แตกต่างกันสร้าง IL ที่แตกต่างกันดังนั้นในแง่นั้นจึงมีผลที่ตามมาสำหรับ IL ที่สร้าง

1 foreachเป็นโครงสร้างที่ใช้กันทั่วไปมากกว่าโค้ดที่คุณโพสต์ไว้ในความคิดเห็นของคุณ ปัญหามักเกิดจากการใช้foreachไม่ใช่การแจงนับด้วยตนเอง นั่นเป็นเหตุผลที่การเปลี่ยนแปลงforeachใน C # 5 ช่วยป้องกันปัญหานี้ แต่ไม่สมบูรณ์


7
ฉันได้ลอง foreach loop กับคอมไพเลอร์ที่แตกต่างกันเพื่อให้ได้ผลลัพธ์ที่แตกต่างกันโดยใช้เป้าหมายเดียวกัน (.Net 3.5) ฉันใช้ VS2010 (ซึ่งในทางกลับกันใช้คอมไพเลอร์ที่เกี่ยวข้องกับ. net 4.0 ฉันเชื่อ) และ VS2012 (.net 4.5 คอมไพเลอร์ที่ฉันเชื่อ) ตามหลักการแล้วหมายความว่าหากคุณใช้ VS2013 และแก้ไขการกำหนดเป้าหมายโครงการ. Net 3.5 และสร้างบนเซิร์ฟเวอร์บิลด์ที่ติดตั้งเฟรมเวิร์กที่เก่ากว่าเล็กน้อยคุณจะเห็นผลลัพธ์ที่แตกต่างกันของโปรแกรมบนเครื่องของคุณเทียบกับบิลด์ที่ปรับใช้
Ykok

คำตอบที่ดี แต่ไม่แน่ใจว่า "foreach" เกี่ยวข้องอย่างไร สิ่งนี้จะไม่เกิดขึ้นกับการแจงนับแบบแมนนวลหรือง่าย ๆ สำหรับลูป (int i = 0; i <collection.Size; i ++)? ดูเหมือนว่าจะเป็นปัญหาเกี่ยวกับการปิดกิจการนอกขอบเขตหรืออย่างถูกต้องกว่านั้นคือปัญหาที่ผู้คนเข้าใจว่าการปิดมีพฤติกรรมอย่างไรเมื่อพวกเขาออกไปนอกขอบเขตที่กำหนดไว้ภายใน
Brad

foreachสิ่งที่นี่มาจากเนื้อหาของคำถาม คุณคิดถูกที่มันสามารถเกิดขึ้นได้หลายวิธีโดยทั่วไป
ta.speot.is

1
ทำไม R # ยังเตือนฉันไม่อ่านกรอบเป้าหมายซึ่งฉันตั้งค่าเป็น 4.5
Johnny_D

1
"ดังนั้นสันนิษฐานว่าคุณต้องกำหนดเป้าหมาย. NET 4.5 หรือใหม่กว่า"ข้อความนี้ไม่เป็นความจริง เวอร์ชันของ. NET ที่คุณกำหนดเป้าหมายจะไม่มีผลต่อสิ่งนี้ลักษณะการทำงานจะเปลี่ยนไปใน. NET 2.0, 3.5 และ 4 ด้วยถ้าคุณใช้ C # 5 (VS 2012 หรือใหม่กว่า) ในการคอมไพล์ นั่นคือเหตุผลที่คุณได้รับคำเตือนนี้บน. NET 4.0 หรือรุ่นก่อนหน้าหากคุณกำหนดเป้าหมาย 4.5 คุณจะไม่ได้รับคำเตือนเนื่องจากคุณไม่สามารถคอมไพล์ 4.5 บนคอมไพเลอร์ C # 4 หรือรุ่นเก่ากว่าได้
Scott Chamberlain

12

คำตอบแรกดีมากดังนั้นฉันคิดว่าฉันจะเพิ่มสิ่งเดียว

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

ถ้าคุณเปลี่ยน

...Where(x => x.Name == property.Value)

ถึง

...Where(x => x.Name == property.Value).ToList()

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


ฉันอ่านคำอธิบายยาว ๆ จำนวนมากที่ไม่สามารถแก้ปัญหานี้ให้ฉันได้จากนั้นก็คำอธิบายสั้น ๆ ที่ได้ ขอบคุณ!
Charles Clayton

ฉันอ่านคำตอบที่ได้รับการยอมรับและคิดว่า "จะปิดอย่างไรหากไม่ผูกมัดตัวแปร" แต่ตอนนี้ฉันเข้าใจแล้วว่าการประเมินจะเกิดขึ้นเมื่อใดขอบคุณ!
เจอโรม

ใช่นี่เป็นวิธีแก้ปัญหาสากลที่ชัดเจน ช้าและใช้หน่วยความจำมาก แต่ฉันคิดว่ามันใช้งานได้จริง 100% สำหรับทุกกรณี
Al Kepp

8

ตัวแปรที่กำหนดขอบเขตบล็อกควรแก้ไขคำเตือน

foreach (var entry in entries)
{
   var en = entry; 
   var result = DoSomeAction(o => o.Action(en));
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.