CodeGym /Các khóa học /C# SELF /Xoá phần tử khỏi collection trong vòng lặp

Xoá phần tử khỏi collection trong vòng lặp foreach

C# SELF
Mức độ , Bài học
Có sẵn

1. Giới thiệu

Gần như ai mới học lập trình C# cũng sẽ sớm gặp một vấn đề giống nhau: có một collection (ví dụ, một list object), và cần xoá các phần tử không cần thiết theo điều kiện nào đó. Nghe thì đơn giản, và tay bạn sẽ tự động viết vòng lặp foreach quen thuộc, vì nó là cách “an toàn” và “thân thiện” nhất để duyệt. Nhưng rồi, vào lúc không ngờ nhất, xuất hiện một lỗi runtime bí ẩn mà ở ví dụ đơn giản thì không có — và chương trình dừng lại ngay lập tức.

Hãy cùng tìm hiểu tại sao lại như vậy, chuyện gì diễn ra “bên trong” collection và iterator, và làm sao để xoá phần tử một cách hợp lý, tránh bất ngờ và bug.

Tại sao foreach không hợp với xoá phần tử

Để cảm nhận rõ hơn, hãy tưởng tượng một hàng người (đó là collection của bạn). Bạn đi dọc hàng và hỏi từng người: "Giữ lại hay gạch tên?" Nếu bạn bắt đầu gạch tên ai đó ngay khi đang duyệt, cả hàng sẽ bị xê dịch, mọi người di chuyển, và kế hoạch “người tiếp theo là người tiếp theo trong danh sách” sẽ hỏng ngay. Có thể ai đó sẽ bị bỏ qua hoặc bạn sẽ hỏi hai lần.

Ví dụ C#:


List<string> names = new List<string> { "Anton", "Boris", "Vika", "Grisha" };

foreach (string name in names)
{
    if (name.StartsWith("V"))
        names.Remove(name); // Bùm! InvalidOperationException
}

Khi chương trình đến "Vika" và quyết định xoá, iterator bên trong sẽ “mất liên lạc với thực tế” — và bạn sẽ nhận được thông báo:
InvalidOperationException: Collection was modified; enumeration operation may not execute.

Đây không phải là “làm khó” bạn — mà C# bảo vệ bạn khỏi bug khó phát hiện và làm hỏng cấu trúc dữ liệu.

2. Tại sao code đơn giản như vậy lại không chạy?

Bên trong hoạt động thế nào?

Khi bạn viết vòng lặp foreach, compiler sẽ tạo ra một object đặc biệt — iterator (IEnumerator), nó theo dõi vị trí hiện tại trong collection. Object này nhớ số phần tử ban đầu, phần tử nào đang “active”, và kiểm soát chặt chẽ để collection không bị thay đổi khi đang duyệt.

Bất kỳ cố gắng xoá hay thêm phần tử trong foreach đều phá vỡ “hợp đồng” này. Tại sao? Nếu sau khi bạn xoá phần tử, các index bị xê dịch, iterator sẽ không thể chuyển đúng sang phần tử tiếp theo. Có thể ai đó bị bỏ qua, ai đó bị tính hai lần — kết quả là loạn hết cả lên. Vì vậy, ngay khi collection bị thay đổi, .NET sẽ ném ra lỗi rõ ràng.

Xoá “thẳng mặt” sẽ ra sao

Giả sử bạn viết chương trình như này:


List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
foreach (int x in numbers)
{
    if (x % 2 == 0)
        numbers.Remove(x);
}

Nghe có vẻ hợp lý: duyệt hết các số, xoá số chẵn. Nhưng đến vòng lặp thứ hai, chương trình sẽ ném lỗi — “collection đã bị thay đổi khi đang duyệt”.

Đôi khi bạn sẽ muốn “lách luật” và thử “liều một phen”. Nhưng kể cả không có lỗi, tuỳ vào cấu trúc collection, kết quả sẽ rất khó đoán. Ví dụ, bạn có thể vô tình “nhảy qua” một số phần tử hoặc không xoá hết những gì cần.

3. Vậy làm đúng thì sao?

Cách 1: Vòng lặp for ngược

Vấn đề là khi xoá, các phần tử phía sau sẽ bị “kéo” lên, và nếu đi từ đầu list, bạn sẽ dễ bị lẫn index và bỏ sót phần tử. Để tránh, hãy đi từ cuối về đầu.


List<string> names = new List<string> { "Anton", "Boris", "Vika", "Grisha" };

for (int i = names.Count - 1; i >= 0; i--)
{
    if (names[i].StartsWith("V"))
        names.RemoveAt(i);
}

Trong ví dụ này, sau mỗi lần xoá, các phần tử phía sau sẽ bị xê dịch, nhưng các index chưa xử lý thì không bị ảnh hưởng. Kết quả là không bị bỏ sót gì cả.

Cách 2: Lọc và tạo list mới

Đôi khi đơn giản (và thường nhanh hơn) là duyệt qua collection, chỉ giữ lại phần tử cần thiết, rồi thay list gốc bằng list mới.


var names = new List<string> { "Anton", "Boris", "Vika", "Grisha" };
names = names.Where(name => !name.StartsWith("V")).ToList();
// Kết quả còn lại "Anton" và "Grisha"

Cách này hợp lý khi collection không quá lớn hoặc không cần giữ nguyên reference gốc.

Cách 3: Dùng method đặc biệt của collection

Nếu bạn dùng List<T> cổ điển, để xoá theo điều kiện có method rất tiện:


names.RemoveAll(name => name.StartsWith("V"));

Toàn bộ quá trình bên trong sẽ được xử lý đúng, và bạn có code ngắn gọn, dễ hiểu.

Cách 4: Gom lại để xoá sau

Có những collection không thể thay đổi “ngay lập tức” (ví dụ, Dictionary, HashSet, hoặc class tự viết). Khi đó, dùng cách “đánh dấu để xoá”:

  1. Đầu tiên duyệt qua collection, gom các phần tử cần xoá vào list riêng.
  2. Sau đó duyệt list này và xoá từng phần tử khỏi collection gốc.

Dictionary<int, string> dict = new Dictionary<int, string> { [1] = "one", [2] = "two", [3] = "three" };
var toDelete = new List<int>();
foreach (var kvp in dict)
{
    if (kvp.Key % 2 == 0)
        toDelete.Add(kvp.Key);
}
foreach (var key in toDelete)
    dict.Remove(key);

4. Một số lưu ý hay ho

Lỗi và “truyền thuyết” của newbie

Một trong những lỗi phổ biến nhất là nghĩ rằng xoá phần tử khỏi collection khi đang duyệt sẽ “kiểu gì cũng chạy”, vì ở một số ngôn ngữ khác (ví dụ Python) thì thường làm được. Nhưng ở C# thì cấm tuyệt đối để bảo vệ bạn: tốt hơn là nhận lỗi rõ ràng còn hơn là bug âm thầm mà không ai debug nổi.

Một lỗi nữa là dùng vòng lặp for tăng index thay vì giảm. Khi đó, sau khi xoá, các phần tử phía sau bị “kéo lên”, và bạn sẽ bỏ qua một số phần tử. Luôn đi từ cuối về đầu nếu xoá theo index.

Bài học rút ra

Bài toán “xoá phần tử khỏi collection theo điều kiện” gặp ở gần như mọi chương trình C#, nhưng làm trực tiếp trong vòng lặp foreach là không được — đó là kiến trúc của ngôn ngữ, để bảo vệ dữ liệu và tránh lỗi bất ngờ.
Nhớ kỹ quy tắc này, bạn sẽ tránh được nhiều đêm mất ngủ vì debug.

Làm đúng thì luôn đúng

  • Đừng bao giờ xoá phần tử khỏi collection ngay trong vòng lặp foreach. Sẽ bị lỗi runtime.
  • Với list (List<T>) và array, dùng vòng lặp for từ cuối về đầu, hoặc method RemoveAll và filter qua LINQ.
  • Với dictionary, set và collection phức tạp khác — gom phần tử cần xoá vào list riêng, rồi duyệt list đó để xoá khỏi collection gốc.
  • Nếu không chắc — hãy nghĩ: collection thay đổi thế nào khi xoá? Iterator sẽ ra sao? Nếu hơi nghi ngờ, tức là cách đó chưa ổn đâu.
1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Lọc phần tử
Làm việc với collection
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION