Hành vi Cơ bản 6 phút đọc

Iterator Pattern trong C#: yield, IEnumerable, IAsyncEnumerable

Iterator pattern trong C# / .NET 10: yield return và IEnumerable chính là pattern này, và khi IAsyncEnumerable cho stream data remote lazy.

Mục lục
  1. Iterator pattern giải quyết bài toán gì trong C#?
  2. yield return cho bạn Iterator miễn phí thế nào?
  3. Khi nào dùng IAsyncEnumerable trong .NET 10?
  4. Khi nào xứng implement IEnumerable tay?
  5. So sánh Iterator với Composite và Visitor thế nào?
  6. Một ví dụ thật trong .NET 10 trông thế nào?
  7. Đọc tiếp gì trong series?

Job fulfillment order cần đi qua mọi order đặt 24 giờ qua, ship cái nào đã trả tiền, email cái nào chưa trả quá lâu. Phần "đi qua mọi order" có ba hình dạng: load tất cả vào RAM rồi foreach; phân trang chunk 1.000; stream khi đến từ service remote. Cả ba chia sẻ một thứ — consumer chỉ muốn viết foreach (var order in source) và không cần quan tâm pagination, allocation, hay async.

Iterator pattern là câu trả lời ngôn ngữ đã build sẵn. Định nghĩa bề mặt "cho element kế" đồng nhất; để source tự giấu nội bộ (array, cây, API paged, DB cursor). C# hiện đại làm vậy từ phiên bản 1 với IEnumerable<T> và từ C# 8 với IAsyncEnumerable<T>. Bài này dành thời gian cho phiên bản hiện đại của pattern, vì class iterator giáo khoa viết tay là phiên bản bạn gần như không bao giờ viết.

Iterator pattern giải quyết bài toán gì trong C#?

Pattern xứng chỗ khi caller muốn bề mặt "từng element một" đồng nhất nhưng layout nội bộ source khác hoặc bị giấu. Ba hình dạng cụ thể:

  1. Giấu layout nội bộ. Cart là cây (Composite); catalog là list; log order là API paginated. Caller phải viết cùng foreach cho cả ba.
  2. Lazy evaluation. Đọc 5 GB order vào List<> crash máy. Iterator yield một row, GC dọn nó.
  3. Transformation compose được. orders.Where(...).Take(100) compose lazy — không phần nào của source được evaluate đến khi consumer xin element đầu. Pattern cho phép điều này.

Cái không phải bài toán Iterator: "Tôi muốn fan out việc cho nhiều worker" — gần với Mediator hay message bus. Iterator là single-consumer, tuần tự.

yield return cho bạn Iterator miễn phí thế nào?

Từ khoá yield C# biến method bình thường thành state-machine implement IEnumerable<T>:

public IEnumerable<Order> RecentOrders()
{
    foreach (var page in _api.PagedOrders(pageSize: 1000))
    {
        foreach (var order in page.Items)
            yield return order;
    }
}

Caller viết:

foreach (var order in RecentOrders())
{
    if (order.IsPaid) await _shipping.ShipAsync(order);
}

Cái compiler sinh: class <RecentOrders>d__0 implement IEnumerator<Order> với field _state, MoveNext resume từ mỗi yield, và Dispose dọn inner foreach. Bạn viết mười dòng code thẳng; compiler viết Iterator pattern giáo khoa.

Bức tranh cấu trúc (cái foreach thực sự làm):

sequenceDiagram
    participant C as Caller (foreach)
    participant E as Enumerator
    C->>E: GetEnumerator()
    loop đến hết
        C->>E: MoveNext()
        E-->>C: true / false
        C->>E: Current
        E-->>C: Order #N
    end
    C->>E: Dispose()

MoveNext chạy method bạn đến yield return kế; Current trả giá trị yielded. Vòng lặp dừng khi MoveNext trả false (method return).

Khi nào dùng IAsyncEnumerable trong .NET 10?

IAsyncEnumerable<T> thêm cùng hình dạng cho source async. Tổ hợp keyword là await foreach cộng await yield return:

public async IAsyncEnumerable<Order> RecentOrdersAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    var pageToken = (string?)null;
    do
    {
        var page = await _api.GetOrdersPageAsync(pageToken, ct);
        foreach (var order in page.Items)
            yield return order;
        pageToken = page.NextToken;
    }
    while (pageToken is not null);
}

Caller:

await foreach (var order in RecentOrdersAsync().WithCancellation(ct))
{
    if (order.IsPaid) await _shipping.ShipAsync(order, ct);
}

Ba thứ cách này cho:

Khi nào xứng implement IEnumerable tay?

Gần như không bao giờ. Hai case có:

Cho mọi thứ khác — API paged, transformation in-memory, đọc file streamed — yield là câu trả lời đúng.

So sánh Iterator với Composite và Visitor thế nào?

Pattern Cái gì đồng nhất? Hướng
Iterator Một element một lần, driven từ ngoài Caller pull
Composite Thao tác hoạt động cho leaf và group Đệ quy (thường kết hợp Iterator)
Visitor Thao tác thêm vào node type cố định Visitor push

Tách: Iterator pull một element một lần; Visitor đi cả cấu trúc thực hiện thao tác; Composite cho cả hai hình dạng cây đồng nhất để làm việc. App .NET điển hình dùng cả ba: Composite cart, Iterator yield leaf của nó, và Visitor tính tax xuyên suốt.

Một ví dụ thật trong .NET 10 trông thế nào?

Kết hợp yield, IAsyncEnumerable, và IEnumerable cho cùng domain:

public sealed record Order(string Id, decimal Total, bool IsPaid);

public interface IOrderSource
{
    // Đồng bộ, in-memory
    IEnumerable<Order>      AllInMemory();
    // Async, remote phân trang
    IAsyncEnumerable<Order> StreamRecent(CancellationToken ct = default);
}

public sealed class HybridOrderSource : IOrderSource
{
    private readonly List<Order> _local = LoadFromCsv();
    private readonly IPaginatedApi _remote;

    public HybridOrderSource(IPaginatedApi remote) => _remote = remote;

    public IEnumerable<Order> AllInMemory()
    {
        foreach (var order in _local)
            yield return order;
    }

    public async IAsyncEnumerable<Order> StreamRecent(
        [EnumeratorCancellation] CancellationToken ct = default)
    {
        string? token = null;
        do
        {
            var page = await _remote.GetPageAsync(token, ct);
            foreach (var order in page.Items)
                yield return order;
            token = page.NextToken;
        }
        while (token is not null);
    }

    private static List<Order> LoadFromCsv() => /* ... */ new();
}

// Caller A - đồng bộ
foreach (var order in source.AllInMemory().Where(o => o.IsPaid))
    Console.WriteLine($"local paid: {order.Id}");

// Caller B - async
await foreach (var order in source.StreamRecent(ct))
    if (order.IsPaid)
        await _shipping.ShipAsync(order, ct);

Code consumer giống hệt hình dạng (foreach / await foreach). Producer giấu source ở RAM, trên đĩa, hay qua mạng. Đồng nhất đó là giá trị của Iterator pattern.

Đọc tiếp gì trong series?

Một ghi chú thực dụng: yield return là design pattern dùng nhiều nhất trong .NET mà không ai gọi nó là design pattern. Mọi LINQ query nối, mọi IEnumerable<T> lặp, mọi async stream đều là Iterator pattern. Thành công của pattern chính là lý do không ai nói về nó nữa — nó tan vào ngôn ngữ.

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

yield return cài Iterator pattern thế nào?
Khi compiler C# thấy yield return trong method trả IEnumerable<T>, nó sinh class state-machine sau hậu trường — đúng enumerator của Iterator pattern. Mỗi yield return thành state tạm dừng; mỗi MoveNext resume từ chỗ yield trước. Bạn viết code thẳng dòng; compiler viết class iterator.
Khi nào dùng IAsyncEnumerable thay vì IEnumerable?
Dùng IAsyncEnumerable<T> khi mỗi element được sinh bởi operation async — call API paginated, query DB streamed, message-bus consumer. Bạn được backpressure, cancellation, và cú pháp await foreach. Dùng IEnumerable<T> khi mỗi bước đồng bộ và source ở RAM hoặc file local.
Khi nào nên implement IEnumerator tay thay vì dùng yield?
Gần như không bao giờ trong code app. Iterator do compiler sinh xử state, exception, và IDisposable đúng. IEnumerator<T> tay chỉ xứng đáng cho type collection custom expose nhiều iterator song song có state share, hoặc cho perf edge-case mà state machine sinh thêm overhead đo được.
Iterator pattern tương tác với Composite pattern thế nào?
Composite định nghĩa cây; Iterator visit element của cây từng cái. Hai cái compose: method cart.Flatten() yield mọi leaf là Iterator trên Composite. Extension method Composite.Flatten() ở chương Composite đúng là kết hợp này.