C # 'da numaralandırırken List <T>' den öğeleri kaldırmanın akıllı yolu


87

Bir öğeyi bir döngüde numaralandırırken bir koleksiyondan kaldırmaya çalışmak gibi klasik bir durum var:

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

Bir değişiklik gerçekleştikten sonra yinelemenin başında bir InvalidOperationExceptionatılır, çünkü numaralandırıcılar temeldeki koleksiyonun değişmesinden hoşlanmaz.

Yineleme sırasında koleksiyonda değişiklik yapmam gerekiyor. Bundan kaçınmak için kullanılabilecek birçok model var , ancak hiçbirinin iyi bir çözümü yok gibi görünüyor:

  1. Bu döngünün içini silmeyin, bunun yerine ana döngüden sonra işlediğiniz ayrı bir "Silme Listesi" tutun.

    Bu normalde iyi bir çözümdür, ancak benim durumumda, öğeyi gerçekten silmek için ana döngüden kodumun mantık akışını değiştirene kadar öğenin anında "bekleyerek" gitmesine ihtiyacım var.

  2. Öğeyi silmek yerine, öğe üzerinde bir bayrak ayarlayın ve onu devre dışı olarak işaretleyin. Ardından listeyi temizlemek için desen 1'in işlevselliğini ekleyin.

    Bu , tüm ihtiyaçlarım için işe yarar, ancak bu, bir öğeye her erişildiğinde etkin olmayan bayrağı kontrol etmek için birçok kodun değiştirilmesi gerektiği anlamına gelir . Bu benim zevkime göre çok fazla idare.

  3. Bir şekilde model 2'nin fikirlerini bir sınıfa dahil edin List<T>. Bu Süper Liste, etkin olmayan bayrağı, nesnelerin olaydan sonra silinmesini ele alacak ve ayrıca etkin olmayan olarak işaretlenen öğeleri numaralandırma tüketicilerine göstermeyecektir. Temel olarak, sadece 2. modelin (ve ardından 1. modelin) tüm fikirlerini kapsamaktadır.

    Bunun gibi bir sınıf var mı? Bunun için kodu olan var mı? Yoksa daha iyi bir yol var mı?

  4. Bunun myIntCollection.ToArray()yerine erişimin myIntCollectionsorunu çözeceği ve döngü içinde silmeme izin vereceği söylendi .

    Bu bana kötü bir tasarım kalıbı gibi geliyor, yoksa sorun olabilir mi?

Detaylar:

  • Liste birçok öğe içerecek ve sadece bazılarını kaldıracağım.

  • Döngünün içinde, her türlü işlemi yapıyor olacağım, ekleyeceğim, çıkaracağım, bu yüzden çözümün oldukça genel olması gerekiyor.

  • Silmem gereken öğe döngüdeki geçerli öğe olmayabilir . Örneğin, 30 maddelik bir döngünün 10. maddesinde olabilirim ve madde 6 ya da madde 26'yı kaldırmam gerekebilir. Bu nedenle, dizi içinde geriye doğru yürümek artık işe yaramayacak. ;Ö(


Başkası için olası yararlı bilgiler: Koleksiyondan Kaçın, değiştirildi hata (model 1'in kapsüllenmesi)
George Duckett

Bir yan not: Değerleri hareket ettiren listeler (genellikle O (N), burada N listenin uzunluğudur). Etkili rastgele erişim gerçekten gerekliyse, kökü olan alt ağaçtaki düğümlerin sayısını tutan dengeli bir ikili ağaç kullanarak O (log N) 'de silmeler elde etmek mümkündür. Bu, anahtarı (dizideki dizin) ima edilen bir BST'dir.
Palec

Yanıtlar:


196

En iyi çözüm genellikle RemoveAll()yöntemi kullanmaktır :

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

Veya belirli öğelerin kaldırılmasına ihtiyacınız varsa :

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

Bu, elbette döngünüzün yalnızca kaldırma amaçlı olduğunu varsayar. Eğer varsa bunu ek bir işleme gerek, o zaman en iyi yöntem bir kullanımı genellikle forya whileo zamandan beri bir sýralayýcý kullanmıyorsanız, döngü:

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

Geriye doğru gitmek, hiçbir öğeyi atlamamanızı sağlar.

Düzenlemeye Yanıt :

Görünüşe göre rastgele öğeleri kaldıracaksanız, en kolay yöntem, kaldırmak istediğiniz öğelerin kaydını tutmak ve ardından hepsini bir kerede kaldırmak olabilir. Bunun gibi bir şey:

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

Cevabınız ile ilgili olarak: Tüm döngü işlendikten sonra değil, o öğenin işlenmesi sırasında öğeleri anında kaldırmam gerekiyor. Kullandığım çözüm, anında silmek istediğim tüm öğeleri NULL yapmak ve daha sonra silmek. Bu ideal bir çözüm değil, çünkü her yerde NULL olup olmadığını kontrol etmem gerekiyor, ancak İŞE ÇALIŞIYOR.
John Stok

'Elem' bir int değilse merak ediyorsanız, bu durumda RemoveAll'ı, düzenlenen yanıt kodunda mevcut olduğu şekilde kullanamayız.
Kullanıcı M

22

Hem a'yı numaralandırmanız hem de List<T>ondan çıkarmanız gerekiyorsa, o zaman basitçe a whileyerine bir döngü kullanmanızı öneririm .foreach

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

Bence bu kabul edilen cevap olmalı. Bu, listenizdeki diğer öğeleri, kaldırılacak öğelerin bir kısa listesini yeniden yinelemeden değerlendirmenize olanak tanır.
Slvrfn

13

Bu gönderinin eski olduğunu biliyorum ama benim için işe yarayan şeyleri paylaşacağımı düşündüm.

Numaralandırma için listenin bir kopyasını oluşturun ve ardından her döngü için kopyalanan değerleri işleyebilir ve kaynak listesiyle kaldır / ekle / ne varsa kaldırabilirsiniz.

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

".ToList ()" hakkında harika fikir! Basit kafa vurucu ve ayrıca hisse senedi "Kaldır ... ()" meths doğrudan kullanmadığınız durumlarda da işe yarar.
galaxis

1
Yine de çok verimsiz.
nawfal

8

Bir listeyi yinelemeniz gerektiğinde ve döngü sırasında onu değiştirmeniz gerektiğinde, bir for döngüsü kullanmanız daha iyi olur:

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

Tabii ki dikkatli olmalısınız, örneğin ibir öğe kaldırıldığında azaltıyorum, aksi takdirde girişleri atlayacağız (bir alternatif listeden geriye gitmektir).

Eğer Linq varsa o zaman sadece kullanmalısınız RemoveAlldlev önerdi olarak.


Ancak, yalnızca geçerli öğeyi kaldırırken çalışır. Rasgele bir öğeyi kaldırıyorsanız, dizininin geçerli dizinden önce mi yoksa sonra mı olduğunu kontrol edip karar vermeniz gerekir --i.
CompuChip

Orijinal soru, mevcut öğe dışındaki diğer öğelerin silinmesinin desteklenmesi gerektiğini açıkça ortaya koymadı, @CompuChip. Bu cevap netleştiğinden beri değişmedi.
Palec

@Palec, anlıyorum, dolayısıyla benim yorumum.
CompuChip

5

Listeyi numaralandırırken, SAKLAMAK istediğinizi yeni bir listeye ekleyin. Daha sonra, yeni listeyimyIntCollection

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

Size kodu ekleyelim:

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

Bir foreach'teyken listeyi değiştirmek isterseniz, yazmalısınız .ToList()

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

1

Yardımcı olabileceği için, yüklemle eşleşen öğeleri kaldırmak ve kaldırılan öğelerin listesini döndürmek için bu Uzantı yöntemini yazdım.

    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

Ne dersin

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

Geçerli C # 'da, foreach (int i in myIntCollection.ToArray()) { myIntCollection.Remove(42); }numaralandırılabilir herhangi bir şekilde yeniden yazılabilir ve List<T>özellikle bu yöntemi .NET 2.0'da bile destekler.
Palec

0

Yüksek performansla ilgileniyorsanız, iki liste kullanabilirsiniz. Aşağıdakiler, çöp toplamayı en aza indirir, bellek yerelliğini en üst düzeye çıkarır ve bir öğeyi bir listeden asla çıkarmaz; bu, son öğe değilse çok verimsizdir.

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

Sadece, işlerken bir listeden öğeleri kaldırmam gereken benzer bir soruna çözümümü paylaşacağımı düşündüm.

Yani temelde "foreach", öğeyi yinelendikten sonra listeden kaldıracaktır.

Benim testim:

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

Bunu, bir eylem gerçekleştirecek ve ardından öğeyi listeden kaldıracak bir uzantı yöntemi "PopForEach" ile çözdüm.

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

Umarım bu herhangi birine yardımcı olabilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.