Từ khóa yield trong C#


Hôm trước mình có một câu hỏi liên quan đến từ khóa yield (https://www.facebook.com/namdotnet/posts/pfbid02ghwNXv2WCxka7BUyYv4pW7jHJjzQrT1qNSZaLE3oWvv8dCWpjWc7jSS91L2UB78wl), sở dĩ mình nhắc đến từ khóa này vì nó liên quan đến khái niệm State Machine (máy trạng thái), cũng là một khái niệm được dùng để tạo nên async/await.

Lưu ý: đã có clip chi tiết về async/await

Giờ thì chúng ta cùng xem yield làm được gì nhé:

yield được sử dụng khi làm việc với các iterator, ví dụ như khi bạn sử dụng foreach để duyệt qua một IEnumerable. Mỗi khi muốn trả một giá trị vào trong iterator, bạn sẽ dùng yield return, khi muốn kết thúc, bạn gọi yield break, rất đơn giản phải không?

Note: bạn có thể vào trang này để chạy thử các ví dụ với yield: https://learn.microsoft.com/…/language…/statements/yield

Điểm đặc biệt của yield nằm ở cách nó hoạt động: mỗi khi bạn gọi yield return (hình 1 dòng 19), hàm của bạn sẽ thực sự kết thúc để trở về vòng lặp trong hàm Main và thực thi câu lệnh Console.Write, ở lần lặp tiếp theo, hàm OddNumbers sẽ được gọi lại và việc thực thi sẽ tiếp tục lại câu lệnh sau yield return (trong ví dụ này không có câu lệnh nào nên nó sẽ i++ và chạy vòng lặp tiếp theo). Có nghĩa là hàm OddNumbers thực sự được gọi nhiều lần, và mỗi lần nó sẽ trả về một giá trị nó tìm được.

Điều này hoàn toàn khác so với cách viết trong hàm OddNumbersUsingList, trong đó ta tìm tất cả số lẻ và trả về trong một danh sách, nếu sử dụng hàm này thay vì OddNumbers, nó sẽ chỉ được gọi duy nhất một lần khi khởi tạo vòng lặp trong hàm Main.

Làm sao .NET biết được cần quay lại vị trí nào trong OddNumbers ở lần kế tiếp nó được gọi? Làm sao nó biết được lúc đó là lần lặp thứ mấy và i đang mang giá trị bao nhiêu? Đó là nhờ nó tạo nên một object để lưu lại giá trị của i, và sau đó mỗi khi được gọi nó dùng lại giá trị đó thay vì hủy nó đi. Chúng ta sẽ dựa trên giá trị của i trong object (tạm gọi là object StateMachine) đó để xác định điều kiện lặp trong OddNumbers (hay nói cách khác, dựa trên trạng thái i của StateMachine).

StateMachine ở đây đóng vai trò là một đối tượng, có trạng thái và sẽ hoạt động dựa trên trạng thái đó, mỗi khi chúng ta yêu cầu nó xử lý, nó sẽ thực hiện thao tác và thay đổi trạng thái sang một trạng thái mới. Ta đặt tên hàm xử lý này là MoveNext.

Như vậy, cùng là lời gọi đến MoveNext, nhưng kết quả có thể khác nhau vì phải dựa trên giá trị của i hiện tại để ra được i tiếp theo. Nhờ máy trạng thái này mà OddNumbers có thể tiếp tục thực thi ngay sau vị trí mà nó thoát ra trước đó.

Khi tôi sử dụng công cụ dịch ngược ILSpy (hình 2), các bạn có thể thấy một lớp có tên <OddNumbers>d__1 được tạo ra, trong đó có biến <i>5__1 và hàm MoveNext().

Các bạn sẽ không thấy máy trạng thái nào liên quan đến OddNumbersUsingList được tạo ra vì chúng ta không sử dụng yield trong hàm này.

Trong hàm MoveNext, ta sẽ thấy biến <>2__current được gán vào giá trị i hiện tại (hình 3), biến <>2__current sẽ được trả về cho hàm Main thông qua thuộc tính Current của IEnumerator (hình 4)

Tới đây hi vọng các bạn đã hiểu cách làm việc của yield thông qua state machine, các bạn cũng được làm quen với ILSpy, một công cụ dịch ngược mạnh mẽ (ILSpy chính là công cụ được sử dụng trong VS 2022 để dịch ngược code khi các bạn nhấn F12). async/await cũng sử dụng state machine khi làm việc, vậy nên hiểu về chúng rất quan trọng (chẳng phải vòng lặp trong hàm Main và vòng lặp trong OddNumbers đang hoạt động đồng thời – hay chính xác là đan xen với nhau sao?). Chúng ta cũng sẽ sử dụng ILSpy để khảo sát code async được sinh ra, cách học này tuy hơi “xương”, nhưng sẽ giúp bạn hiểu tường tận những gì diễn ra đằng sau.

One thought on “Từ khóa yield trong C#

Leave a comment