如何在 PHP 7.4+ 中以一种有效的方式触发代码 before/after 一个 foreach 循环,只有当这个循环被输入时?
How to trigger code before/after a foreach loop, only if this one would be entered, in an efficient way in PHP 7.4+?
这有点类似于问题:How to determine the first and last iteration in a foreach loop?,但是,这个 +10 岁的问题在很大程度上是 array
导向的,并且 none 的答案与许多事实兼容可以循环播放不同的类型。
在 PHP >= 7.4(数组、迭代器、生成器、PDOStatement
、DatePeriod
, 对象属性,...), 我们如何以有效的方式触发需要在循环之前/之后发生的代码, 但仅在进入循环的情况下?
一个典型的用例可能是生成一个 HTML 列表:
<ul>
<li>...</li>
<li>...</li>
...
</ul>
<ul>
和</ul>
必须打印只有如果有一些元素。
这些是我目前发现的约束:
empty()
: Can't be used on generators/iterators.
each()
:已弃用。
iterator_to_array()
打败了发电机的优势
- A boolean flag tested inside and after the loop is not considered efficient 因为它会导致每次都执行该测试单次迭代,而不是在循环开始时一次,在循环结束时一次。
- 虽然在上面的例子中可以使用输出缓冲或字符串连接来生成输出,但它不适合循环不产生任何输出的情况。 (感谢 @barmar 的额外想法)
下面的代码片段总结了许多我们可以迭代的不同类型foreach
,它可以作为提供答案的开始:
<?php
// Function that iterates on $iterable
function iterateOverIt($iterable) {
// How to generate the "<ul>"?
foreach ($iterable as $item) {
echo "<li>", $item instanceof DateTime ? $item->format("c") : (
isset($item["col"]) ? $item["col"] : $item
), "</li>\n";
}
// How to generate the "</ul>"?
}
// Empty array
iterateOverIt([]);
iterateOverIt([1, 2, 3]);
// Empty generator
iterateOverIt((function () : Generator {
return;
yield;
})());
iterateOverIt((function () : Generator {
yield 4;
yield 5;
yield 6;
})());
// Class with no public properties
iterateOverIt(new stdClass());
iterateOverIt(new class { public $a = 7, $b = 8, $c = 9;});
$db = mysqli_connect("localhost", "test", "test", "test");
// Empty resultset
iterateOverIt($db->query("SELECT 0 FROM DUAL WHERE false"));
iterateOverIt($db->query("SELECT 10 AS col UNION SELECT 11 UNION SELECT 12"));
// DatePeriod generating no dates
iterateOverIt(new DatePeriod(new DateTime("2020-01-01 00:00:00"), new DateInterval("P1D"), new DateTime("2020-01-01 00:00:00"), DatePeriod::EXCLUDE_START_DATE));
iterateOverIt(new DatePeriod(new DateTime("2020-01-01 00:00:00"), new DateInterval("P1D"), 3, DatePeriod::EXCLUDE_START_DATE));
这样的脚本应该产生以下输出:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<ul>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
<ul>
<li>7</li>
<li>8</li>
<li>9</li>
</ul>
<ul>
<li>10</li>
<li>11</li>
<li>12</li>
</ul>
<ul>
<li>2020-01-02T00:00:00+00:00</li>
<li>2020-01-03T00:00:00+00:00</li>
<li>2020-01-04T00:00:00+00:00</li>
</ul>
好吧,这至少是一个有趣的思想实验...
class BeforeAfterIterator implements Iterator
{
private $iterator;
public function __construct(iterable $iterator, $before, $after)
{
if (!$iterator instanceof Iterator) {
$iterator = (function ($iterable) {
yield from $iterable;
})($iterator);
}
if ($iterator->valid()) {
$this->iterator = new AppendIterator();
$this->iterator->append(new ArrayIterator([$before]));
$this->iterator->append($iterator);
$this->iterator->append(new ArrayIterator([$after]));
} else {
$this->iterator = new ArrayIterator([]);
}
}
public function current()
{
return $this->iterator->current();
}
public function next()
{
$this->iterator->next();
}
public function key()
{
return $this->iterator->key();
}
public function valid()
{
return $this->iterator->valid();
}
public function rewind()
{
$this->iterator->rewind();
}
}
用法示例:
function generator() {
foreach (range(1, 5) as $x) {
yield "<li>$x";
}
}
$beforeAfter = new \BeforeAfterIterator(generator(), '<ul>', '</ul>');
foreach ($beforeAfter as $value) {
echo $value, PHP_EOL;
}
输出
<ul>
<li>1
<li>2
<li>3
<li>4
<li>5
</ul>
如果您传递的迭代器没有产生任何值,您将没有输出。
我绝不赞同这是个好主意,这完全取决于您。我确信有一些更优雅的方法可以使用一些更晦涩的 SPL 类 来做到这一点。通过扩展而不是合成也可能更简单。
我认为我们不需要向该语言添加任何新内容。在 PHP 7.1 中,我们收到了一个新类型 iterable
。使用类型提示,你可以强制你的 function/method 只接受可以被迭代并且在语义上应该被迭代的参数(与不实现 Traversable
的对象相反)。
// Function that iterates on $iterable
function iterateOverIt(iterable $iterable) {
echo '<ul>'.PHP_EOL;
foreach ($iterable as $item) {
echo "<li>", $item instanceof DateTime ? $item->format("c") : (
isset($item["col"]) ? $item["col"] : $item
), "</li>\n";
}
echo '</ul>'.PHP_EOL;
}
当然这并不能确保循环至少迭代一次。在您的测试用例中,您可以简单地使用输出缓冲,但这不适用于仅当循环至少迭代一次时才需要执行操作的情况。这不可能。实现这种一致性在技术上是不可能的。举个简单的例子:
class SideEffects implements Iterator {
private $position = 0;
private $IHoldValues = [1, 1, 2, 3, 5, 8];
public function __construct() {
$this->position = 0;
}
public function doMeBefore() {
$this->IHoldValues = [];
echo "HAHA!".PHP_EOL;
}
public function rewind() {
$this->position = 0;
}
public function current() {
return $this->IHoldValues[$this->position];
}
public function key() {
return $this->position;
}
public function next() {
++$this->position;
}
public function valid() {
return isset($this->IHoldValues[$this->position]);
}
}
$it = new SideEffects();
if (1/* Some logic to check if the iterator is valid */) {
$it->doMeBefore(); // causes side effect, which invalidates previous statement
foreach ($it as $row) {
echo $row.PHP_EOL;
}
}
正如您在这个鲜为人知的示例中看到的那样,您的代码不可能实现如此完美的连贯性。
我们在 PHP 中使用不同的方式迭代内容的原因是因为没有 "one size fits all" 解决方案。你想做的恰恰相反。您正在尝试创建一个解决方案,该解决方案适用于可以迭代的所有内容,即使它可能不应该被迭代。相反,您应该编写可以适当处理所有方法的代码。
function iterateOverIt(iterable $iterable) {
if (is_array($iterable)) {
echo 'The parameter is an array'.PHP_EOL;
if ($iterable) {
// foreach
}
} elseif ($iterable instanceof Iterator) {
echo 'The parameter is a traversable object'.PHP_EOL;
/**
* @var \Iterator
*/
$iterable = $iterable;
if ($iterable->valid()) {
// foreach
}
} elseif ($iterable instanceof Traversable) {
echo 'The parameter is a traversable object'.PHP_EOL;
// I donno, some hack?
foreach ($iterable as $i) {
// do PRE
foreach ($iterable as $el) {
// foreach calls reset first on traversable objects. Each loop should start anew
}
// do POST
break;
}
} else {
// throw new Exception?
}
}
如果你真的想要,你甚至可以使用 is_object()
在其中包含普通对象,但正如我所说,除非它实现 Traversable
不要迭代它。
适用于所有情况 (PHP >= 7.2) 的解决方案是使用双 foreach
, where the first one acts like an if
,它不会真正执行循环,但启动它:
function iterateOverIt($iterable) {
// First foreach acts like a guard condition
foreach ($iterable as $_) {
// Pre-processing:
echo "<ul>\n";
// Real looping, this won't start a *NEW* iteration process but will continue the one started above:
foreach ($iterable as $item) {
echo "<li>", $item instanceof DateTime ? $item->format("c") : (
isset($item["col"]) ? $item["col"] : $item
), "</li>\n";
}
// Post-processing:
echo "</ul>\n";
break;
}
}
这有点类似于问题:How to determine the first and last iteration in a foreach loop?,但是,这个 +10 岁的问题在很大程度上是 array
导向的,并且 none 的答案与许多事实兼容可以循环播放不同的类型。
在 PHP >= 7.4(数组、迭代器、生成器、PDOStatement
、DatePeriod
, 对象属性,...), 我们如何以有效的方式触发需要在循环之前/之后发生的代码, 但仅在进入循环的情况下?
一个典型的用例可能是生成一个 HTML 列表:
<ul>
<li>...</li>
<li>...</li>
...
</ul>
<ul>
和</ul>
必须打印只有如果有一些元素。
这些是我目前发现的约束:
empty()
: Can't be used on generators/iterators.each()
:已弃用。iterator_to_array()
打败了发电机的优势- A boolean flag tested inside and after the loop is not considered efficient 因为它会导致每次都执行该测试单次迭代,而不是在循环开始时一次,在循环结束时一次。
- 虽然在上面的例子中可以使用输出缓冲或字符串连接来生成输出,但它不适合循环不产生任何输出的情况。 (感谢 @barmar 的额外想法)
下面的代码片段总结了许多我们可以迭代的不同类型foreach
,它可以作为提供答案的开始:
<?php
// Function that iterates on $iterable
function iterateOverIt($iterable) {
// How to generate the "<ul>"?
foreach ($iterable as $item) {
echo "<li>", $item instanceof DateTime ? $item->format("c") : (
isset($item["col"]) ? $item["col"] : $item
), "</li>\n";
}
// How to generate the "</ul>"?
}
// Empty array
iterateOverIt([]);
iterateOverIt([1, 2, 3]);
// Empty generator
iterateOverIt((function () : Generator {
return;
yield;
})());
iterateOverIt((function () : Generator {
yield 4;
yield 5;
yield 6;
})());
// Class with no public properties
iterateOverIt(new stdClass());
iterateOverIt(new class { public $a = 7, $b = 8, $c = 9;});
$db = mysqli_connect("localhost", "test", "test", "test");
// Empty resultset
iterateOverIt($db->query("SELECT 0 FROM DUAL WHERE false"));
iterateOverIt($db->query("SELECT 10 AS col UNION SELECT 11 UNION SELECT 12"));
// DatePeriod generating no dates
iterateOverIt(new DatePeriod(new DateTime("2020-01-01 00:00:00"), new DateInterval("P1D"), new DateTime("2020-01-01 00:00:00"), DatePeriod::EXCLUDE_START_DATE));
iterateOverIt(new DatePeriod(new DateTime("2020-01-01 00:00:00"), new DateInterval("P1D"), 3, DatePeriod::EXCLUDE_START_DATE));
这样的脚本应该产生以下输出:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<ul>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
<ul>
<li>7</li>
<li>8</li>
<li>9</li>
</ul>
<ul>
<li>10</li>
<li>11</li>
<li>12</li>
</ul>
<ul>
<li>2020-01-02T00:00:00+00:00</li>
<li>2020-01-03T00:00:00+00:00</li>
<li>2020-01-04T00:00:00+00:00</li>
</ul>
好吧,这至少是一个有趣的思想实验...
class BeforeAfterIterator implements Iterator
{
private $iterator;
public function __construct(iterable $iterator, $before, $after)
{
if (!$iterator instanceof Iterator) {
$iterator = (function ($iterable) {
yield from $iterable;
})($iterator);
}
if ($iterator->valid()) {
$this->iterator = new AppendIterator();
$this->iterator->append(new ArrayIterator([$before]));
$this->iterator->append($iterator);
$this->iterator->append(new ArrayIterator([$after]));
} else {
$this->iterator = new ArrayIterator([]);
}
}
public function current()
{
return $this->iterator->current();
}
public function next()
{
$this->iterator->next();
}
public function key()
{
return $this->iterator->key();
}
public function valid()
{
return $this->iterator->valid();
}
public function rewind()
{
$this->iterator->rewind();
}
}
用法示例:
function generator() {
foreach (range(1, 5) as $x) {
yield "<li>$x";
}
}
$beforeAfter = new \BeforeAfterIterator(generator(), '<ul>', '</ul>');
foreach ($beforeAfter as $value) {
echo $value, PHP_EOL;
}
输出
<ul>
<li>1
<li>2
<li>3
<li>4
<li>5
</ul>
如果您传递的迭代器没有产生任何值,您将没有输出。
我绝不赞同这是个好主意,这完全取决于您。我确信有一些更优雅的方法可以使用一些更晦涩的 SPL 类 来做到这一点。通过扩展而不是合成也可能更简单。
我认为我们不需要向该语言添加任何新内容。在 PHP 7.1 中,我们收到了一个新类型 iterable
。使用类型提示,你可以强制你的 function/method 只接受可以被迭代并且在语义上应该被迭代的参数(与不实现 Traversable
的对象相反)。
// Function that iterates on $iterable
function iterateOverIt(iterable $iterable) {
echo '<ul>'.PHP_EOL;
foreach ($iterable as $item) {
echo "<li>", $item instanceof DateTime ? $item->format("c") : (
isset($item["col"]) ? $item["col"] : $item
), "</li>\n";
}
echo '</ul>'.PHP_EOL;
}
当然这并不能确保循环至少迭代一次。在您的测试用例中,您可以简单地使用输出缓冲,但这不适用于仅当循环至少迭代一次时才需要执行操作的情况。这不可能。实现这种一致性在技术上是不可能的。举个简单的例子:
class SideEffects implements Iterator {
private $position = 0;
private $IHoldValues = [1, 1, 2, 3, 5, 8];
public function __construct() {
$this->position = 0;
}
public function doMeBefore() {
$this->IHoldValues = [];
echo "HAHA!".PHP_EOL;
}
public function rewind() {
$this->position = 0;
}
public function current() {
return $this->IHoldValues[$this->position];
}
public function key() {
return $this->position;
}
public function next() {
++$this->position;
}
public function valid() {
return isset($this->IHoldValues[$this->position]);
}
}
$it = new SideEffects();
if (1/* Some logic to check if the iterator is valid */) {
$it->doMeBefore(); // causes side effect, which invalidates previous statement
foreach ($it as $row) {
echo $row.PHP_EOL;
}
}
正如您在这个鲜为人知的示例中看到的那样,您的代码不可能实现如此完美的连贯性。
我们在 PHP 中使用不同的方式迭代内容的原因是因为没有 "one size fits all" 解决方案。你想做的恰恰相反。您正在尝试创建一个解决方案,该解决方案适用于可以迭代的所有内容,即使它可能不应该被迭代。相反,您应该编写可以适当处理所有方法的代码。
function iterateOverIt(iterable $iterable) {
if (is_array($iterable)) {
echo 'The parameter is an array'.PHP_EOL;
if ($iterable) {
// foreach
}
} elseif ($iterable instanceof Iterator) {
echo 'The parameter is a traversable object'.PHP_EOL;
/**
* @var \Iterator
*/
$iterable = $iterable;
if ($iterable->valid()) {
// foreach
}
} elseif ($iterable instanceof Traversable) {
echo 'The parameter is a traversable object'.PHP_EOL;
// I donno, some hack?
foreach ($iterable as $i) {
// do PRE
foreach ($iterable as $el) {
// foreach calls reset first on traversable objects. Each loop should start anew
}
// do POST
break;
}
} else {
// throw new Exception?
}
}
如果你真的想要,你甚至可以使用 is_object()
在其中包含普通对象,但正如我所说,除非它实现 Traversable
不要迭代它。
适用于所有情况 (PHP >= 7.2) 的解决方案是使用双 foreach
, where the first one acts like an if
,它不会真正执行循环,但启动它:
function iterateOverIt($iterable) {
// First foreach acts like a guard condition
foreach ($iterable as $_) {
// Pre-processing:
echo "<ul>\n";
// Real looping, this won't start a *NEW* iteration process but will continue the one started above:
foreach ($iterable as $item) {
echo "<li>", $item instanceof DateTime ? $item->format("c") : (
isset($item["col"]) ? $item["col"] : $item
), "</li>\n";
}
// Post-processing:
echo "</ul>\n";
break;
}
}