Menu

Vòng lặp foreach C#: mảng, List, Dictionary, chỉ số và lỗi

Cách foreach hoạt động trong C#: duyệt mảng, list, chuỗi và từ điển (KeyValuePair và deconstruction), lấy chỉ số, vì sao biến lặp chỉ đọc, ngoại lệ InvalidOperationException "Collection was modified" và cách khắc phục, và một kiểu cần gì để dùng được trong foreach.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

foreach chạy thân vòng lặp một lần cho mỗi phần tử của một collection, theo thứ tự, mà không cần quản lý chỉ số:

Đầu ra:

Welcome, Ava
Welcome, Noah
Welcome, Mia
Total: 255
[C][#][7]

Vòng lặp hoạt động như nhau với mảng, List<T>, chuỗi (kiểu phần tử là char), HashSet<T>, một truy vấn LINQ hay bất kỳ dãy nào khác. Kiểu khai báo có thể là var, cách viết hay gặp khi tên kiểu phần tử dài: foreach (var order in pendingOrders).

foreach làm gì ở phía sau

foreach xin collection một enumerator rồi duyệt qua nó. Với một List<int>, trình biên dịch biến vòng lặp thành đại khái như sau:

List<int>.Enumerator e = scores.GetEnumerator();
try
{
    while (e.MoveNext())
    {
        int score = e.Current;
        // loop body
    }
}
finally
{
    e.Dispose();
}

Từ đây có ba hệ quả. Mọi kiểu có phương thức public GetEnumerator() đều dùng được, phương thức này thường có được nhờ cài đặt IEnumerable<T>. Enumerator được dispose kể cả khi thân vòng lặp ném ngoại lệ hoặc gặp break. Và biến phần tử là một bản sao mới của e.Current ở mỗi lượt, đó là lý do gán cho nó không thể thay đổi collection.

Một kiểu không có GetEnumerator thì không dùng được:

error CS1579: foreach statement cannot operate on variables of type 'Order' because 'Order' does not contain a public instance or extension definition for 'GetEnumerator'

Hãy cài đặt IEnumerable<T> cho kiểu đó, hoặc duyệt collection mà nó chứa (order.Lines thay vì order). Bạn có thể tự viết dãy của mình bằng yield return, được trình bày ở trang IEnumerable và yield.

Duyệt Dictionary

Các phần tử của từ điển là giá trị KeyValuePair<TKey, TValue> với hai thuộc tính Key và Value:

Đầu ra:

apples: 12
pears: 0
kiwis: 30
<apples><pears><kiwis>
Units in stock: 42
 30 kiwis
 12 apples
  0 pears

Dictionary không cam kết thứ tự duyệt nào. Trên thực tế, nó theo thứ tự thêm vào cho đến khi bạn xóa phần tử, nhưng code phụ thuộc vào thứ tự nên sắp xếp tường minh, như vòng lặp cuối làm với OrderByDescending, hoặc dùng SortedDictionary.

Trên .NET Core 2.0 trở lên (và mọi phiên bản .NET hiện đại), KeyValuePair có phương thức Deconstruct, nên có thể tách cặp thành hai biến ngay trong phần đầu vòng lặp:

// modern .NET: KeyValuePair deconstruction
foreach (var (fruit, count) in stock)
{
    Console.WriteLine($"{fruit}: {count}");
}

Đây là cách viết ngắn nhất và là cách bạn sẽ gặp trong code hiện nay. Nó không biên dịch được trên .NET Framework, nơi KeyValuePair không có Deconstruct; ở đó hãy dùng item.Key và item.Value.

Lấy chỉ số

foreach không có chỉ số riêng. Hai cách thường dùng để có chỉ số là tự duy trì một biến đếm, hoặc dùng overload Select của LINQ, overload này truyền chỉ số đi kèm mỗi phần tử:

Đầu ra:

1. Kenji
2. Sofia
3. Omar
index 0: Kenji
index 1: Sofia
index 2: Omar

Biến đếm là cách rẻ nhất và rõ ràng nhất. Nếu thấy mình dùng chỉ số để đọc podium[i - 1] hoặc ghi podium[i] = ..., hãy chuyển sang vòng lặp for, vốn được tạo ra đúng cho việc đó.

Biến lặp là chỉ đọc

Bạn không thể gán giá trị cho biến lặp:

foreach (string name in names)
{
    name = name.Trim();
    // error CS1656: Cannot assign to 'name' because it is a 'foreach iteration variable'
}

Dù có được phép, việc gán cũng chỉ thay đổi một bản sao cục bộ. Để biến đổi mọi phần tử, hãy tạo một dãy mới (names.Select(n => n.Trim()).ToArray()) hoặc dùng vòng lặp for ghi names[i] = names[i].Trim().

Những gì bạn có thể làm phụ thuộc vào việc phần tử là lớp hay struct. Với lớp, biến giữ một tham chiếu, nên thay đổi thuộc tính sẽ thay đổi chính đối tượng trong collection:

Đầu ra:

Order 1: shipped
Order 2: shipped

Nếu Order là một struct, cùng phép gán đó sẽ lỗi CS1654 (Cannot modify members of 'order' because it is a 'foreach iteration variable'), vì biến là bản sao của giá trị chứ không phải tham chiếu đến nó.

Sửa collection khi đang duyệt: InvalidOperationException

Thêm hoặc xóa phần tử của một List hay Dictionary khi foreach đang duyệt nó sẽ làm hỏng enumerator. Lần MoveNext tiếp theo ném InvalidOperationException với thông báo "Collection was modified; enumeration operation may not execute.":

Đầu ra:

InvalidOperationException: collection was modified
milk, bread
milk, bread

Các cách khắc phục, từ đơn giản nhất đến linh hoạt nhất:

  • RemoveAll(predicate) trên List<T> xóa mọi phần tử khớp trong một lượt.
  • Duyệt trên một bản sao bằng ToList() hoặc ToArray(), rồi thoải mái sửa bản gốc.
  • Gom trước, áp dụng sau: trong vòng lặp, gom các khóa cần xóa vào một list riêng, rồi xóa chúng sau đó. Đây là cách làm quen thuộc với Dictionary.
  • Vòng lặp for chạy ngược với RemoveAt(i).

Dictionary có một ngoại lệ đáng biết. Từ .NET Core 3.0, ghi đè giá trị của một khóa đã có (stock[key] = 0) hoặc xóa phần tử trong lúc foreach qua nó không còn ném ngoại lệ, còn thêm khóa mới thì vẫn ném. Trên .NET Framework cả ba đều ném, nên gom khóa trước là cách chạy được ở mọi nơi.

break, continue và collection null

break và continue hoạt động như trong mọi vòng lặp khác:

Đầu ra:

reading 21
reading 23
no tags, no crash

Duyệt một collection null sẽ ném NullReferenceException khi GetEnumerator được gọi. ?? Enumerable.Empty<T>() biến một dãy có thể null thành một dãy rỗng; tốt hơn nữa, hãy trả về collection rỗng thay vì null từ các phương thức của bạn.

foreach và List.ForEach

List<T> có phương thức ForEach(Action<T>): names.ForEach(n => Console.WriteLine(n));. Nó không giống vòng lặp. Thân của nó là một lambda, nên không dùng được break, continue và yield, còn return chỉ kết thúc lời gọi cho phần tử hiện tại. Nó chỉ có trên List<T>, không có trên mảng hay các dãy khác. Hãy ưu tiên câu lệnh foreach; chỉ dùng ForEach cho một hành động một dòng mà bạn đã có sẵn dưới dạng phương thức.

Câu hỏi thường gặp

Làm sao duyệt Dictionary bằng foreach trong C#?

Mỗi phần tử là một KeyValuePair<TKey, TValue>: foreach (var pair in prices) Console.WriteLine($"{pair.Key}: {pair.Value}");. Trên .NET Core 2.0 trở lên, bạn có thể tách cặp này ra: foreach (var (name, price) in prices). Để chỉ duyệt khóa hoặc giá trị, dùng prices.Keys hoặc prices.Values.

Làm sao lấy chỉ số trong vòng lặp foreach của C#?

foreach không có chỉ số sẵn. Hãy giữ một biến đếm (int i = 0; trước vòng lặp, i++ ở cuối thân), hoặc dùng LINQ để gắn mỗi phần tử với chỉ số của nó: foreach (var item in names.Select((name, index) => new { name, index })). Nếu bạn cần chỉ số để ghi ngược hoặc so sánh các phần tử kề nhau, vòng lặp for phù hợp hơn.

"Collection was modified; enumeration operation may not execute" nghĩa là gì?

Bạn đã thêm hoặc xóa phần tử trong một List, Dictionary hay collection khác khi một foreach đang duyệt nó. Enumerator phát hiện thay đổi và ném InvalidOperationException ở bước tiếp theo. Hãy duyệt trên một bản sao (foreach (var x in list.ToList())), gom các thay đổi lại và áp dụng sau vòng lặp, dùng list.RemoveAll(...), hoặc dùng vòng lặp for chạy ngược.

Vì sao không gán được giá trị cho biến foreach trong C#?

Biến lặp là chỉ đọc: name = name.Trim(); bên trong foreach (string name in names) gây lỗi CS1656. Dù có gán được thì collection cũng không thay đổi. Để thay thế phần tử, hãy dùng vòng lặp for với chỉ số, hoặc tạo collection mới bằng Select. Thay đổi thuộc tính của một đối tượng thuộc lớp (order.Status = ...) thì được phép.

Có thoát khỏi vòng lặp foreach trong C# được không?

Có. break kết thúc vòng lặp ngay lập tức và continue chuyển sang phần tử tiếp theo, giống hệt trong for và while. return thoát khỏi cả phương thức. Phương thức List<T>.ForEach thì khác: thân của nó là một lambda, nên không dùng được break và continue ở đó.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu