Bagaimana cara menghapus elemen dari daftar umum saat iterasi di atasnya?

451

Saya mencari pola yang lebih baik untuk bekerja dengan daftar elemen yang masing-masing perlu diproses dan kemudian tergantung pada hasil yang dihapus dari daftar.

Anda tidak dapat menggunakan .Remove(element)di dalam foreach (var element in X)(karena itu menghasilkan Collection was modified; enumeration operation may not execute.pengecualian) ... Anda juga tidak dapat menggunakan for (int i = 0; i < elements.Count(); i++)dan .RemoveAt(i)karena itu mengganggu posisi Anda saat ini dalam koleksi relatif terhadap i.

Apakah ada cara yang elegan untuk melakukan ini?

InvertedAcceleration
sumber

Jawaban:

734

Iterasi daftar Anda secara terbalik dengan for for:

for (int i = safePendingList.Count - 1; i >= 0; i--)
{
    // some code
    // safePendingList.RemoveAt(i);
}

Contoh:

var list = new List<int>(Enumerable.Range(1, 10));
for (int i = list.Count - 1; i >= 0; i--)
{
    if (list[i] > 5)
        list.RemoveAt(i);
}
list.ForEach(i => Console.WriteLine(i));

Sebagai alternatif, Anda dapat menggunakan metode RemoveAll dengan predikat untuk menguji:

safePendingList.RemoveAll(item => item.Value == someValue);

Berikut ini contoh sederhana untuk diperagakan:

var list = new List<int>(Enumerable.Range(1, 10));
Console.WriteLine("Before:");
list.ForEach(i => Console.WriteLine(i));
list.RemoveAll(i => i > 5);
Console.WriteLine("After:");
list.ForEach(i => Console.WriteLine(i));
Ahmad Mageed
sumber
19
Bagi mereka yang datang dari Jawa, Daftar C # seperti ArrayList dalam penyisipan / penghapusan adalah O (n) dan pengambilan melalui indeks adalah O (1). Ini bukan daftar tertaut tradisional. Tampaknya agak disayangkan C # menggunakan kata "Daftar" untuk menggambarkan struktur data ini karena mengingatkan daftar tertaut klasik.
Jarrod Smith
79
Tidak ada dalam nama 'Daftar' yang mengatakan 'LinkedList'. Orang-orang yang datang dari bahasa lain selain Jawa mungkin bingung ketika itu akan menjadi daftar yang ditautkan.
GolezTrol
5
Saya berakhir di sini melalui pencarian vb.net jadi jika ada yang menginginkan sintaks setara vb.net untuk RemoveAll:list.RemoveAll(Function(item) item.Value = somevalue)
pseudocoder
1
Ini juga berfungsi di Jawa dengan modifikasi minimal: .size()bukannya .Countdan .remove(i)bukannya .removeAt(i). Pintar - terima kasih!
Autumn Leonard
2
Saya membuat tes kecil untuk kinerja dan ternyata itu RemoveAll()membutuhkan waktu tiga kali lebih banyak daripada forloop mundur . Jadi saya pasti tetap dengan loop, setidaknya di bagian yang penting.
Crusha K. Rool
84

Solusi sederhana dan mudah:

Gunakan standar untuk loop yang berjalan mundur pada koleksi Anda dan RemoveAt(i)untuk menghapus elemen.

Jan
sumber
2
Perlu diketahui bahwa menghapus item satu per satu tidak efisien jika daftar Anda mengandung banyak item. Ini berpotensi menjadi O (n ^ 2). Bayangkan sebuah Daftar dengan dua miliar item di mana kebetulan bahwa miliar item pertama semuanya pada akhirnya dihapus. Setiap penghapusan memaksa semua item nantinya untuk disalin, sehingga Anda akhirnya menyalin satu miliar item masing-masing satu kali. Ini bukan karena iterasi terbalik, itu karena penghapusan satu per satu. RemoveAll memastikan bahwa setiap item disalin paling banyak satu kali sehingga linear. Penghapusan satu per satu waktu mungkin satu miliar kali lebih lambat. O (n) versus O (n ^ 2).
Bruce Dawson
71

Balikkan iterasi harus menjadi hal pertama yang terlintas dalam pikiran ketika Anda ingin menghapus elemen dari Koleksi saat iterasi.

Untungnya, ada solusi yang lebih elegan daripada menulis for for loop yang melibatkan pengetikan yang tidak perlu dan bisa rentan kesalahan.

ICollection<int> test = new List<int>(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10});

foreach (int myInt in test.Reverse<int>())
{
    if (myInt % 2 == 0)
    {
        test.Remove(myInt);
    }
}
jedesah
sumber
7
Ini bekerja dengan baik untuk saya. Sederhana, elegan, dan membutuhkan sedikit perubahan pada kode saya.
Stephen MacDougall
1
Apakah ini genius flippin saja? Dan saya setuju dengan @StephenMacDougall, saya tidak perlu menggunakan C ++ 'y untuk loop dan melanjutkan dengan LINQ sebagaimana dimaksud.
Piotr Kula
5
Saya tidak melihat keuntungan dari foreach sederhana (int myInt in test.ToList ()) {if (myInt% 2 == 0) {test.Remove (myInt); }} Anda masih harus mengalokasikan salinan untuk Reverse dan memperkenalkan Huh? saat - mengapa ada Reverse.
Jahav
11
@ jedesah Ya, Reverse<T>()membuat iterator yang menelusuri daftar mundur, tetapi mengalokasikan buffer tambahan dengan ukuran yang sama dengan daftar itu sendiri untuk itu ( Referenceource.microsoft.com/#System.Core/System/Linq/… ). Reverse<T>tidak hanya melalui daftar asli dalam urutan terbalik (tanpa mengalokasikan memori tambahan). Karena itu keduanya ToList()dan Reverse()memiliki konsumsi memori yang sama (keduanya membuat salinan), tetapi ToList()tidak melakukan apa pun untuk data. Dengan Reverse<int>(), saya akan bertanya-tanya mengapa daftar dibalik, untuk alasan apa.
jahav
1
@jahav, saya mengerti maksud Anda. Itu cukup mengecewakan bahwa implementasi Reverse<T>()menciptakan buffer baru, saya tidak yakin saya mengerti mengapa itu perlu. Sepertinya saya bahwa tergantung pada struktur yang mendasari Enumerable, setidaknya dalam beberapa kasus mungkin untuk mencapai iterasi terbalik tanpa mengalokasikan memori linier.
jedesah
68
 foreach (var item in list.ToList()) {
     list.Remove(item);
 }

Jika Anda menambahkan ".ToList ()" ke daftar Anda (atau hasil kueri LINQ), Anda dapat menghapus "item" secara langsung dari "daftar" tanpa " Koleksi yang ditakuti telah diubah; operasi penghitungan tidak dapat dijalankan ." kesalahan. Kompiler membuat salinan "daftar", sehingga Anda dapat dengan aman melakukan penghapusan pada array.

Meskipun pola ini tidak sangat efisien, ia memiliki nuansa alami dan cukup fleksibel untuk hampir semua situasi . Seperti ketika Anda ingin menyimpan setiap "item" ke DB dan menghapusnya dari daftar hanya ketika penyimpanan DB berhasil.

Greg Little
sumber
6
Ini adalah solusi terbaik jika efisiensi tidak penting.
IvanP
2
ini lebih cepat dan lebih mudah dibaca: list.RemoveAll (i => true);
santiago arizti
1
@ Greg Little, apakah saya mengerti Anda dengan benar - ketika Anda menambahkan ToList () kompiler melewati koleksi yang disalin tetapi menghapus dari yang asli?
Pyrejkee
Jika daftar Anda berisi item duplikat dan Anda hanya ingin menghapus item yang terjadi kemudian dalam daftar maka bukankah ini akan menghapus item yang salah?
Tim MB
Apakah ini juga berfungsi untuk daftar objek?
Tom el Safadi
23

Pilih elemen yang Anda lakukan inginkan daripada mencoba untuk menghapus elemen yang Anda tidak inginkan. Ini jauh lebih mudah (dan umumnya juga lebih efisien) daripada menghapus elemen.

var newSequence = (from el in list
                   where el.Something || el.AnotherThing < 0
                   select el);

Saya ingin memposting ini sebagai komentar untuk menanggapi komentar yang ditinggalkan oleh Michael Dillon di bawah ini, tetapi terlalu panjang dan mungkin berguna untuk menjawab:

Secara pribadi, saya tidak akan pernah menghapus item satu-per-satu, jika Anda memang perlu dihapus, panggil RemoveAllyang mengambil predikat dan hanya mengatur ulang array internal satu kali, sedangkan Removemelakukan Array.Copyoperasi untuk setiap elemen yang Anda hapus. RemoveAlljauh lebih efisien.

Dan ketika Anda mundur mengulangi daftar, Anda sudah memiliki indeks elemen yang ingin Anda hapus, jadi akan jauh lebih efisien untuk memanggil RemoveAt, karena Removepertama-tama lakukan traversal daftar untuk menemukan indeks elemen yang Anda inginkan. Sedang mencoba untuk menghapus, tetapi Anda sudah tahu indeks itu.

Jadi semuanya, saya tidak melihat alasan untuk memanggil Removefor-loop. Dan idealnya, jika memungkinkan, gunakan kode di atas untuk mengalirkan elemen dari daftar sesuai kebutuhan sehingga tidak ada struktur data kedua yang harus dibuat sama sekali.

JulianR
sumber
1
Entah ini, atau tambahkan pointer ke elemen yang tidak diinginkan ke dalam daftar kedua, kemudian setelah loop Anda berakhir, ulangi daftar penghapusan dan gunakan itu untuk menghapus elemen.
Michael Dillon
22

Menggunakan ToArray () pada daftar generik memungkinkan Anda untuk melakukan Hapus (item) pada Daftar generik Anda:

        List<String> strings = new List<string>() { "a", "b", "c", "d" };
        foreach (string s in strings.ToArray())
        {
            if (s == "b")
                strings.Remove(s);
        }
Etienne Brouillard
sumber
2
Ini tidak salah, tetapi saya harus menunjukkan bahwa ini memotong kebutuhan untuk membuat daftar "penyimpanan" kedua dari item yang ingin Anda hapus dengan mengorbankan menyalin seluruh daftar ke array. Daftar elemen pilihan kedua mungkin akan memiliki lebih sedikit item.
Ahmad Mageed
20

Menggunakan .ToList () akan membuat salinan daftar Anda, seperti yang dijelaskan dalam pertanyaan ini: ToList () - Apakah itu Membuat Daftar Baru?

Dengan menggunakan ToList (), Anda dapat menghapus dari daftar asli Anda, karena Anda benar-benar mengulangi salinan.

foreach (var item in listTracked.ToList()) {    

        if (DetermineIfRequiresRemoval(item)) {
            listTracked.Remove(item)
        }

     }
StuartQ
sumber
1
Tetapi dari sudut pandang kinerja Anda menyalin daftar Anda, yang mungkin memerlukan waktu. Cara yang baik dan mudah untuk melakukannya, tetapi dengan kinerja yang tidak begitu baik
Florian K
12

Jika fungsi yang menentukan item mana yang akan dihapus tidak memiliki efek samping dan tidak bermutasi item (itu adalah fungsi murni), solusi sederhana (efisien waktu linear) adalah:

list.RemoveAll(condition);

Jika ada efek samping, saya akan menggunakan sesuatu seperti:

var toRemove = new HashSet<T>();
foreach(var item in items)
{
     ...
     if(condition)
          toRemove.Add(item);
}
items.RemoveAll(toRemove.Contains);

Ini masih waktu linier, dengan asumsi hash baik. Tetapi memiliki peningkatan penggunaan memori karena hashset.

Akhirnya jika daftar Anda hanya merupakan IList<T>bukan List<T>saya sarankan jawaban saya untuk Bagaimana saya bisa melakukan iterator foreach khusus ini? . Ini akan memiliki runtime linear yang diberikan implementasi tipikal IList<T>, dibandingkan dengan runtime kuadrat dari banyak jawaban lainnya.

CodesInChaos
sumber
11

Karena semua pemindahan dilakukan dengan syarat Anda dapat menggunakan

list.RemoveAll(item => item.Value == someValue);
Ahmad
sumber
Itu solusi terbaik jika pemrosesan tidak bermutasi item dan tidak memiliki efek samping.
CodesInChaos
9
List<T> TheList = new List<T>();

TheList.FindAll(element => element.Satisfies(Condition)).ForEach(element => TheList.Remove(element));
Mauricio Ramalho
sumber
8

Anda tidak dapat menggunakan foreach, tetapi Anda bisa mengulanginya ke depan dan mengelola variabel indeks loop Anda saat Anda menghapus item, seperti:

for (int i = 0; i < elements.Count; i++)
{
    if (<condition>)
    {
        // Decrement the loop counter to iterate this index again, since later elements will get moved down during the remove operation.
        elements.RemoveAt(i--);
    }
}

Perhatikan bahwa secara umum semua teknik ini bergantung pada perilaku koleksi yang diulang. Teknik yang ditunjukkan di sini akan bekerja dengan Daftar standar (T). (Hal ini sangat mungkin untuk menulis kelas koleksi Anda sendiri dan iterator yang tidak memungkinkan penghapusan item yang selama loop foreach.)

yoyo
sumber
6

Menggunakan Removeatau RemoveAtpada daftar sambil mengulangi daftar itu sengaja dibuat sulit, karena hampir selalu merupakan hal yang salah untuk dilakukan . Anda mungkin bisa membuatnya bekerja dengan beberapa trik pintar, tetapi itu akan sangat lambat. Setiap kali Anda menyebutnya Removeharus memindai seluruh daftar untuk menemukan elemen yang ingin Anda hapus. Setiap kali Anda menyebutnya RemoveAtharus memindahkan elemen 1 posisi berikutnya ke kiri. Dengan demikian, solusi apa pun yang menggunakan Removeatau RemoveAt, akan membutuhkan waktu kuadratik, O (n²) .

Gunakan RemoveAlljika Anda bisa. Jika tidak, pola berikut akan memfilter daftar di tempat dalam waktu linier, O (n) .

// Create a list to be filtered
IList<int> elements = new List<int>(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
// Filter the list
int kept = 0;
for (int i = 0; i < elements.Count; i++) {
    // Test whether this is an element that we want to keep.
    if (elements[i] % 3 > 0) {
        // Add it to the list of kept elements.
        elements[kept] = elements[i];
        kept++;
    }
}
// Unfortunately IList has no Resize method. So instead we
// remove the last element of the list until: elements.Count == kept.
while (kept < elements.Count) elements.RemoveAt(elements.Count-1);
bcmpinc
sumber
3

Saya berharap "pola" itu seperti ini:

foreach( thing in thingpile )
{
    if( /* condition#1 */ )
    {
        foreach.markfordeleting( thing );
    }
    elseif( /* condition#2 */ )
    {
        foreach.markforkeeping( thing );
    }
} 
foreachcompleted
{
    // then the programmer's choices would be:

    // delete everything that was marked for deleting
    foreach.deletenow(thingpile); 

    // ...or... keep only things that were marked for keeping
    foreach.keepnow(thingpile);

    // ...or even... make a new list of the unmarked items
    others = foreach.unmarked(thingpile);   
}

Ini akan menyelaraskan kode dengan proses yang berlangsung di otak programmer.

warrens
sumber
1
Cukup mudah. Cukup buat array boolean flag (gunakan tipe 3-negara, mis. Nullable<bool>, Jika Anda ingin mengizinkan yang tidak ditandai), kemudian gunakan setelah pendahuluan untuk menghapus / menyimpan item.
Dan Bechard
3

Dengan mengasumsikan bahwa predikat adalah properti Boolean dari suatu elemen, bahwa jika itu benar, maka elemen tersebut harus dihapus:

        int i = 0;
        while (i < list.Count())
        {
            if (list[i].predicate == true)
            {
                list.RemoveAt(i);
                continue;
            }
            i++;
        }
roeesha
sumber
Saya memberikan ini upvote karena kadang-kadang bisa lebih efisien untuk bergerak dalam Daftar secara berurutan (bukan urutan terbalik). Mungkin Anda bisa berhenti ketika menemukan item pertama tidak dihapus karena daftar dipesan. (Bayangkan sebuah 'break' di mana i ++ berada dalam contoh ini.
FrankKrumnow
3

Saya akan menetapkan ulang daftar dari kueri LINQ yang memfilter elemen yang tidak ingin Anda pertahankan.

list = list.Where(item => ...).ToList();

Kecuali jika daftarnya sangat besar, seharusnya tidak ada masalah kinerja yang signifikan dalam melakukan ini.

Martin Liversage
sumber
3

Cara terbaik untuk menghapus item dari daftar sambil mengulanginya adalah dengan menggunakannya RemoveAll(). Tetapi perhatian utama yang ditulis oleh orang-orang adalah bahwa mereka harus melakukan beberapa hal kompleks di dalam loop dan / atau memiliki kasus perbandingan yang kompleks.

Solusinya adalah tetap menggunakan RemoveAll()tetapi gunakan notasi ini:

var list = new List<int>(Enumerable.Range(1, 10));
list.RemoveAll(item => 
{
    // Do some complex operations here
    // Or even some operations on the items
    SomeFunction(item);
    // In the end return true if the item is to be removed. False otherwise
    return item > 5;
});
Hüseyin Yağlı
sumber
2
foreach(var item in list.ToList())

{

if(item.Delete) list.Remove(item);

}

Cukup buat daftar yang sama sekali baru dari yang pertama. Saya mengatakan "Mudah" daripada "Benar" karena membuat daftar yang sama sekali baru mungkin datang dengan kinerja yang lebih tinggi dari metode sebelumnya (saya belum peduli dengan tolok ukur apa pun.) Saya biasanya lebih suka pola ini, ini juga dapat berguna dalam mengatasi Batasan Linq-To-Entities.

for(i = list.Count()-1;i>=0;i--)

{

item=list[i];

if (item.Delete) list.Remove(item);

}

Dengan cara ini, siklus melalui daftar mundur dengan loop tua For polos. Melakukan ini ke depan bisa bermasalah jika ukuran koleksi berubah, tetapi mundur harus selalu aman.

Lijo
sumber
1

Salin daftar yang sedang Anda iterasi. Kemudian hapus dari salinan dan interate yang asli. Mundur adalah membingungkan dan tidak berfungsi dengan baik saat perulangan paralel.

var ids = new List<int> { 1, 2, 3, 4 };
var iterableIds = ids.ToList();

Parallel.ForEach(iterableIds, id =>
{
    ids.Remove(id);
});
Timothy Gonzalez
sumber
1

Saya akan melakukan ini

using System.IO;
using System;
using System.Collections.Generic;

class Author
    {
        public string Firstname;
        public string Lastname;
        public int no;
    }

class Program
{
    private static bool isEven(int i) 
    { 
        return ((i % 2) == 0); 
    } 

    static void Main()
    {    
        var authorsList = new List<Author>()
        {
            new Author{ Firstname = "Bob", Lastname = "Smith", no = 2 },
            new Author{ Firstname = "Fred", Lastname = "Jones", no = 3 },
            new Author{ Firstname = "Brian", Lastname = "Brains", no = 4 },
            new Author{ Firstname = "Billy", Lastname = "TheKid", no = 1 }
        };

        authorsList.RemoveAll(item => isEven(item.no));

        foreach(var auth in authorsList)
        {
            Console.WriteLine(auth.Firstname + " " + auth.Lastname);
        }
    }
}

KELUARAN

Fred Jones
Billy TheKid
Gambitier
sumber
0

Saya menemukan diri saya dalam situasi yang sama di mana aku harus menghapus setiap n th elemen dalam diberikan List<T>.

for (int i = 0, j = 0, n = 3; i < list.Count; i++)
{
    if ((j + 1) % n == 0) //Check current iteration is at the nth interval
    {
        list.RemoveAt(i);
        j++; //This extra addition is necessary. Without it j will wrap
             //down to zero, which will throw off our index.
    }
    j++; //This will always advance the j counter
}
Paul Nelson Baker
sumber
0

Biaya menghapus item dari daftar sebanding dengan jumlah item setelah item dihapus. Dalam kasus di mana paruh pertama item memenuhi syarat untuk dihapus, pendekatan apa pun yang didasarkan pada penghapusan item secara terpisah akan berakhir dengan melakukan sekitar N * N / 4 operasi penyalinan item, yang bisa menjadi sangat mahal jika daftar besar .

Pendekatan yang lebih cepat adalah memindai melalui daftar untuk menemukan item pertama yang akan dihapus (jika ada), dan kemudian mulai dari itu salin setiap item yang harus disimpan ke tempat di mana barang itu berada. Setelah ini selesai, jika item R harus dipertahankan, item R pertama dalam daftar akan menjadi item R tersebut, dan semua item yang membutuhkan penghapusan akan berada di akhir. Jika item-item itu dihapus dalam urutan terbalik, sistem tidak akan akhirnya harus menyalin salah satu dari mereka, jadi jika daftar memiliki N item yang item R, termasuk semua F pertama, dipertahankan, perlu untuk salin item RF, dan menyusutkan daftar dengan satu item NR kali. Semua waktu linier.

supercat
sumber
0

Pendekatan saya adalah saya pertama kali membuat daftar indeks, yang seharusnya dihapus. Setelah itu saya mengulangi indeks dan menghapus item dari daftar awal. Ini terlihat seperti ini:

var messageList = ...;
// Restrict your list to certain criteria
var customMessageList = messageList.FindAll(m => m.UserId == someId);

if (customMessageList != null && customMessageList.Count > 0)
{
    // Create list with positions in origin list
    List<int> positionList = new List<int>();
    foreach (var message in customMessageList)
    {
        var position = messageList.FindIndex(m => m.MessageId == message.MessageId);
        if (position != -1)
            positionList.Add(position);
    }
    // To be able to remove the items in the origin list, we do it backwards
    // so that the order of indices stays the same
    positionList = positionList.OrderByDescending(p => p).ToList();
    foreach (var position in positionList)
    {
        messageList.RemoveAt(position);
    }
}
pengujian
sumber
0

Dalam C # satu cara mudah adalah dengan menandai yang ingin Anda hapus kemudian buat daftar baru untuk beralih ke ...

foreach(var item in list.ToList()){if(item.Delete) list.Remove(item);}  

atau bahkan menggunakan linq ....

list.RemoveAll(p=>p.Delete);

tetapi perlu dipertimbangkan jika tugas atau utas lain akan memiliki akses ke daftar yang sama pada saat yang sama Anda sibuk menghapus, dan mungkin menggunakan ConcurrentList sebagai gantinya.

andrew pate
sumber
0

Lacak elemen yang akan dihapus dengan properti, dan hapus semuanya setelah proses.

using System.Linq;

List<MyProperty> _Group = new List<MyProperty>();
// ... add elements

bool cond = true;
foreach (MyProperty currObj in _Group)
{
    if (cond) 
    {
        // SET - element can be deleted
        currObj.REMOVE_ME = true;
    }
}
// RESET
_Group.RemoveAll(r => r.REMOVE_ME);
Roberto Mutti
sumber
0

Hanya ingin menambahkan 2 sen saya untuk ini kalau-kalau ini membantu siapa pun, saya punya masalah yang sama tetapi perlu menghapus beberapa elemen dari daftar array saat itu sedang diulangi. jawaban tervvatif tertinggi melakukannya untuk saya sebagian besar sampai saya mengalami kesalahan dan menyadari bahwa indeks lebih besar dari ukuran daftar array dalam beberapa kasus karena beberapa elemen dihapus tetapi indeks loop tidak tetap melacak itu. Saya memperbaikinya dengan cek sederhana:

ArrayList place_holder = new ArrayList();
place_holder.Add("1");
place_holder.Add("2");
place_holder.Add("3");
place_holder.Add("4");

for(int i = place_holder.Count-1; i>= 0; i--){
    if(i>= place_holder.Count){
        i = place_holder.Count-1; 
    }

// some method that removes multiple elements here
}
Sasha1296
sumber
-4
myList.RemoveAt(i--);

simples;
SW
sumber
1
Apa yang simples;dilakukan di sini?
Denny
6
itu delegasi .. itu menurunkan jawaban saya setiap kali berjalan
SW
SW ROFL! Harus menyukai komentar Anda.
Erick Brown