Cara cerdas untuk menghapus item dari Daftar <T> saat menghitung di C #

87

Saya memiliki kasus klasik mencoba menghapus item dari koleksi sambil menghitungnya dalam satu lingkaran:

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.
}

Pada awal iterasi setelah perubahan terjadi, an InvalidOperationExceptiondilemparkan, karena enumerator tidak suka ketika koleksi yang mendasarinya berubah.

Saya perlu melakukan perubahan pada koleksi saat melakukan iterasi. Ada banyak pola yang dapat digunakan untuk menghindari hal ini , tetapi tidak satupun dari pola tersebut memiliki solusi yang baik:

  1. Jangan hapus di dalam loop ini, sebagai gantinya simpan "Hapus Daftar" terpisah, yang Anda proses setelah loop utama.

    Ini biasanya merupakan solusi yang baik, tetapi dalam kasus saya, saya perlu item untuk segera pergi sebagai "menunggu" sampai setelah loop utama untuk benar-benar menghapus item mengubah aliran logika kode saya.

  2. Daripada menghapus item, cukup setel bendera pada item dan tandai sebagai tidak aktif. Kemudian tambahkan fungsionalitas pola 1 untuk membersihkan daftar.

    Ini akan berfungsi untuk semua kebutuhan saya, tetapi itu berarti bahwa banyak kode harus diubah untuk memeriksa bendera tidak aktif setiap kali item diakses. Ini terlalu banyak administrasi untuk saya sukai.

  3. Entah bagaimana, gabungkan ide-ide pola 2 dalam kelas yang diturunkan dari List<T>. Superlist ini akan menangani flag tidak aktif, penghapusan objek setelah fakta dan juga tidak akan mengekspos item yang ditandai sebagai tidak aktif untuk konsumen pencacahan. Pada dasarnya, ini hanya merangkum semua ide dari pola 2 (dan selanjutnya pola 1).

    Apakah kelas seperti ini ada? Apakah ada yang punya kode untuk ini? Atau apakah ada cara yang lebih baik?

  4. Saya telah diberi tahu bahwa mengakses myIntCollection.ToArray()alih-alih myIntCollectionakan menyelesaikan masalah dan memungkinkan saya menghapus di dalam loop.

    Ini sepertinya pola desain yang buruk bagi saya, atau mungkin tidak masalah?

Rincian:

  • Daftar akan berisi banyak item dan saya hanya akan menghapus beberapa dari mereka.

  • Di dalam loop, saya akan melakukan semua jenis proses, menambahkan, menghapus, dll., Jadi solusinya harus cukup umum.

  • Item yang perlu saya hapus mungkin bukan item saat ini dalam loop. Misalnya, saya mungkin berada di item 10 dari loop 30 item dan perlu menghapus item 6 atau item 26. Berjalan mundur melalui array tidak akan berfungsi lagi karena ini. ;Hai(

John Stock
sumber
Info yang mungkin berguna untuk orang lain: Kesalahan Hindari Koleksi telah dimodifikasi (enkapsulasi pola 1)
George Duckett
Catatan tambahan: Mencantumkan banyak waktu luang (umumnya O (N), di mana N adalah panjang daftar) memindahkan nilai. Jika akses acak yang efisien benar-benar diperlukan, dimungkinkan untuk mencapai penghapusan di O (log N), menggunakan pohon biner yang seimbang yang menahan jumlah node di subpohon yang akarnya. Ini adalah BST yang kuncinya (indeks dalam urutan) tersirat.
Palec
Silakan lihat jawabannya: stackoverflow.com/questions/7193294/…
Dabbas

Jawaban:

196

Solusi terbaik biasanya menggunakan RemoveAll()metode:

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

Atau, jika Anda ingin elemen tertentu dihapus:

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

Ini mengasumsikan bahwa loop Anda semata-mata ditujukan untuk tujuan penghapusan, tentu saja. Jika Anda memang perlu pemrosesan tambahan, maka metode terbaik biasanya menggunakan foratau whileloop, karena Anda tidak menggunakan enumerator:

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

Mundur memastikan bahwa Anda tidak melewatkan elemen apa pun.

Tanggapan untuk Edit :

Jika Anda akan menghapus elemen yang tampaknya sewenang-wenang, metode termudah mungkin adalah dengan melacak elemen yang ingin Anda hapus, dan kemudian menghapus semuanya sekaligus setelahnya. Sesuatu seperti ini:

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));
dlev
sumber
Mengenai tanggapan Anda: Saya perlu menghapus item secara instan selama pemrosesan item itu, bukan setelah seluruh putaran diproses. Solusi yang saya gunakan adalah NULL semua item yang ingin saya hapus secara instan dan menghapusnya setelahnya. Ini bukan solusi yang ideal karena saya harus memeriksa NULL di semua tempat, tetapi TIDAK berhasil.
John Stock
Bertanya-tanya Jika 'elem' bukan int, maka kita tidak dapat menggunakan RemoveAll karena itu ada pada kode tanggapan yang diedit.
Pengguna M
22

Jika Anda berdua harus menghitung a List<T>dan menghapusnya maka saya sarankan cukup menggunakan whileloop daripada aforeach

var index = 0;
while (index < myList.Count) {
  if (someCondition(myList[index])) {
    myList.RemoveAt(index);
  } else {
    index++;
  }
}
JaredPar
sumber
Ini harus menjadi jawaban yang diterima menurut saya. Hal ini memungkinkan Anda untuk mempertimbangkan item lainnya dalam daftar Anda, tanpa mengulang daftar item yang akan dihapus.
Slvrfn
13

Saya tahu posting ini sudah lama, tetapi saya pikir saya akan membagikan apa yang berhasil untuk saya.

Buat salinan daftar untuk pencacahan, dan kemudian di untuk setiap loop, Anda dapat memproses pada nilai yang disalin, dan menghapus / menambah / apa pun dengan daftar sumber.

private void ProcessAndRemove(IList<Item> list)
{
    foreach (var item in list.ToList())
    {
        if (item.DeterminingFactor > 10)
        {
            list.Remove(item);
        }
    }
}
D-Jones
sumber
Ide bagus di ".ToList ()"! Penampakan kepala yang sederhana, dan juga bekerja dalam kasus di mana Anda tidak menggunakan stok "Hapus ... ()" meths secara langsung.
galaxis
1
Sangat tidak efisien.
nawfal
8

Saat Anda perlu mengulang daftar dan mungkin memodifikasinya selama pengulangan, lebih baik Anda menggunakan perulangan for:

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

Tentu saja Anda harus berhati-hati, misalnya saya mengurangi isetiap kali item dihapus karena jika tidak kami akan melewatkan entri (alternatifnya adalah mundur melalui daftar).

Jika Anda memiliki Linq maka Anda harus menggunakan RemoveAllseperti yang disarankan dlev.

Justin
sumber
Hanya berfungsi ketika Anda menghapus elemen saat ini. Jika Anda menghapus elemen arbitrer, Anda perlu memeriksa apakah indeksnya berada pada / sebelum atau setelah indeks saat ini untuk memutuskan apakah akan --i.
CompuChip
Pertanyaan asli tidak menjelaskan bahwa penghapusan elemen selain yang sekarang harus didukung, @CompuChip. Jawaban ini tidak berubah sejak diklarifikasi.
Palec
@Palec, saya mengerti, maka komentar saya.
CompuChip
5

Saat Anda menghitung daftar, tambahkan yang ingin Anda TETAP ke daftar baru. Setelah itu, tetapkan daftar baru kemyIntCollection

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;
James Curran
sumber
2

Mari tambahkan kode Anda:

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

Jika Anda ingin mengubah daftar saat Anda berada di foreach, Anda harus mengetik .ToList()

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

Bagi mereka yang mungkin membantu, saya menulis metode Ekstensi ini untuk menghapus item yang cocok dengan predikat dan mengembalikan daftar item yang dihapus.

    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;
    }
kvb
sumber
0

Bagaimana tentang

int[] tmp = new int[myIntCollection.Count ()];
myIntCollection.CopyTo(tmp);
foreach(int i in tmp)
{
    myIntCollection.Remove(42); //The error is no longer here.
}
Olaf
sumber
Dalam C # saat ini, ini dapat ditulis ulang foreach (int i in myIntCollection.ToArray()) { myIntCollection.Remove(42); }untuk semua enumerable, dan List<T>secara khusus mendukung metode ini bahkan dalam .NET 2.0.
Palec
0

Jika Anda tertarik dengan kinerja tinggi, Anda dapat menggunakan dua daftar. Hal berikut meminimalkan pengumpulan sampah, memaksimalkan lokalitas memori dan tidak pernah benar-benar menghapus item dari daftar, yang sangat tidak efisien jika itu bukan item terakhir.

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;
}
Will Calderwood
sumber
0

Baru saja membayangkan saya akan membagikan solusi saya untuk masalah serupa di mana saya perlu menghapus item dari daftar saat memprosesnya.

Jadi pada dasarnya "foreach" yang akan menghapus item dari daftar setelah iterasi.

Tes saya:

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));

Saya menyelesaikan ini dengan metode ekstensi "PopForEach" yang akan melakukan tindakan dan kemudian menghapus item dari daftar.

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);
        }
    }
}

Semoga ini bisa membantu siapa saja.

Markus Knappen Johansson
sumber