Menu

Generator trong PHP: yield, yield from và bộ nhớ

Generator là một hàm dùng yield để trả từng giá trị một, nhờ đó foreach có thể lặp qua một triệu phần tử mà không cần tạo mảng. Tìm hiểu yield, khóa, tiết kiệm bộ nhớ, dãy vô hạn, yield from, send() và getReturn().

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Generator là một hàm có chứa yield. Gọi hàm này không chạy phần thân; nó trả về một đối tượng Generator mà foreach có thể lặp qua. Mỗi yield $value trao một giá trị cho vòng lặp và tạm dừng hàm cho đến khi vòng lặp xin giá trị tiếp theo, nên các giá trị được tạo ra từng cái một thay vì gom vào một mảng trước.

Đầu ra xen kẽ giữa generator và vòng lặp: mỗi giá trị chỉ được tạo ra khi vòng lặp yêu cầu.

Generator tiết kiệm bộ nhớ

range(1, 1000000) tạo một mảng một triệu số nguyên trước khi vòng lặp bắt đầu. Một generator yield cùng những số đó thì không bao giờ giữ quá một số. Hãy chạy và so sánh bộ nhớ mà mỗi phiên bản cần:

Cả hai vòng lặp in ra cùng một tổng, 500000500000, nhưng phiên bản dùng mảng báo extra memory 16.0 MB còn generator báo extra memory 0.0 MB. Khoảng cách này càng lớn khi dữ liệu càng nhiều. Cũng chính ý tưởng này làm generator hữu ích với tệp lớn, kết quả truy vấn cơ sở dữ liệu và các trang của API: xử lý một phần tử, bỏ nó đi, lấy phần tử tiếp theo.

Yield kèm khóa và giá trị

yield $key => $value đặt khóa mà foreach ($gen as $key => $value) nhận được. Không có khóa, generator đánh số các giá trị từ 0, giống một danh sách. Một cách dùng phổ biến là đọc tệp từng dòng kèm số dòng:

Mỗi lúc chỉ có một dòng nằm trong bộ nhớ, nên cùng hàm này xử lý được cả tệp log 2 GB. fopen() và fgets() giải thích các hàm xử lý tệp được dùng ở đây.

Dãy vô hạn và pipeline lười

Generator có thể lặp mãi mãi, vì nó chỉ chạy đến chừng nào nơi dùng còn yêu cầu. Dừng bằng break, hoặc bọc nó trong một generator khác chỉ lấy vài giá trị đầu. Nối các generator nhỏ với nhau tạo thành một pipeline mà mọi bước đều lười (lazy):

fibonacci() tự nó không bao giờ kết thúc, vậy mà chương trình vẫn chạy xong: take() ngừng yêu cầu khi đã đủ số lượng, và không có gì vượt quá mức đó được tính cả.

yield from: giao việc cho generator khác

yield from chuyển tiếp mọi giá trị của một mảng, một iterator hoặc một generator khác. Đây là cách chia một generator thành các generator nhỏ hơn, hoặc duyệt đệ quy một cấu trúc lồng nhau:

yield from giữ nguyên khóa bên trong, nên cả hai mảng đều yield các khóa 0 và 1. Sau đó iterator_to_array() ghi đè a và b. Hãy truyền false làm đối số thứ hai bất cứ khi nào bạn chỉ quan tâm đến giá trị.

send() và getReturn()

Generator cũng có thể nhận giá trị. $x = yield; tạm dừng và chờ; $gen->send($value) cho generator chạy tiếp với $value là kết quả của yield đó. Generator còn có thể return một giá trị cuối cùng, bạn đọc nó bằng getReturn() sau khi generator đã chạy xong.

Phần lớn code không bao giờ cần đến send(); đọc lười với foreach mới là cách dùng hằng ngày. Đây là nền tảng của các thư viện coroutine.

Generator chỉ lặp được một lần

Generator chạy tiến và không thể quay lại từ đầu. Một khi foreach đã dùng hết nó, vòng lặp thứ hai trên cùng đối tượng sẽ ném lỗi:

Cách sửa là gọi lại hàm generator cho mỗi lượt, foreach (colors() as $c), hoặc gom giá trị một lần bằng iterator_to_array(colors(), false) khi bạn thật sự cần dùng chúng hai lần. Nếu thấy mình thường xuyên làm vậy, thì một mảng thông thường mới là lựa chọn đúng cho dữ liệu đó.

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

yield trong PHP làm gì?

yield $value trao một giá trị cho đoạn code đang lặp qua hàm và tạm dừng hàm ngay tại đó. Lượt lặp tiếp theo sẽ tiếp tục hàm từ sau yield. Mọi hàm chứa yield khi được gọi sẽ trả về một đối tượng Generator thay vì chạy ngay.

Vì sao nên dùng generator thay cho mảng?

Mảng giữ mọi phần tử trong bộ nhớ cùng lúc; generator tạo ra từng phần tử một. Lặp qua range(1, 1000000) phải cấp phát cả một triệu số nguyên, còn generator yield các số đó thì giữ bộ nhớ gần như không đổi. Generator còn cho phép tạo dãy vô hạn và dừng công việc sớm khi vòng lặp break.

yield from trong PHP là gì?

yield from $source chuyển tiếp mọi giá trị của một generator khác, một mảng hoặc một Traversable, như thể chính generator hiện tại đã yield chúng. Biểu thức này cũng nhận giá trị trả về của generator bên trong.

Có thể lặp qua một generator hai lần không?

Không. Generator chỉ chạy tiến. Sau khi một vòng foreach đã chạy xong, lặp lại sẽ ném ra Exception: Cannot traverse an already closed generator. Hãy gọi lại hàm generator để có generator mới, hoặc lưu các giá trị bằng iterator_to_array() nếu bạn cần dùng chúng hai lần.

Làm sao chuyển generator thành mảng?

iterator_to_array($gen) gom mọi giá trị và giữ nguyên khóa của generator. Nếu khóa bị trùng, chẳng hạn sau yield from, giá trị sau sẽ ghi đè giá trị trước, nên hãy truyền false làm đối số thứ hai để nhận một danh sách thường: iterator_to_array($gen, false).

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu