【PHP】有關PHP生成器的使用
參考文章:
https://mp.weixin.qq.com/s/R3MtOvi-hQiCMfDzSj6bzw
一、什麼是生成器?
生成器其實就是一個用於迭代的迭代器。它提供了一種更容易的方式來實現物件迭代,相比定義類實現Iterator介面的方式大大降低效能開銷和複雜度。
生成器必須在方法中並使用 yield 關鍵字;其次每一個 yield 都可以看作是一次 return ;最後外部迴圈時每次迴圈取一個 yield 的返回值。
function test1()
{
for ($i = 0; $i < 3; $i++) {
yield $i + 1;
//每一個yield都可以看成一次return,這是重點
}
yield 1000;
yield 1001;
}
foreach (test1() as $t) {
echo $t, PHP_EOL;
//PHP_EOL:PHP 換行符,相當於Windows平臺的echo "\r\n"
}
//輸出1 2 3 1000 1001,一共5次
在上面例子中,迴圈了3次並返回了1、2、3這三個數字。然後在迴圈外部又寫了2行 yield 分別輸出了1000和1001。因此外部的 foreach迴圈一共輸出了5次。
明明是一個方法,為什麼它能夠迴圈,而且返回一種迴圈體的格式。我們直接列印這個 test() 方法看看列印的是什麼:
// 是一個生成器物件
var_dump (test1());
//輸出: object(Generator)#2 (0) { }
上面的例子告訴你:當使用了 yield 進行內容返回後,返回的是一個 Generator 物件。這個物件就叫作生成器物件,這個物件是不能直接被 new 例項化,只能通過生成器函式這種方式返回例項。
Generator這個類包含 current() 、 key() 等方法,最主要的這個類實現了 Iterator 介面,也就是說它就是一個特殊的迭代器類。
Generator implements Iterator {
/* 方法 */
public current ( void ) : mixed
public key ( void ) : mixed
public next ( void ) : void
public rewind ( void ) : void
public send ( mixed $value ) : mixed
public throw ( Exception $exception ) : void
public valid ( void ) : bool
public __wakeup ( void ) : void
}
二、生成器有什麼用?
生成器最強大的部分就在於它不需要一個數組或者任何的資料結構來儲存這一系列資料。每次迭代執行到 yield 時都是動態返回的。所以生成器能在很大程度上節約記憶體。
// 記憶體佔用測試
$start_time = microtime(true);
function test2($clear = false)
{
$arr = [];
if($clear){
$arr = null;
return;
}
for ($i = 0; $i < 1000000; $i++) {
$arr[] = $i + 1;
}
return $arr;
}
$array = test2();
foreach ($array as $val) {
}
$end_time = microtime(true);
echo "time: ", bcsub($end_time, $start_time, 4), PHP_EOL;
echo "memory (byte): ", memory_get_usage(true), PHP_EOL;
// time: 0.0513
// memory (byte): 35655680
$start_time = microtime(true);
function test3()
{
for ($i = 0; $i < 1000000; $i++) {
yield $i + 1;
}
}
$array = test3();
foreach ($array as $val) {
}
$end_time = microtime(true);
echo "time: ", bcsub($end_time, $start_time, 4), PHP_EOL;
echo "memory (byte): ", memory_get_usage(true), PHP_EOL;
// time: 0.0517
// memory (byte): 2097152
以上程式碼進行 1000000 個迴圈後獲取結果的簡單操作,你可以可以直觀地看出使用生成器的版本僅僅消耗了大約 2M 的記憶體,而未使用生成器的卻消耗了 35M 的記憶體,有10多倍的差距了,而且越大的資料量差距會更加明顯。所以又有人說生成器是PHP中最不可被低估了的一個特性。
【思考】為什麼會有那麼大差別呢?什麼原因造成的呢?
生成器的應用
1.返回空值以及中斷
生成器可以返回空值,也就是說不帶任何值就可以返回一個空值了,你直接 yield就好了;用return來中斷生成器的繼續執行。
下面的程式碼我們在 $i = 4; 的時候返回了空值,也就是不會輸出 5 (因為我們返回的是 $i + 1 )。然後在 $i == 7 的時候使用 return;來中斷生成器的繼續執行,也就是說迴圈最多隻會輸出到 7 ,就結束了。
// 返回空值以及中斷
function test4()
{
for ($i = 0; $i < 10; $i++) {
if ($i == 4) {
yield; // 返回null值
}
if ($i == 7) {
return; // 中斷生成器執行
}
yield $i + 1;
}
}
foreach (test4() as $t) {
echo $t, PHP_EOL;
}
//輸出1 2 3 4 5 6 7
在4和5之間其實輸出了一個空值null,我們改一下程式碼,就能更好的理解生成器了。
function test4()
{
for ($i = 0; $i < 10; $i++) {
if ($i == 4) {
yield "看這裡"; // 返回字串
}
if ($i == 7) {
return; // 中斷生成器執行
}
yield $i + 1; //1~7是這裡輸出的
}
}
foreach (test4() as $t) {
echo $t, PHP_EOL;
}
//輸出:1 2 3 4 看這裡 5 6 7
2.返回鍵值對形式
生成器也可以返回鍵值對形式的可遍歷物件,可以供 foreach 使用的,語法:yield key => value; 和陣列項的定義形式一樣,非常直觀。程式碼如下:
function test5()
{
for ($i = 0; $i < 10; $i++) {
yield 'key.' . $i => $i + 1;
}
}
foreach (test5() as $k=>$t) {
echo $k . ':' . $t, PHP_EOL;
}
// key.0:1
// key.1:2
// key.2:3
// key.3:4
// key.4:5
// key.5:6
// key.6:7
// key.7:8
// key.8:9
// key.9:10
外部傳遞資料
我們也可以通過 Generator::send 方法來向生成器中傳入一個值。這個值將會被當做生成器當前 yield 的返回值。
正常獲取迴圈值,當外部send過來值後,yield獲取到的就是外部傳來的值了。記住:變數獲取 yield 的值時必須要用括號括起來。
function test6()
{
for ($i = 0; $i < 10; $i++) {
// 正常獲取迴圈值,當外部send過來值後,yield獲取到的就是外部傳來的值了
$data = (yield $i + 1);
if($data == 'stop'){
return;
}
}
}
$t6 = test6();
foreach($t6 as $t){
if($t == 3){
$t6->send('stop');
}
echo $t, PHP_EOL;
}
// 1
// 2
// 3
上面的test6()的作用是輸出1~10,正常情況下用foreach()去迴圈test6()是輸出1到10的,但是當輸出到3的時候,我們向test6裡面send了一個stop字串,yield獲取到外部傳來的值,進行判斷,條件成立,中斷了迴圈,因此上面的程式碼只輸出了1 2 3
yield from 語法
這種語法其實就是指從另一個可迭代物件中一個一個的獲取資料並形成生成器返回。看程式碼比較好理解:
在 下面的方法中使用了 yield from 分別從普通陣列、迭代器物件和另一個生成器中獲取了資料,並做為當前生成器的內容進行返回。
function test7()
{
yield from [1, 2, 3, 4];
yield from new ArrayIterator([5, 6]);
yield from test1();
}
foreach (test7() as $t) {
echo 'test7:', $t, PHP_EOL;
}
// test7:1
// test7:2
// test7:3
// test7:4
// test7:5
// test7:6
// test7:1
// test7:2
// test7:3
// test7:1000
生成器可以用count獲取數量嗎?
測試一下就知道了,生成器是不能用count來獲取它的數量的。使用 count 獲取生成器的數量將會直接 Warning 警告。直接輸出將會一直顯示是 1 ,因為 count 的特性(強制轉換成陣列都會顯示 1 )。
$c = count(test1());
// Warning: count(): Parameter must be an array or an object that implements Countable
// echo $c, PHP_EOL;
總結:
yield生成器不僅大大的節約了記憶體的開銷,而且語法其實也非常的簡潔明瞭。我們不需要在方法內部再多定義一個數組去儲存返回值,直接 yield 就可以了。在實際的專案中你可以多多的使用這種生成器哦!