วิธีที่ชาญฉลาดในการลบรายการออกจากรายการ <T> ในขณะที่ระบุใน C #


87

ฉันมีกรณีคลาสสิกในการพยายามลบรายการออกจากคอลเลกชั่นในขณะที่ระบุเป็นวงรอบ:

List<int> myIntCollection = new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);

foreach (int i in myIntCollection)
{
    if (i == 42)
        myIntCollection.Remove(96);    // The error is here.
    if (i == 25)
        myIntCollection.Remove(42);    // The error is here.
}

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

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

  1. อย่าลบภายในลูปนี้ แต่ให้เก็บ "ลบรายการ" แยกต่างหากที่คุณประมวลผลหลังจากลูปหลัก

    นี่เป็นวิธีแก้ปัญหาที่ดี แต่ในกรณีของฉันฉันต้องการให้ไอเท็มหายไปทันทีในฐานะ "รอ" จนกระทั่งหลังจากลูปหลักเพื่อลบไอเท็มจะเปลี่ยนโฟลว์ลอจิกของโค้ดของฉัน

  2. แทนที่จะลบรายการเพียงแค่ตั้งค่าสถานะบนรายการและทำเครื่องหมายว่าไม่ใช้งาน จากนั้นเพิ่มการทำงานของรูปแบบ 1 เพื่อล้างรายการ

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

  3. อย่างใดนำแนวคิดของรูปแบบที่ 2 List<T>ในชั้นเรียนที่มาจาก ซูเปอร์ลิสต์นี้จะจัดการแฟล็กที่ไม่ใช้งานการลบอ็อบเจ็กต์หลังความจริงและจะไม่เปิดเผยรายการที่ทำเครื่องหมายว่าไม่ใช้งานต่อผู้บริโภคการแจงนับ โดยพื้นฐานแล้วมันจะสรุปความคิดทั้งหมดของรูปแบบ 2 (และรูปแบบที่ 1 ในภายหลัง)

    คลาสแบบนี้มีอยู่จริงหรือไม่? ใครมีรหัสสำหรับสิ่งนี้? หรือมีวิธีที่ดีกว่านี้?

  4. ฉันได้รับแจ้งว่าการเข้าถึงmyIntCollection.ToArray()แทนmyIntCollectionจะช่วยแก้ปัญหาและอนุญาตให้ฉันลบภายในลูป

    ดูเหมือนว่าจะเป็นรูปแบบการออกแบบที่ไม่ดีสำหรับฉันหรืออาจจะดี?

รายละเอียด:

  • รายการจะมีหลายรายการและฉันจะลบเพียงบางรายการเท่านั้น

  • ภายในลูปฉันจะทำกระบวนการทุกประเภทเพิ่มลบ ฯลฯ ดังนั้นวิธีแก้ปัญหาจึงต้องค่อนข้างทั่วไป

  • รายการที่ฉันต้องการลบอาจไม่ใช่รายการปัจจุบันในลูป ตัวอย่างเช่นฉันอาจอยู่ในรายการ 10 จาก 30 รายการวนซ้ำและจำเป็นต้องลบรายการ 6 หรือรายการ 26 การเดินย้อนกลับไปตามอาร์เรย์จะไม่ทำงานอีกต่อไปเนื่องจากสิ่งนี้ ; o (


ข้อมูลที่เป็นประโยชน์สำหรับคนอื่น: ข้อผิดพลาดในการหลีกเลี่ยงคอลเล็กชันได้รับการแก้ไข (การห่อหุ้มรูปแบบ 1)
George Duckett

หมายเหตุด้านข้าง: รายการเผื่อเวลาไว้มาก (โดยทั่วไปคือ O (N) โดยที่ N คือความยาวของรายการ) ย้ายค่า หากต้องการการเข้าถึงแบบสุ่มที่มีประสิทธิภาพจริงๆก็เป็นไปได้ที่จะลบใน O (log N) โดยใช้ต้นไม้ไบนารีที่สมดุลซึ่งถือจำนวนโหนดในทรีย่อยที่มีรูทอยู่ เป็น BST ที่มีคีย์ (ดัชนีในลำดับ) เป็นนัย
Palec

โปรดดูคำตอบ: stackoverflow.com/questions/7193294/…
Dabbas

คำตอบ:


196

ทางออกที่ดีที่สุดมักจะใช้RemoveAll()วิธีการ:

myList.RemoveAll(x => x.SomeProp == "SomeValue");

หรือหากคุณต้องการลบองค์ประกอบบางอย่าง :

MyListType[] elems = new[] { elem1, elem2 };
myList.RemoveAll(x => elems.Contains(x));

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

for (int i = myList.Count - 1; i >= 0; i--)
{
    // Do processing here, then...
    if (shouldRemoveCondition)
    {
        myList.RemoveAt(i);
    }
}

การถอยหลังทำให้แน่ใจว่าคุณจะไม่ข้ามองค์ประกอบใด ๆ

การตอบสนองต่อการแก้ไข :

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

List<int> toRemove = new List<int>();
foreach (var elem in myList)
{
    // Do some stuff

    // Check for removal
    if (needToRemoveAnElement)
    {
        toRemove.Add(elem);
    }
}

// Remove everything here
myList.RemoveAll(x => toRemove.Contains(x));

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

สงสัยว่าถ้า 'elem' ไม่ใช่ int เราจะไม่สามารถใช้ RemoveAll ได้ด้วยวิธีนี้ที่มีอยู่ในโค้ดตอบกลับที่แก้ไข
User M

22

หากคุณต้องระบุ a List<T>และลบออกจากนั้นฉันขอแนะนำให้ใช้whileloop แทน aforeach

var index = 0;
while (index < myList.Count) {
  if (someCondition(myList[index])) {
    myList.RemoveAt(index);
  } else {
    index++;
  }
}

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

13

ฉันรู้ว่าโพสต์นี้เก่า แต่ฉันคิดว่าจะแบ่งปันสิ่งที่ได้ผลสำหรับฉัน

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

private void ProcessAndRemove(IList<Item> list)
{
    foreach (var item in list.ToList())
    {
        if (item.DeterminingFactor > 10)
        {
            list.Remove(item);
        }
    }
}

ความคิดที่ดีใน ".oList ()"! นักตบหัวธรรมดาและยังใช้งานได้ในกรณีที่คุณไม่ได้ใช้เมธ "ลบ ... ()" ในสต็อกโดยตรง
galaxis

1
ไม่มีประสิทธิภาพมากแม้ว่า
nawfal

8

เมื่อคุณต้องการวนซ้ำผ่านรายการและอาจแก้ไขระหว่างลูปคุณควรใช้ for loop:

for (int i = 0; i < myIntCollection.Count; i++)
{
    if (myIntCollection[i] == 42)
    {
        myIntCollection.Remove(i);
        i--;
    }
}

แน่นอนว่าคุณต้องระวังตัวอย่างเช่นฉันลดลงiเมื่อใดก็ตามที่รายการถูกลบออกมิฉะนั้นเราจะข้ามรายการ (อีกทางเลือกหนึ่งคือย้อนกลับรายการ)

หากคุณมี Linq คุณควรใช้RemoveAllตามที่ dlev แนะนำ


ใช้ได้เฉพาะเมื่อคุณกำลังลบองค์ประกอบปัจจุบันเท่านั้น หากคุณกำลังเอาองค์ประกอบโดยพลการที่คุณจะต้องตรวจสอบว่าดัชนีอยู่ที่ / --iก่อนหรือหลังดัชนีปัจจุบันจะตัดสินใจว่าจะ
CompuChip

คำถามเดิมไม่ได้ระบุชัดเจนว่าต้องรองรับการลบองค์ประกอบอื่นที่ไม่ใช่องค์ประกอบปัจจุบัน @CompuChip คำตอบนี้ไม่ได้เปลี่ยนแปลงตั้งแต่มีการชี้แจง
Palec

@Palec ฉันเข้าใจดังนั้นความคิดเห็นของฉัน
CompuChip

5

ในขณะที่คุณระบุรายการให้เพิ่มรายการที่คุณต้องการเก็บไว้ในรายการใหม่ หลังจากนั้นกำหนดรายการใหม่ให้กับไฟล์myIntCollection

List<int> myIntCollection=new List<int>();
myIntCollection.Add(42);
List<int> newCollection=new List<int>(myIntCollection.Count);

foreach(int i in myIntCollection)
{
    if (i want to delete this)
        ///
    else
        newCollection.Add(i);
}
myIntCollection = newCollection;

2

เพิ่มรหัสให้คุณ:

List<int> myIntCollection=new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);

หากคุณต้องการเปลี่ยนรายการในขณะที่คุณอยู่ใน foreach คุณต้องพิมพ์ .ToList()

foreach(int i in myIntCollection.ToList())
{
    if (i == 42)
       myIntCollection.Remove(96);
    if (i == 25)
       myIntCollection.Remove(42);
}

1

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

    public static IList<T> RemoveAllKeepRemoved<T>(this IList<T> source, Predicate<T> predicate)
    {
        IList<T> removed = new List<T>();
        for (int i = source.Count - 1; i >= 0; i--)
        {
            T item = source[i];
            if (predicate(item))
            {
                removed.Add(item);
                source.RemoveAt(i);
            }
        }
        return removed;
    }

0

เกี่ยวกับ

int[] tmp = new int[myIntCollection.Count ()];
myIntCollection.CopyTo(tmp);
foreach(int i in tmp)
{
    myIntCollection.Remove(42); //The error is no longer here.
}

ใน C # ปัจจุบันสามารถเขียนforeach (int i in myIntCollection.ToArray()) { myIntCollection.Remove(42); }ซ้ำได้List<T>ตามจำนวนที่ระบุได้และสนับสนุนวิธีนี้โดยเฉพาะแม้ใน. NET 2.0
Palec

0

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

private void RemoveItems()
{
    _newList.Clear();

    foreach (var item in _list)
    {
        item.Process();
        if (!item.NeedsRemoving())
            _newList.Add(item);
    }

    var swap = _list;
    _list = _newList;
    _newList = swap;
}

0

แค่คิดว่าฉันจะแบ่งปันวิธีแก้ปัญหาที่คล้ายกันซึ่งฉันจำเป็นต้องลบรายการออกจากรายการขณะประมวลผล

โดยพื้นฐานแล้ว "foreach" ที่จะลบรายการออกจากรายการหลังจากที่ทำซ้ำแล้ว

การทดสอบของฉัน:

var list = new List<TempLoopDto>();
list.Add(new TempLoopDto("Test1"));
list.Add(new TempLoopDto("Test2"));
list.Add(new TempLoopDto("Test3"));
list.Add(new TempLoopDto("Test4"));

list.PopForEach((item) =>
{
    Console.WriteLine($"Process {item.Name}");
});

Assert.That(list.Count, Is.EqualTo(0));

ฉันแก้ไขสิ่งนี้ด้วยวิธีการขยาย "PopForEach" ที่จะดำเนินการแล้วลบรายการออกจากรายการ

public static class ListExtensions
{
    public static void PopForEach<T>(this List<T> list, Action<T> action)
    {
        var index = 0;
        while (index < list.Count) {
            action(list[index]);
            list.RemoveAt(index);
        }
    }
}

หวังว่านี่จะเป็นประโยชน์กับทุกคน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.