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
- Iterator pattern giải quyết bài toán gì trong C#?
- yield return cho bạn Iterator miễn phí thế nào?
- Khi nào dùng IAsyncEnumerable trong .NET 10?
- Khi nào xứng implement IEnumerable tay?
- So sánh Iterator với Composite và Visitor thế nào?
- Một ví dụ thật trong .NET 10 trông thế nào?
- Đọ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ể:
- 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
foreachcho cả ba. - Lazy evaluation. Đọc 5 GB order vào
List<>crash máy. Iterator yield một row, GC dọn nó. - 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:
- Backpressure. Producer dừng tự nhiên giữa các page — body
của consumer phải xong trước khi
MoveNextkế chạy. - Cancellation.
[EnumeratorCancellation]cộngWithCancellation(ct)là cách BCL khuyến nghị truyền token qua iterator async. - Compose. EF Core 6+ expose
DbContext.Set<T>().AsAsyncEnumerable(); controller ASP.NET Core trảIAsyncEnumerable<T>và framework stream response. Pattern là một phần framework giờ, không chỉ cú pháp lạ.
Khi nào xứng implement IEnumerable tay?
Gần như không bao giờ. Hai case có:
- Type collection custom. Cấu trúc data domain-specific (skip
list lock-free, cây custom, ring buffer) mà consumer mong
foreach. Struct enumerator có thể tránh allocation tốt hơn state machine compiler sinh. - Iterator song song. Type mà hai consumer có thể đi
collection cùng lúc mà không thấy state nhau. State machine sinh
bởi
yieldkhông thiết kế cho việc này; class enumerator tường minh thì 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?
- Bài trước: Interpreter — khi thứ bạn đi là AST và mỗi node biết evaluate.
- Bài kế: Mediator — khi nhiều peer phải nói chuyện qua hub thay vì trực tiếp.
- Tham chiếu chéo: Composite — cây iterator của bạn thường đi.
- Tham chiếu chéo: Visitor — khi bước đi cần thực hiện nhiều thao tác và bạn muốn giữ chúng ngoài class node.
- Cây quyết định: Cách chọn design pattern phù hợp.
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?
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?
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?
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.