簡單介紹 PHP 的生成器 (Generators)

程式技術

PHP 的生成器 (Generators) 提供了一個簡單的方式去實現迭代器 (Iterrators)。

如官方文件所說:

A generator allows you to write code that uses foreach to iterate over a set of data without needing to build an array in memory, which may cause you to exceed a memory limit, or require a considerable amount of processing time to generate.

生成器可以幫助你寫出一個可以使用 foreach() 迭代一組數據的程式碼,使用在構建可能超出記憶體限制或者需要大量時間生成的數組很方便。

以下方的程式碼為例,range(1, 10_000_000) 會生成一組資料量龐大的陣列並儲存在記憶體中,這會導致程式執行過程中因為超出記憶體限制而出現錯誤。

<?php

foreach (range(1, 10_000_000) as $number) {
    echo $number;
}

執行上述的程式碼,大概率會得到下方的錯誤訊息。

PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted (tried to allocate 536870920 bytes) in ...

在 PHP 中,我們可以使用 yield 關鍵字來生成一個生成器 (Generator) 物件。

<?php

// yield 關鍵字會返回一個生成器物件
function lazyRange(int $start, int $end): Generator
{
    echo 'start' . PHP_EOL;

    for ($num = $start; $num <= $end; $num++) {
        echo 'number-';

        yield $num;
    }
}

// 生成器可以被 foreach 迭代,現在可以順暢的印出所有結果而不會導致記憶體爆掉
foreach (lazyRange(start: 1, end: 10_000_000) as $number) {
    echo $number;
}

// 印出的結果
// start
// number-1
// number-2
// number-3
// number-4
// number-5
// ...

除了 foreach,你也可以使用 generator 提供的一些方法來進行迭代。

$numbers = lazyRange(start: 1, end: 10);
// 返回第一個 yield 的值
// 如果 generator 已經開始迭代,這裡會拋出錯誤
$numbers->rewind();

// 返回第一個 yield 的內容,即 1
// 此時畫面上的輸出為
// start
// number-1
echo $numbers->current();

// 繼續執行到下一個 yield 之前停止
// 此時畫面上的輸出為
// start
// number-1
// number-
echo $numbers->next();

// 返回第二個 yield 值的內容,即 2
// 此時畫面上的輸出為
// start
// number-1
// number-2
echo $numbers->current();

// 繼續執行到下一個 yield 之前停止
// 此時畫面上的輸出為
// start
// number-1
// number-2
// number-
echo $numbers->next();

生成器在讀取擁有大量資料的檔案 (如好幾百萬行的 csv) 時非常方便,可以多加利用。

<?php

function readLargeCsvFile($filePath)
{
    $handle = fopen($filePath, 'r');

    if ($handle) {
        // fgets() 會從文件的第一行開始讀取資料並返回結果
        // 返回結果後,會將指針移往下一行,因此再次執行就會是下一行的資料
        while (($line = fgets($handle)) !== false) {
            yield $line;
        }

        fclose($handle);
    }
}

$generator = readLargeCsvFile('example.csv');

foreach ($generator as $line) {
    echo $line;
}

參考資料

sharkHead
written by
sharkHead

後端打工仔,在下班後喜歡研究各種不同的技術。稍微擅長 PHP,並偶爾涉獵前端開發。個性就像動態語言般隨興,但渴望做事能像囉嗦的靜態語言那樣嚴謹。

0 則留言