php 迭代器是一种 PHP 设计模式,是指可在内部迭代自己的外部迭代器或类的接口;迭代器模式可以在不需要了解内部实现的前提下,遍历一个聚合对象的内部元素。
PHP5 开始内置了 Iterator
即迭代器接口,所以如果你定义了一个类,并实现了 Iterator
接口,那么你的这个类对象就是 ZEND_ITER_OBJECT
即可迭代的,否则就是 ZEND_ITER_PLAIN_OBJECT
。
对于 ZEND_ITER_PLAIN_OBJECT
的类,foreach
会获取该对象的默认属性数组,然后对该数组进行迭代。
而对于 ZEND_ITER_OBJECT
的类对象,则会通过调用对象实现的 Iterator
接口相关函数来进行迭代。
任何实现了 Iterator
接口的类都是 可迭代的 ,即都可以用 foreach
语句来遍历。
Iterator 接口
interface Iterator extends Traversable {
// 获取当前内部标量指向的元素的数据
public mixed current()
// 获取当前标量
public scalar key()
// 移动到下一个标量
public void next()
// 重置标量
public void rewind()
// 检查当前标量是否有效
public boolean valid()
}
常规实现 range 函数
PHP 自带的 range 函数原型:
- range — 根据范围创建数组,包含指定的元素
- array range (mixed start , mixed end [, number $step = 1 ])
- 建立一个包含指定范围单元的数组。
在不使用迭代器的情况要实现一个和 PHP 自带的range
函数类似的功能,可能会这么写:
function range ($start, $end, $step = 1){
$ret = [];
for ($i = $start; $i <= $end; $i += $step) {
$ret[] = $i;
}
return $ret;
}
需要将生成的所有元素放在内存数组中,如果需要生成一个非常大的集合,则会占用巨大的内存。
迭代器实现 xrange 函数
来看看迭代实现的 range
,我们叫做 xrange
,他实现了 Iterator
接口必须的 5 个方法:
class Xrange implements Iterator {
protected $start;
protected $limit;
protected $step;
protected $current;
public function __construct($start, $limit, $step = 1) {
$this->start = $start;
$this->limit = $limit;
$this->step = $step;
}
public function rewind() {
$this->current = $this->start;
}
public function next() {
$this->current += $this->step;
}
public function current() {
return $this->current;
}
public function key() {
return $this->current + 1;
}
public function valid() {
return $this->current <= $this->limit;
}
}
使用时代码如下:
foreach (new Xrange(0, 9) as $key => $val) {
echo $key, ' ', $val, "\n";
}
输出:
0 0
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
看上去功能和 range()
函数所做的一致,不同点在于迭代的是一个 对象(Object)
而不是数组:
var_dump(new Xrange(0, 9));
输出:
object(Xrange)#1 (4) {
["start":protected]=>
int(0)
["limit":protected]=>
int(9)
["step":protected]=>
int(1)
["current":protected]=>
NULL
}
另外,内存的占用情况也完全不同:
// range
$startMemory = memory_get_usage();
$arr = range(0, 500000);
echo 'range(): ', memory_get_usage() - $startMemory, " bytes\n";
unset($arr);
// xrange
$startMemory = memory_get_usage();
$arr = new Xrange(0, 500000);
echo 'xrange(): ', memory_get_usage() - $startMemory, " bytes\n";
输出:
xrange(): 624 bytes
range(): 72194784 bytes
range()
函数在执行后占用了 50W 个元素内存空间,而 xrange
对象在整个迭代过程中只占用一个对象的内存。
Yii2 Query
在喜闻乐见的各种 PHP 框架里有不少生成器的实例,比如 Yii2 中用来构建 SQL 语句的 \yii\db\Query
类:
$query = (new \yii\db\Query)->from('user');
// yii\db\BatchQueryResult
foreach ($query->batch() as $users) {
// 每次循环得到多条 user 记录
}
来看一下 batch()
做了什么:
/**
* Starts a batch query.
*
* A batch query supports fetching data in batches, which can keep the memory usage under a limit.
* This method will return a [[BatchQueryResult]] object which implements the [[\Iterator]] interface
* and can be traversed to retrieve the data in batches.
*
* For example,
*
*
* $query = (new Query)->from('user');
* foreach ($query->batch() as $rows) {
* // $rows is an array of 10 or fewer rows from user table
* }
*
*
* @param integer $batchSize the number of records to be fetched in each batch.
* @param Connection $db the database connection. If not set, the "db" application component will be used.
* @return BatchQueryResult the batch query result. It implements the [[\Iterator]] interface
* and can be traversed to retrieve the data in batches.
*/
public function batch($batchSize = 100, $db = null)
{
return Yii::createObject([
'class' => BatchQueryResult::className(),
'query' => $this,
'batchSize' => $batchSize,
'db' => $db,
'each' => false,
]);
}
实际上返回了一个 BatchQueryResult
类,类的源码实现了 Iterator
接口 5 个关键方法:
class BatchQueryResult extends Object implements \Iterator
{
public $db;
public $query;
public $batchSize = 100;
public $each = false;
private $_dataReader;
private $_batch;
private $_value;
private $_key;
/**
* Destructor.
*/
public function __destruct()
{
// make sure cursor is closed
$this->reset();
}
/**
* Resets the batch query.
* This method will clean up the existing batch query so that a new batch query can be performed.
*/
public function reset()
{
if ($this->_dataReader !== null) {
$this->_dataReader->close();
}
$this->_dataReader = null;
$this->_batch = null;
$this->_value = null;
$this->_key = null;
}
/**
* Resets the iterator to the initial state.
* This method is required by the interface [[\Iterator]].
*/
public function rewind()
{
$this->reset();
$this->next();
}
/**
* Moves the internal pointer to the next dataset.
* This method is required by the interface [[\Iterator]].
*/
public function next()
{
if ($this->_batch === null || !$this->each || $this->each && next($this->_batch) === false) {
$this->_batch = $this->fetchData();
reset($this->_batch);
}
if ($this->each) {
$this->_value = current($this->_batch);
if ($this->query->indexBy !== null) {
$this->_key = key($this->_batch);
} elseif (key($this->_batch) !== null) {
$this->_key++;
} else {
$this->_key = null;
}
} else {
$this->_value = $this->_batch;
$this->_key = $this->_key === null ? 0 : $this->_key + 1;
}
}
/**
* Fetches the next batch of data.
* @return array the data fetched
*/
protected function fetchData()
{
// ...
}
/**
* Returns the index of the current dataset.
* This method is required by the interface [[\Iterator]].
* @return integer the index of the current row.
*/
public function key()
{
return $this->_key;
}
/**
* Returns the current dataset.
* This method is required by the interface [[\Iterator]].
* @return mixed the current dataset.
*/
public function current()
{
return $this->_value;
}
/**
* Returns whether there is a valid dataset at the current position.
* This method is required by the interface [[\Iterator]].
* @return boolean whether there is a valid dataset at the current position.
*/
public function valid()
{
return !empty($this->_batch);
}
}
以迭代器的方式实现了类似分页取的效果,同时避免了一次性取出所有数据占用太多的内存空间。
迭代器优缺点分析:
优点:
- 支持多种遍历方式。比如有序列表,我们根据需要提供正序遍历、倒序遍历两种迭代器。用户只需要得到我们的迭代器,就可以对集合执行遍历操作
- 简化了聚合类。由于引入了迭代器,原有的集合对象不需要自行遍历集合元素了
- 增加新的聚合类和迭代器类很方便,两个维度上可各自独立变化
- 为不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上操作
缺点:
迭代器模式将存储数据和遍历数据的职责分离增加新的集合对象时需要增加对应的迭代器类,类的个数成对增加,在一定程度上增加系统复杂度。
迭代器使用场景
- 使用返回迭代器的包或库时(如 PHP5 中的 SPL 迭代器)
- 无法在一次调用获取所需的所有元素时
- 要处理数量巨大的元素时(数据库中要处理的结果集内容超过内存)
- ...