PHP的生成器、yield和協程


PHP的生成器、yield和協程

雖然之前就接觸了PHP的yield關鍵字和與之對應的生成器,但是一直沒有場景去使用它,就一直沒有對它上心的研究。不過公司的框架是基於php的協程實現,覺得有必要深入的瞅瞅了。

由於之前對於生成器接觸不多,后來也是在看了鳥哥的介紹在PHP中使用協程實現多任務調度才有所了解。下面也只是說說我的理解。

迭代和迭代器

在了解生成器之前我們先來看一下迭代器和迭代。迭代是指反復執行一個過程,每執行一次叫做迭代一次。比如普通的遍歷便是迭代:

$arr = [1, 2, 3, 4, 5];

foreach($arr as $key => $value) {
	echo $key . ' => ' . $value . "\n";
}

我們可以看到通過foreach對數組遍歷並迭代輸出其內容。在foreach內部,每次迭代都會將當前的元素的值賦給$value並將數組的指針移動指向下一個元素為下一次迭代坐准備,從而實現順序遍歷。像這樣能夠讓外部的函數迭代自己內部數據的接口就是迭代器接口,對應的那個被迭代的自己就是迭代器對象

PHP提供了統一的迭代器接口:

Iterator extends Traversable {

	// 返回當前的元素
	abstract public mixed current(void)
	// 返回當前元素的鍵
	abstract public scalar key(void)
	// 向下移動到下一個元素
	abstract public void next(void)
	// 返回到迭代器的第一個元素
	abstract public void rewind(void)
	// 檢查當前位置是否有效
	abstract public boolean valid(void)
}

通過實現Iterator接口,我們可以自行的決定如何遍歷對象。比如通過實現Iterator接口我們可以觀察迭代器的調用順序。


class MyIterator implements Iterator {
	private $position = 0;
	private $arr = [
		'first', 'second', 'third',
	];
	
	public function __construct() {
		$this->position = 0;
	}
	
	public function rewind() {
		var_dump(__METHOD__);
		$this->position = 0;
	}
	
	public function current() {
		var_dump(__METHOD__);
		return $this->arr[$this->position];
	}
	
	public function key() {
		var_dump(__METHOD__);
		return $this->position;
	}
	
	public function next() {
		var_dump(__METHOD__);
		++$this->position;
	}
	
	public function valid() {
		var_dump(__METHOD__);
		return isset($this->arr[$this->position]);
	}
	
}

$it = new MyIterator();

foreach($it as $key => $value) {
	echo "\n";
	var_dump($key, $value);
}

通過這個例子能夠清楚的看到了foreach循環中調用的順序。從例子也能看出通過迭代器能夠將一個普通的對象轉化為一個可被遍歷的對象。這在有些時候,能夠將一個普通的UsersInfo對象轉化為一個可以遍歷的對象,那么就不需要通過UsersInfo::getAllUser()獲取一個數組然后遍歷數組,而且還可以在對象中對數據進行預處理。

yield和生成器

相比較迭代器,生成器提供了一種更容易的方法來實現簡單的對象迭代,性能開銷和復雜性都大大降低。

一個生成器函數看起來像一個普通的函數,不同的是普通函數返回一個值,而一個生成可以yield生成許多它所需要的值,並且每一次的生成返回值只是暫停當前的執行狀態,當下次調用生成器函數時,PHP會從上次暫停的狀態繼續執行下去。

我們在使用生成器的時候可以像關聯數組那樣指定一個鍵名對應生成的值。如下生成一個鍵值對與定義一個關聯數組相似。


function xrange($start, $limit, $step = 1) {
	for ($i = $start, $j = 0; $i <= $limit; $i += $step, $j++) {
		// 給予鍵值
		yield $j => $i;
	}
}

$xrange = xrange(1, 10, 2);
foreach ($xrange as $key => $value) {
	echo $key . ' => ' . $value . "\n";
}

更多的生成器語法可以參見生成器語法

實際上生成器函數返回的是一個Generator對象,這個對象不能通過new實例化,並且實現了Iterator接口。


Generator implements Iterator {
	public mixed current(void)
	public mixed key(void)
	public void next(void)
	public void rewind(void)
	// 向生成器傳入一個值
	public mixed send(mixed $value)
	public void throw(Exception $exception)
	public bool valid(void)
	// 序列化回調
	public void __wakeup(void)
}

可以看到出了實現Iterator的接口之外Generator還添加了send方法,用來向生成器傳入一個值,並且當做yield表達式的結果,然后繼續執行生成器,直到遇到下一個yield后會再次停住。

function printer() {
	while(true) {
		echo 'receive: ' . yield . "\n";
	}
}

$printer = printer();
$printer->send('Hello');
$printer->send('world');

以上的例子會輸出:

receive: Hello
receive: world

在上面的例子中,經過第一個send()方法,yield表達式的值變為Hello,之后執行echo語句,輸出第一條結果receive: Hello,輸出完畢后繼續執行到第二個yield處,只不過當前的語句沒有執行到底,不會執行輸出。如果將例子改改就能夠看出來yield的繼續執行到哪里。


function printer() {
	$i = 1;
	while(true) {
		echo 'this is the yield ' . $i . "\n";
		echo 'receive: ' . yield . "\n";
		$i++;
	}
}

$printer = printer();
$printer->send('Hello');
$printer->send('world');

這次的輸出便會變為:

this is the yield 1
receive: hello
this is the yield 2
receive: world
this is the yield 3

這邊可以清楚的看出send之后的繼續執行到第二個yield處,之前的代碼照常執行。

我們再對例子進行適當的修改:


function printer() {
	$i = 1;
	while(true) {
		echo 'this is the yield ' . (yield $i) . "\n";
		$i++;
	}
}

$printer = printer();
var_dump($printer->send('first'));
var_dump($printer->send('second'));

執行一下會發現結果為:

this is the yield first
int(2)
this is the yield second
int(3)

讓我們來看一下,是不是發現了問題,跑出來的結果不是從1開始的而是從2開始,這是為啥嘞,我們來分析一下:

在此之前我們先來跑另外一段代碼:


function printer() {
	$i = 1;
	while(true) {
		echo 'this is the yield ' . (yield $i) . "\n";
		$i++;
	}
}

$printer = printer();
var_dump($printer->current());
var_dump($printer->send('first'));
var_dump($printer->send('second'));

這個時候我們會發現執行的結果變成了:

int(1)
this is the yield first
int(2)
this is the yield second
int(3)

可以看到在第一次調用生成器函數的時候,生成器已經執行到了第一個yield表達式處,所以在$printer->send('first')之前,生成器便已經yield 1出來了,只是沒有對這個生成的值進行接收處理,在send()了之后,echo語句便會緊接着完整的執行,執行完畢繼續執行$i++,下次循環便是var_dump(2)

至此,我們看到了yield不僅能夠返回數據而且還可以接收數據,而且兩者可以同時進行,此時yield便成了數據雙向傳輸的工具,這就為了實現協程提供了可能性。

至於接下來的協程的知識,水平有限不好介紹,還是看鳥哥的原文比較直接,里面例子很豐富,介紹的很詳盡。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM