Các yield
từ khóa cho phép bạn tạo ra một IEnumerable<T>
trong các hình thức trên một khối iterator . Khối lặp này hỗ trợ thực thi hoãn lại và nếu bạn không quen với khái niệm này, nó có thể xuất hiện gần như kỳ diệu. Tuy nhiên, vào cuối ngày, nó chỉ là mã thực thi mà không có bất kỳ thủ đoạn kỳ lạ nào.
Một khối lặp có thể được mô tả là đường cú pháp trong đó trình biên dịch tạo ra một máy trạng thái theo dõi mức độ liệt kê của liệt kê đã tiến triển đến đâu. Để liệt kê một vô số, bạn thường sử dụng một foreach
vòng lặp. Tuy nhiên, mộtforeach
vòng lặp cũng là cú pháp đường. Vì vậy, bạn có hai khái niệm trừu tượng bị xóa khỏi mã thực, đó là lý do ban đầu có thể khó hiểu làm thế nào tất cả hoạt động cùng nhau.
Giả sử rằng bạn có một khối lặp rất đơn giản:
IEnumerable<int> IteratorBlock()
{
Console.WriteLine("Begin");
yield return 1;
Console.WriteLine("After 1");
yield return 2;
Console.WriteLine("After 2");
yield return 42;
Console.WriteLine("End");
}
Các khối lặp thực thường có các điều kiện và các vòng lặp nhưng khi bạn kiểm tra các điều kiện và hủy kiểm soát các vòng lặp thì chúng vẫn kết thúc như yield
câu lệnh xen kẽ với các mã khác.
Để liệt kê khối foreach
lặp, một vòng lặp được sử dụng:
foreach (var i in IteratorBlock())
Console.WriteLine(i);
Đây là đầu ra (không có gì ngạc nhiên ở đây):
Bắt đầu
1
Sau 1
2
Sau 2
42
Kết thúc
Như đã nêu ở trên foreach
là cú pháp đường:
IEnumerator<int> enumerator = null;
try
{
enumerator = IteratorBlock().GetEnumerator();
while (enumerator.MoveNext())
{
var i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
enumerator?.Dispose();
}
Trong một nỗ lực gỡ rối điều này, tôi đã đưa ra một sơ đồ trình tự với các tóm tắt được loại bỏ:
Máy trạng thái được tạo bởi trình biên dịch cũng thực hiện trình liệt kê nhưng để làm cho sơ đồ rõ ràng hơn, tôi đã chỉ ra chúng như các trường hợp riêng biệt. (Khi máy trạng thái được liệt kê từ một luồng khác, bạn thực sự có các trường hợp riêng biệt nhưng chi tiết đó không quan trọng ở đây.)
Mỗi khi bạn gọi khối lặp của mình, một phiên bản mới của máy trạng thái được tạo. Tuy nhiên, không có mã nào của bạn trong khối iterator được thực thi cho đến khi enumerator.MoveNext()
thực thi lần đầu tiên. Đây là cách thực hiện trì hoãn hoạt động. Đây là một ví dụ (khá ngớ ngẩn):
var evenNumbers = IteratorBlock().Where(i => i%2 == 0);
Tại thời điểm này, trình vòng lặp chưa được thực thi. Các Where
khoản tạo ra một mới IEnumerable<T>
mà kết thúc tốt đẹp các IEnumerable<T>
trả về bởi IteratorBlock
nhưng đếm này vẫn chưa được liệt kê. Điều này xảy ra khi bạn thực hiện một foreach
vòng lặp:
foreach (var evenNumber in evenNumbers)
Console.WriteLine(eventNumber);
Nếu bạn liệt kê số đếm hai lần thì một phiên bản mới của máy trạng thái được tạo ra mỗi lần và khối lặp của bạn sẽ thực thi cùng một mã hai lần.
Chú ý rằng phương pháp LINQ thích ToList()
, ToArray()
, First()
, Count()
vv sẽ sử dụng một foreach
vòng lặp để liệt kê các đếm được. Ví dụ, ToList()
sẽ liệt kê tất cả các yếu tố của vô số và lưu trữ chúng trong một danh sách. Bây giờ bạn có thể truy cập vào danh sách để có được tất cả các phần tử có thể đếm được mà không cần khối lặp lại thực thi lại. Có một sự đánh đổi giữa việc sử dụng CPU để tạo ra các phần tử của vô số lần và bộ nhớ để lưu trữ các phần tử của phép liệt kê để truy cập chúng nhiều lần khi sử dụng các phương thức như thế nào ToList()
.