CodeGym /Các khóa học /C# SELF /Hợp đồng liệt kê: IEnumera...

Hợp đồng liệt kê: IEnumerable<T>

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

1. Giới thiệu

Có thể bạn đã để ý, các collection trong C# có nhiều điểm chung. Ví dụ, gần như collection nào cũng có thể duyệt bằng vòng lặp foreach:


var names = new List<string> { "Anya", "Boris", "Vika" };

// Lưu ý — code này chạy được với List, mảng, thậm chí cả HashSet!
foreach (var name in names)
{
    Console.WriteLine(name);
}

Có gì "ảo diệu" ở đây? Tất cả là nhờ các collection hiện đại đều implement interface IEnumerable<T> — kiểu như một "hợp đồng" đảm bảo collection đó biết cách trả về từng phần tử một, theo thứ tự.

Hãy tưởng tượng một công ty, nơi mọi collection nhân viên (phòng ban, team dự án, danh sách party...) đều có quy tắc: nếu object đó implement interface "IEnumerable", bạn luôn có thể duyệt qua tất cả nhân viên theo thứ tự nhất định, không cần quan tâm "họ lưu ở đâu", chỉ cần biết là có thể duyệt được.

2. Interface IEnumerable<T> — bên trong có gì?

Xem thử interface này trong .NET trông thế nào nhé:


public interface IEnumerable<out T>
{
    IEnumerator<T> GetEnumerator();
}

Nó chỉ có một method duy nhấtGetEnumerator, trả về object kiểu IEnumerator<T>. Object này chịu trách nhiệm cho việc "liệt kê": nó biết phần tử hiện tại là gì, làm sao chuyển sang phần tử tiếp theo, khi nào thì hết.

Tóm lại: nếu class (hoặc collection) implement IEnumerable<T>, nghĩa là bạn có thể duyệt nó bằng vòng lặp foreach, lấy từng phần tử một — không quan trọng bên trong nó lưu thế nào: danh sách, hash-table, hay thậm chí trên ổ cứng.

Sơ đồ nhỏ


Collection (ví dụ List<T>)
    |
    v
IEnumerable<T>
    |
    v
IEnumerator<T> (thằng "duyệt từng phần tử" thực sự)

3. Thực hành và code đa năng

Điểm mạnh lớn của hợp đồng này là tính đa năng. Nếu function hoặc method nhận vào IEnumerable<T>, nghĩa là nó chơi được với mọi loại collection — từ mảng, list, hashset, queue, thậm chí collection tự chế!

Ví dụ, thử viết function tính tổng các phần tử:


// Có thể tính tổng bất kỳ tập int nào hỗ trợ IEnumerable<int>
int Sum(IEnumerable<int> numbers)
{
    int result = 0;
    foreach (var n in numbers)
        result += n;
    return result;
}

// Chạy với List<int>
var list = new List<int> { 1, 2, 3 };
Console.WriteLine(Sum(list));

// Chạy với mảng!
int[] array = { 4, 5, 6 };
Console.WriteLine(Sum(array));

// Chạy cả với kết quả method lọc phần tử
Console.WriteLine(Sum(list.Where(x => x % 2 == 0))); // Dùng LINQ

Ví dụ thực tế: method đa năng

Giả sử bạn cần viết tool tìm từ dài nhất trong bất kỳ collection string nào. Nhờ IEnumerable<string>, bạn làm được với mọi nguồn — mảng, list, kết quả filter, v.v.:


string FindLongest(IEnumerable<string> words)
{
    string longest = "";
    foreach (var word in words)
        if (word.Length > longest.Length)
            longest = word;
    return longest;
}

Bạn có thể dùng với bất kỳ tập hợp nào phù hợp.

4. Tại sao vòng lặp foreach chạy được với IEnumerable<T>?

Câu hỏi hay gặp: tại sao vòng lặp foreach lại "hiểu" mọi collection? Đơn giản thôi: compiler C# sẽ tìm method GetEnumerator trong class, và mong đợi nhận về object có method MoveNext() và property Current. Đó chính là interface chuẩn — IEnumerator<T>.

Nhờ vậy, bạn thậm chí có thể tự viết class "phát" từng phần tử (ví dụ, generator dãy Fibonacci), và nếu nó implement IEnumerable<int>, bạn dùng được với foreach như list bình thường.

5. Enumerator là gì và hoạt động ra sao?

Bên trong mọi collection implement IEnumerable<T> đều có một thằng “duyệt từng phần tử” đặc biệt — Enumerator (hay nói chuẩn: object implement interface IEnumerator<T>). Chính nó "kéo" từng phần tử ra khi bạn viết vòng lặp foreach.

Interface IEnumerator<T>

Enumerator chuẩn làm được gì:


public interface IEnumerator<T> : IDisposable
{
    T Current { get; }         // Phần tử hiện tại
    bool MoveNext();           // Chuyển sang phần tử tiếp theo
    void Reset();              // Quay lại đầu (hiếm khi dùng)
}

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

  • MoveNext() — di chuyển "con trỏ" sang phần tử tiếp theo và trả về true nếu còn phần tử. Nếu hết thì trả về false.
  • Current — trả về phần tử hiện tại (con trỏ đang chỉ vào đâu thì lấy cái đó).
  • Reset() — đưa Enumerator về đầu (gần như không ai dùng).
  • Dispose() — giải phóng tài nguyên (dùng cho collection làm việc với file hoặc mạng).

Ví dụ: foreach hoạt động "bên dưới" ra sao

Khi bạn viết:


var numbers = new List<int> { 1, 2, 3 };
foreach (var n in numbers)
    Console.WriteLine(n);

Thực ra compiler sẽ biến nó thành code kiểu này:


var numbers = new List<int> { 1, 2, 3 };

// Lấy "thằng duyệt"
var enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
    var n = enumerator.Current;
    Console.WriteLine(n);
}
// Compiler tự gọi Dispose() trong using block (nếu enumerator implement IDisposable)

Lưu ý: nếu collection làm việc với tài nguyên ngoài (file, database...), Enumerator có thể tự giải phóng khi duyệt xong.

Sơ đồ trực quan


Bắt đầu duyệt -> GetEnumerator() -> Enumerator
         |
         v
  MoveNext() -> Current
         |
         v
  MoveNext() -> Current
         |
        ...
         |
         v
  MoveNext() == false -> kết thúc duyệt

6. IEnumerable và mảng, list, set: ai là họ hàng?

Xem thử các container chuẩn .NET nào implement interface này:

Loại collection Có implement IEnumerable<T>? Duyệt bằng foreach được không?
Mảng (
int[]
)
List<T>
Dictionary<TKey, V>
✅ (theo cặp, key, value)
HashSet<T>
Queue<T>
Stack<T>

Ngay cả string (string) cũng implement IEnumerable thường, nên bạn cũng có thể duyệt từng ký tự trong string.

7. Tự implement Enumerable của riêng bạn

Thử thách thú vị: thử tự viết collection nhỏ lưu các số chẵn từ 0 đến N và implement IEnumerable<int>. Khi đó bạn có thể duyệt nó bằng vòng lặp và dùng với LINQ.


// Class-collection có thể "duyệt"
class EvenNumbers : IEnumerable<int>
{
    private int max;

    public EvenNumbers(int max)
    {
        this.max = max;
    }

    public IEnumerator<int> GetEnumerator()
    {
        for (int i = 0; i <= max; i += 2)
            yield return i; // "Ma thuật" đặc biệt để tạo enumerator
    }

    // Implement rõ ràng interface IEnumerable không generic cho tương thích ngược
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

// Sử dụng:
var evens = new EvenNumbers(10);
foreach(var e in evens)
    Console.Write($"{e} "); // 0 2 4 6 8 10

Điểm mấu chốt: nếu class của bạn implement IEnumerable<T> — bạn tự động làm nó tương thích với hầu hết công cụ .NET: LINQ, foreach, các method nhận enumerable.

8. Lỗi thường gặp và lưu ý

Đôi khi newbie nghĩ IEnumerable<T> là một collection riêng biệt có sẵn phần tử. Thực ra — nó chỉ là "lời hứa", rằng nếu bạn bắt đầu duyệt, nó sẽ "phát" từng phần tử cho bạn.

Nếu bạn muốn truy cập ngẫu nhiên theo index (myList[5]), dùng method kiểu Add hoặc Remove — interface IEnumerable<T> không giúp gì đâu. Nó chỉ để duyệt tuần tự thôi!

Lỗi: cố gắng sửa collection khi đang duyệt. Ví dụ:


foreach (var item in myList)
{
    if (item < 0)
        myList.Remove(item); // NGUY HIỂM! InvalidOperationException
}

Tốt nhất là tạo list riêng để xóa, hoặc dùng method tạo collection mới:


// Cách an toàn — tự tạo collection mới
var newList = new List<int>();
foreach (var item in myList)
{
    if (item >= 0)
        newList.Add(item);
}
myList = newList;

// Hoặc xóa theo index ngược lại
for (int i = myList.Count - 1; i >= 0; i--)
{
    if (myList[i] < 0)
        myList.RemoveAt(i);
}
2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Tự triển khai một collection hỗ trợ IEnumerable
Tự triển khai một collection hỗ trợ IEnumerable
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION