PHP yield 協程 生成器用法探究 (一)

xiaobai_php發表於2020-05-26

寫在前面

這篇文章,要和大家探討的是 PHP yield 在 生成器用法,不帶 foreachfor, while 迴圈的那種。就討論 yield 將一個函式變成為生成器的用法。

關於yield 特性,是在開發 PHP5 時被提上日程,PHP5.5 版本正式加入。

關於yield的使用,我看到大部分文章都停留在,使用yield如何在foreach中穿出資料,今天想給大家講講 生成器 所有語法。

官網講解

生成器允許你在 foreach 程式碼塊中寫程式碼來迭代一組資料而不需要在記憶體中建立一個陣列, 那會使你的記憶體達到上限,或者會佔據可觀的處理時間。相反,你可以寫一個生成器函式,就像一個普通的自定義函式一樣, 和普通函式只返回一次不同的是, 生成器可以根據需要 yield 多次,以便生成需要迭代的值。

看了下官網對他講解:php.net 生成器語法 . 每個字都認識,但似乎還是體會到它講的內涵。官網我們主要看兩部分內容:

  1. yield 的語法。

  2. 程式碼例子。

先說語法, yield 的左邊是一個賦值語句,右邊可以是值(也可是表示式) 。而yield 會先執行右邊的表示式,並把值$value送到生成器外面。當生成器收到值後,會執行yield左邊的語句,賦值給$data.

<?php
function func()
{
    $data = (yield [$express]);
}

語法就這樣,估計大家還是有些懵,那就看看官網下面程式碼例子吧,我看裡面例子參差不齊。

注意yield 外面包的這一層括號,如果是在php5.5,右側$express的優先順序是判斷,可能會比左側$data的賦值語句低的。所以在php5用yield,yield 右邊是可執行表示式,左側需要接受返回並賦值,那麼這個括號是有必要的。在php7不會有這個問題。

通過例子來了解它

不論是學 人類語言,計算機語言,都是模仿開始

對於一個用人類語言來描述,都不那麼明晰時,所以那就通過例子告訴你它能做什麼,不能做什麼。

相關程式碼,我放到gitee了,希望你能複製到你本地執行下,親自執行感受下,有助於了理解接下來的內容。

git clone gitee.com/xupaul/PHP-generator-yie...

怎樣才能產生 Generator

先定義一個函式,在函式內 寫個 yield 關鍵詞,將這個函式呼叫賦值給一個變數。一個生成器就產生了。

程式碼 /php-yield-test/yieldFunctions.php 是生成器按照不同語法組合定義了多個生成器。

測試程式碼 /php-yield-test/whatIsGenerator.php,用來檢查哪些函式能構成生成器,哪些不能。執行結果如下

test result

  1. 函式內必須有 yield 關鍵詞,函式可以是全劇函式,或者類的方法。
  2. 哪怕 yield 肯定不會被執行,也會產生生成器。見:yield_func4
  3. 光禿禿 的 yield 關鍵詞就行(不向外送出,不處理外面的輸入)。見: yield_func2
  4. 函式內使用 生成器 並不能讓自己也成為生成器,見:yield_func5
  5. eval函式中直接執行 yield 會報錯, 見:yield_func11

是的,函式內有沒有foreach,while,for 語句都不是關鍵,關鍵是 yield. 生成器的型別判斷用 $gen instanceof Generator

生成器的函式

Generator 物件是從 generators返回的.

Generator 物件不能通過 new 例項化.

摘自 php.net generator

看著以上方法,是不想起了Iterator, 他們的確很像。同時注意,官網zh語言版本的文件沒有索引方法getReturn,訪問也是404。文件以en版為準,ch做參考。

以上就是生成器所有的方法,我們一個個來看。

測試方法程式碼 /php-yield-test/generatorMothod.php, 這裡面對每個方法都有使用舉例,執行結果如下。

run result 2

run result 3

好接下來對舉例做個一一講解。

Generator::current

  • 返回當前產生的值
<?php
function yield_func()
{
    yield 12;
    return 'a';
}

$gen = yield_func();
$re = $gen->current();
echo 'current return : ' . $re;

輸出:

current return : 12

看到 php-yield-test/generatorMothod.php 程式碼。

通過第一個程式碼事例,可得,對一個generator呼叫current方法,才算真正開始執行。執行到yield為止。如果不能命中yield,則執行到函式結束。

非generoator會立馬執行並得到結果,而非一個生成器物件。

通過例子2,呼叫current一次,兩次呢,第一次可以看到程式碼執行日誌,第二次,只是把上一次的結果返回給我們而已,並不是讓該生成器重新執行。

通過例子1,呼叫該函式還會獲取到返回值,返回的內容就是 yield 表示式左邊的內容。如果表示式無內容,則是NULL.

Generator::send

  • 向生成器yield點中傳入一個值,並返回下一次current值。
<?php
function yield_func()
{
    $data = yield 12;
    echo 'get yield data: ' . $data;
    return 'a';
}

$gen = yield_func();
$re = $gen->current();
$gen->send(32);

輸出:

get yield data: 32

例子3,是一個current,send的常規呼叫。呼叫current程式碼執行yield等到使用者send輸入引數。接收到輸入後,繼續執行。current能夠接收到yield彈出的值,send返回值為空。

例子4,直接呼叫send,相當於呼叫current,send。不過current的返回值,並不會通過send傳給使用者。

例子21中,可以看到直接呼叫send(1),會執行生成器,並向第一個yield處輸入1,繼續執行至下一個yield的返回值value。所以,$gen->send(2),和 $gen->current() 結果都是同一個值。

也就是說:跳過current,直接呼叫send,會丟失第一次yield的彈出值。

Generator::next

  • 跳過中斷,並讓生成器繼續執行
<?php
function yield_func()
{
    echo 'run to code line: ' . __LINE__ . PHP_EOL;
    yield;
    echo 'run to code line: ' . __LINE__ . PHP_EOL;
    return $result;
}

$gen = yield_func();
$gen->current();
echo 'current called' . PHP_EOL;
$gen->next();

輸出:

run to code line: 4
current called
run to code line: 6

例子5,這是一個較為常規的呼叫,呼叫current程式碼執行yield等到使用者輸入,這是呼叫next跳過,讓程式碼繼續執行。

例子6,直接呼叫next,相當於呼叫currentnext。而且通過最後列印$result, 我們發現怎麼有點像在呼叫 $gen->send(NULL);

Generator::rewind

  • 重置迭代器
<?php
function yield_func()
{
    echo 'run to code line: ' . __LINE__ . PHP_EOL;
    $result = yield 12;
    echo 'run to code line: ' . __LINE__ . PHP_EOL;
}

$gen = yield_func();
echo 'call yield_func rewind ' . PHP_EOL;
$gen->rewind();

輸出:

call yield_func rewind 
run to code line: 4

例子7,8 中,發現呼叫該方法,會導致隱式呼叫current

例子9 中,發現在執行過一個yield程式碼段後,再次呼叫該方法,會導致報錯(哪怕該 生成器已結束)。

Generator::throw

  • 向生成器中拋入一個異常
<?php
function yield_func()
{
    try {
        $re = yield 'exception';
    } catch (Exception $e) {
        echo 'catched exception msg: ' .$e->getMessage();
    }
}

$gen = yield_func();
$gen->throw(new \Exception('new yield  exception'));

輸出:

catched exception msg: new yield  exception

通過以上簡單的例子可得,throw 就是讓yield這行程式碼產生異常,讓外面的try catch 捕獲我們生成的那個異常。

例子11中,構造生成器,並呼叫current方法,執行到yield處,再呼叫throw,就能捕獲到異常。

例子12中,當呼叫send方法,跳過函式內yield程式碼時,再呼叫throw傳入異常,就沒法捕獲了。

Generator::valid

  • 檢查迭代器是否被關閉
<?php
function yield_func()
{
    yield 12;
    return 'a';
}

$gen = yield_func();
$gen->send(1);
$check = $gen->valid();
echo 'the generator valid ? ' . intval($check);

輸出:

the generator valid ? 0

例子12中,發現current被隱式呼叫。

例子13中,可得,當生成器執行到yield程式碼段時,用valid函式檢查,都會返回true

所以,別問我是否已執行,問就是執行。該方法用來獲取是否關閉狀態,不是 是否執行狀態!執行到底,執行到return就是 關閉狀態。

Generator::key

  • 返回當前產生的鍵
<?php
function yield_func()
{
    yield 1 => 'abc';
}

$gen = yield_func();
echo 'value is :' . $gen->current() . PHP_EOL;
echo 'key is: ' . $gen->key() . PHP_EOL;

輸出:

value is :abc
key is: 1

從以上例子中,可得yield可顯示設定返回的key.

例子15 中,發現key的分發規律和PHP陣列鍵值發放策略是差不多的,預設從0開始,未指定則是以上一個數字key+1作為當前的key.

例子16 中,我們又發現current被隱式呼叫。

Generator::__wakeup

  • Generator::__wakeup — 序列化回撥
<?php
function yield_func()
{
    yield 1 => 'abc';
}
$gen = yield_func();
try {
$ser = serialize($gen);
} catch (\Exception $e) {
    print_r($e->getMessage());
}

輸出:

Serialization of 'Generator' is not allowed

這是一個魔術方法,見 PHP 魔術方法,也就是說 生成器 不能被序列化成一個字串。

例子17就不用說了,看下例子18,看樣子序列化成功了。也就是說一個生成器做為一個方法可以被序列化,當函式變成生成器時,就不能被序列化了。

Generator::getReturn

<?php
function yield_func()
{
    yield 1 => 'abc';
    return 32;
}

$gen = yield_func();
$gen->send(0);
echo 'call yield_func return, and get: ' . $gen->getReturn();

輸出:

call yield_func return, and get: 32

該函式就是獲取生成器最後的返回值。如果沒有return語句,或者沒有執行到return語句,呼叫該函式得到的就是NULL。

例子19 可得,getReturn 能夠獲取到生成器最後的返回值。

例子19、20 可得,當生成器沒有執行到return語句,或者沒有執行到最後時,呼叫getReturn是會導致報錯。

綜上所述

到這裡,我們就發現rewind,next__wakeup 這兩個函式感覺沒啥叼用呢,為啥還存在呢,因為Generator繼承Iterator,自然就有了rewind, next方法,PHP 雖然支援方法覆蓋,但子類的訪問修飾符 不能縮緊,所以Generator只能重寫這兩個方法。 __wakeup 繼承自 stdClass

狀態轉換

看圖:

PHP yield 生命週期圖

畫了兩個狀態轉換圖,上面的要細緻,繁複一點。下面的精簡版,便於快速理解。

總結

以上就是關於 PHP 生成器的基礎內容,希望你看了後對它有更進一步認識。下一講,我們手把手一起來做一個任務排程器,實戰一下。

有問題歡迎提問,謝謝大家!

沒人比我更懂

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章