Cách thông minh để xóa các mục khỏi Danh sách <T> khi liệt kê trong C #


87

Tôi gặp trường hợp cổ điển là cố gắng xóa một mục khỏi bộ sưu tập trong khi liệt kê nó trong một vòng lặp:

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

Khi bắt đầu lặp lại sau khi một thay đổi diễn ra, an InvalidOperationExceptionsẽ được ném ra, vì người điều tra không thích khi tập hợp cơ bản thay đổi.

Tôi cần thực hiện các thay đổi đối với bộ sưu tập trong khi lặp lại. Có nhiều mẫu có thể được sử dụng để tránh điều này , nhưng dường như không có mẫu nào trong số đó có giải pháp tốt:

  1. Không xóa bên trong vòng lặp này, thay vào đó giữ một “Danh sách xóa” riêng biệt mà bạn xử lý sau vòng lặp chính.

    Đây thường là một giải pháp tốt, nhưng trong trường hợp của tôi, tôi cần mục phải biến mất ngay lập tức khi "chờ đợi" cho đến sau vòng lặp chính để thực sự xóa mục sẽ thay đổi luồng logic của mã của tôi.

  2. Thay vì xóa mục, chỉ cần đặt cờ trên mục và đánh dấu mục đó là không hoạt động. Sau đó, thêm chức năng của mẫu 1 để dọn dẹp danh sách.

    Điều này sẽ hoạt động cho tất cả các nhu cầu của tôi, nhưng nó có nghĩa là rất nhiều mã sẽ phải thay đổi để kiểm tra cờ không hoạt động mỗi khi một mục được truy cập. Đây là quá nhiều quản lý đối với ý thích của tôi.

  3. Bằng cách nào đó, kết hợp các ý tưởng của mẫu 2 trong một lớp bắt nguồn từ đó List<T>. Superlist này sẽ xử lý cờ không hoạt động, xóa các đối tượng sau khi thực tế và cũng sẽ không để lộ các mục được đánh dấu là không hoạt động cho người tiêu dùng trong danh sách. Về cơ bản, nó chỉ gói gọn tất cả các ý tưởng của mẫu 2 (và sau đó là mẫu 1).

    Một lớp học như thế này có tồn tại không? Có ai có mã cho điều này? đây có phải là cách tốt hơn không?

  4. Tôi đã được thông báo rằng truy cập myIntCollection.ToArray()thay vì myIntCollectionsẽ giải quyết vấn đề và cho phép tôi xóa bên trong vòng lặp.

    Đây có vẻ là một mẫu thiết kế xấu đối với tôi, hoặc có thể nó ổn?

Chi tiết:

  • Danh sách sẽ chứa nhiều mục và tôi sẽ chỉ xóa một số trong số chúng.

  • Bên trong vòng lặp, tôi sẽ thực hiện tất cả các loại quy trình, thêm, bớt, v.v., vì vậy giải pháp cần phải khá chung chung.

  • Mục mà tôi cần xóa có thể không phải là mục hiện tại trong vòng lặp. Ví dụ: tôi có thể đang ở mục 10 trong vòng lặp 30 mục và cần phải xóa mục 6 hoặc mục 26. Việc đi bộ ngược qua mảng sẽ không còn hoạt động vì điều này. ; o (


Thông tin hữu ích nhất có thể cho người khác: Tránh Bộ sưu tập đã được sửa đổi lỗi (một đóng gói của mô hình 1)
George Duckett

Một lưu ý nhỏ: Danh sách dành nhiều thời gian (thường là O (N), trong đó N là độ dài của danh sách) để di chuyển các giá trị. Nếu thực sự cần truy cập ngẫu nhiên hiệu quả, có thể đạt được số lần xóa trong O (log N), sử dụng cây nhị phân cân bằng chứa số lượng nút trong cây con có gốc của nó. Nó là một BST có khóa (chỉ mục trong chuỗi) được ngụ ý.
Palec

Vui lòng xem câu trả lời: stackoverflow.com/questions/7193294/…
Dabbas

Câu trả lời:


196

Giải pháp tốt nhất thường là sử dụng RemoveAll()phương pháp:

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

Hoặc, nếu bạn cần xóa một số yếu tố nhất định :

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

Điều này giả định rằng vòng lặp của bạn chỉ dành cho mục đích xóa, tất nhiên. Nếu bạn làm cần thiết phải xử lý bổ sung, sau đó phương pháp tốt nhất thường là sử dụng một forhoặc whilevòng lặp, kể từ đó bạn không sử dụng một Enumerator:

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

Quay ngược lại đảm bảo rằng bạn không bỏ qua bất kỳ yếu tố nào.

Phản hồi để chỉnh sửa :

Nếu bạn sắp xóa các phần tử có vẻ tùy ý, thì phương pháp đơn giản nhất có thể là chỉ cần theo dõi các phần tử bạn muốn loại bỏ, rồi xóa tất cả chúng cùng một lúc. Một cái gì đó như thế này:

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

Về phản hồi của bạn: Tôi cần xóa các mục ngay lập tức trong quá trình xử lý mục đó không phải sau khi toàn bộ vòng lặp đã được xử lý. Giải pháp tôi đang sử dụng là NULL bất kỳ mục nào mà tôi muốn xóa ngay lập tức và xóa chúng sau đó. Đây không phải là một giải pháp lý tưởng vì tôi phải kiểm tra NULL khắp nơi, nhưng nó KHÔNG hoạt động.
John Stock

Tự hỏi Nếu 'elem' không phải là int, thì chúng ta không thể sử dụng RemoveAll theo cách đó nó hiện diện trên mã phản hồi đã chỉnh sửa.
Người dùng M

22

Nếu bạn phải liệt kê cả a List<T>và xóa khỏi nó thì tôi khuyên bạn chỉ cần sử dụng một whilevòng lặp thay vìforeach

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

Đây nên là câu trả lời được chấp nhận theo ý kiến ​​của tôi. Điều này cho phép bạn xem xét phần còn lại của các mục trong danh sách của mình mà không cần lặp lại danh sách các mục cần loại bỏ.
Slvrfn

13

Tôi biết bài đăng này đã cũ, nhưng tôi nghĩ tôi sẽ chia sẻ những gì hiệu quả với tôi.

Tạo một bản sao của danh sách để liệt kê, sau đó trong mỗi vòng lặp, bạn có thể xử lý các giá trị đã sao chép và xóa / thêm / bất cứ thứ gì với danh sách nguồn.

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

Ý tưởng tuyệt vời trên ".ToList ()"! Cái tát đầu đơn giản và cũng hoạt động trong trường hợp bạn không sử dụng trực tiếp cổ phiếu "Xóa ... ()".

1
Mặc dù rất kém hiệu quả.
nawfal

8

Khi bạn cần lặp lại một danh sách và có thể sửa đổi nó trong suốt vòng lặp thì tốt hơn hết bạn nên sử dụng vòng lặp for:

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

Tất nhiên bạn phải cẩn thận, ví dụ như tôi giảm ibất cứ khi nào một mục bị xóa vì nếu không chúng tôi sẽ bỏ qua các mục nhập (một cách thay thế là quay ngược lại mặc dù danh sách).

Nếu bạn có Linq thì bạn chỉ nên sử dụng RemoveAllnhư dlev đã đề xuất.


Chỉ hoạt động khi bạn đang xóa phần tử hiện tại. Nếu bạn đang xóa một phần tử tùy ý, bạn sẽ cần phải kiểm tra xem chỉ mục của nó ở / trước hay sau chỉ mục hiện tại để quyết định xem có nên hay không --i.
CompuChip

Câu hỏi ban đầu không làm rõ rằng việc xóa các phần tử khác với phần tử hiện tại phải được hỗ trợ, @CompuChip. Câu trả lời này không thay đổi kể từ khi nó được làm rõ.
Palec

@Palec, tôi hiểu, do đó nhận xét của tôi.
CompuChip

5

Khi bạn liệt kê danh sách, hãy thêm danh sách bạn muốn GIỮ vào danh sách mới. Sau đó, chỉ định danh sách mới chomyIntCollection

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;

3

Hãy thêm mã cho bạn:

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

Nếu bạn muốn thay đổi danh sách trong khi bạn đang ở gần, bạn phải nhập .ToList()

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

1

Đối với những thứ có thể hữu ích, tôi đã viết phương thức Mở rộng này để loại bỏ các mục khớp với vị từ và trả lại danh sách các mục đã loại bỏ.

    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

Làm thế nào về

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

Trong C # hiện tại, nó có thể được viết lại như foreach (int i in myIntCollection.ToArray()) { myIntCollection.Remove(42); }đối với bất kỳ kiểu liệt kê nào và List<T>đặc biệt hỗ trợ phương pháp này ngay cả trong .NET 2.0.
Palec

0

Nếu bạn quan tâm đến hiệu suất cao, bạn có thể sử dụng hai danh sách. Phần sau giảm thiểu việc thu gom rác, tối đa hóa vị trí bộ nhớ và không bao giờ thực sự xóa một mục khỏi danh sách, điều này rất kém hiệu quả nếu đó không phải là mục cuối cùng.

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

Chỉ cần tìm, tôi sẽ chia sẻ giải pháp của mình cho một vấn đề tương tự mà tôi cần xóa các mục khỏi danh sách trong khi xử lý chúng.

Vì vậy, về cơ bản "foreach" sẽ xóa mục khỏi danh sách sau khi nó đã được lặp lại.

Bài kiểm tra của tôi:

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

Tôi đã giải quyết vấn đề này bằng một phương pháp mở rộng "PopForEach" sẽ thực hiện một hành động và sau đó xóa mục khỏi danh sách.

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

Hy vọng điều này có thể hữu ích cho bất kỳ ai.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.