เหตุใด List <T> .ForEach จึงอนุญาตให้แก้ไขรายการได้


90

ถ้าฉันใช้:

var strings = new List<string> { "sample" };
foreach (string s in strings)
{
  Console.WriteLine(s);
  strings.Add(s + "!");
}

Addในforeachพ่น InvalidOperationException (เก็บได้รับการปรับเปลี่ยนการดำเนินการแจงนับไม่อาจดำเนินการ) ซึ่งผมคิดว่าตรรกะเนื่องจากเรามีการดึงพรมจากภายใต้เท้าของเรา

อย่างไรก็ตามหากฉันใช้:

var strings = new List<string> { "sample" };
strings.ForEach(s =>
  {
    Console.WriteLine(s);
    strings.Add(s + "!");
  });

มันยิงตัวเองที่เท้าทันทีโดยการวนซ้ำจนกว่ามันจะพ่น OutOfMemoryException

นี้มาเป็นแปลกใจให้ฉันเป็นฉันคิดเสมอว่า List.ForEach เป็นอย่างใดอย่างหนึ่งเพียงเสื้อคลุมสำหรับหรือforeach ใครมีคำอธิบายเกี่ยวกับวิธีการและสาเหตุของพฤติกรรมนี้หรือไม่?for

(Inpired byEach loop for a Generic List ซ้ำไม่รู้จบ )


7
ฉันเห็นด้วย. นี่คือ - น่าสงสัย ฉันขอแนะนำให้คุณโพสต์สิ่งนั้นบน microsoft connect และขอคำชี้แจง
TomTom

4
"สิ่งนี้เป็นเรื่องแปลกใจสำหรับฉันอย่างที่ฉันคิดเสมอว่า ListForEach เป็นเพียงกระดาษห่อหุ้มforeachหรือสำหรับfor" มันยังคงใช้งานforได้ คุณสามารถดำเนินการเดียวกันในforลูปและสร้าง OutOfMemoryException เดียวกันเป็นผลลัพธ์
Anthony Pegram

นี่เป็นไปตามคำถามของฉัน: stackoverflow.com/q/9311272/132239ขอบคุณ SWeko สำหรับการลงรายละเอียด
Kasrak

คำตอบ:


68

เป็นเพราะForEachวิธีการนี้ไม่ได้ใช้ตัวแจงนับมันจะวนforซ้ำรายการด้วยการวนซ้ำ:

public void ForEach(Action<T> action)
{
    if (action == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    for (int i = 0; i < this._size; i++)
    {
        action(this._items[i]);
    }
}

(รหัสที่ได้รับจาก JustDecompile)

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


ใช่ แต่_sizeคำนวณอย่างไร? หากเป็นเพียงการคำนวณล่วงหน้าหากควรรันเพียงครั้งเดียวสำหรับตัวอย่างของฉัน เห็นได้ชัดว่ามันสดชื่นอย่างใด
SWeko

7
มีการรีเฟรชที่ Add method -> this._items [this._size ++] = item;
Fabio

1
@SWeko ไม่ได้คำนวณมีการอัปเดตทุกครั้งที่มีการเพิ่มหรือลบรายการ
Thomas Levesque

1
มี_versionตัวแปรส่วนตัวList<T>ที่สามารถตรวจจับสถานการณ์ประเภทนี้ได้เนื่องจากมีการอัปเดตในการดำเนินการที่เปลี่ยนแปลงรายการเอง
SWeko

คุณสามารถหลีกเลี่ยงข้อยกเว้นได้โดยรับขนาด (int theSize = this._size) ก่อนจากนั้นใช้ภายใน for loop?
Lazlow

14

List<T>.ForEachถูกนำไปใช้forภายในดังนั้นจึงไม่ใช้ตัวนับและอนุญาตให้แก้ไขคอลเลกชัน


6

เนื่องจาก ForEach ที่แนบมากับคลาส List ภายในจะใช้ for loop ที่แนบโดยตรงกับสมาชิกภายในซึ่งคุณสามารถดูได้โดยดาวน์โหลดซอร์สโค้ดสำหรับ. NET framework

http://referencesource.microsoft.com/netframework.aspx

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


และเพื่อตอบความคิดเห็นใน @Thomas โพสต์เกี่ยวกับวิธีการรีเฟรช - สมาชิกภายในจะได้รับการรีเฟรชเมื่อมีการเรียก add ดังนั้นจึงสามารถติดตามการเปลี่ยนแปลงได้ หากคุณต้องทำการแทรกที่ดัชนีน้อยกว่าดัชนีปัจจุบันคุณจะไม่ดำเนินการกับรายการนั้นเนื่องจากมีการแสดงซ้ำแล้วซ้ำอีกในรายการนั้น แต่เนื่องจากคุณเพิ่มจนจบมันก็ใช้ได้
Mike Perrenoud

1
ใช่การเปลี่ยนAddบรรทัดโดยstrings.Insert(0, s + "!")พิมพ์แค่ 'ตัวอย่าง' เป็นเรื่องแปลกที่ไม่มีการกล่าวถึงสิ่งนี้เลยในเอกสาร
SWeko

ฉันคิดว่า Microsoft ตระหนักดีว่าเป็นไปไม่ได้เลยที่จะให้ทุกข้อแม้ที่มีอยู่ในเอกสารของพวกเขาดังนั้นพวกเขาจึงให้ซอร์สโค้ดทันที ฉันพบว่าวิธีแก้ปัญหาที่ดีกว่าโดยสุจริต แต่ปัญหาเดียวที่ฉันพบคือผลิตภัณฑ์อย่าง WF ไม่ได้รับการอัปเดตอย่างรวดเร็ว - ซอร์สโค้ด 4.x WF ยังไม่พร้อมใช้งาน
Mike Perrenoud

4

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

        var list = new List<string>();
        list.Add("Foo");
        list.Add("Bar");

        list.ForEach((item) => 
        { 
            if(item=="Foo") 
                list.Remove(item); 
        });

ประโยชน์ของวิธีนี้นั้นเป็นที่น่าสงสัยตามที่Eric Lippertชี้ให้เห็นดังนั้นเราจึงไม่ได้รวมไว้ใน. NET สำหรับแอปสไตล์ Metro (เช่นแอป Windows 8)

เดวิดคีน (ทีม BCL)


1
ฉันเห็นว่านี่จะเป็นการเปลี่ยนแปลงครั้งใหญ่ที่เลวร้าย แต่ถึงกระนั้นมันก็สามารถล้มเหลวได้ในรูปแบบที่ไม่ชัดเจนและนั่นก็ไม่ใช่เรื่องดี ฉันไม่เห็นสถานการณ์ที่การใช้วิธีการ ForEach นั้นเหนือกว่าวิธีง่ายๆสำหรับ (หรือ foreach หากไม่จำเป็นต้องมีการเปลี่ยนแปลงรายการต้นฉบับ)
SWeko
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.