5

这有点类似于问题:如何确定 foreach 循环中的第一次和最后一次迭代?,但是,这个 +10 岁的问题非常有array针对性,并且没有一个答案与可以循环许多不同类型的事实兼容。

给定一个可以在 PHP >= 7.4中迭代PDOStatement的东西(数组、迭代器、生成器、DatePeriod...、对象属性等)的循环,我们如何以有效的方式触发需要在循环,但仅在进入循环的情况下

一个典型的用例可能是生成 HTML 列表:

<ul>
  <li>...</li>
  <li>...</li>
  ...
</ul>

<ul>并且只有在存在某些元素时才</ul>必须打印。

这些是我到目前为止发现的约束:

  1. empty(): 不能用于生成器/迭代器
  2. each(): 已弃用。
  3. iterator_to_array()打败了发电机的优势。
  4. 在循环内部和之后测试的布尔标志不被认为是有效的,因为它会导致在每次迭代时执行该测试,而不是在循环开始和结束时执行一次。
  5. 虽然在上面的示例中可以使用输出缓冲或字符串连接来生成输出,但它不适合循环不会产生任何输出的情况。(感谢@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>
4

3 回答 3

0

我认为我们不需要在语言中添加任何新内容。在 PHP 7.1 中,我们收到了一个新类型iterable。使用类型提示,您可以强制您的函数/方法仅接受可以迭代并且在语义上应该被迭代的参数(与不实现的对象相反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 中有不同的迭代方式的原因是因为没有“一刀切”的解决方案。你试图做的恰恰相反。您正在尝试创建一个解决方案,该解决方案适用于可以迭代的所有内容,即使它可能不应该被迭代。相反,您应该编写可以适当处理所有方法的代码。

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不要对其进行迭代。

于 2020-03-02T20:27:01.957 回答
0

适用于所有情况(PHP >= 7.2)的解决方案是使用 double foreach,其中第一个行为类似于 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;
    }
}

3v4l.org 上的完整演示

于 2020-03-03T09:44:21.427 回答
0

好吧,这至少是一个有趣的思想实验……

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>

如果您传递的迭代器没有产生任何值,则不会得到任何输出。

https://3v4l.org/0Xa1a

我绝不赞同这是一个好主意,这完全取决于你。我确信有一些更优雅的方法可以使用一些更晦涩的 SPL 类来做到这一点。通过扩展而不是组合来完成它也可能更简单。

于 2020-03-02T19:38:23.470 回答