概要
Generator は Countable
を実装しないので、count
関数に渡すと常に1の値が返されます。1 が返されることはマニュアルに明記されています。要素の数がゼロであるかの判定をする際に Generator を配列と同じように考えると正しく処理できない可能性があります。PHP Internals に提出された RFC は典型的な間違えとして次のコードを示しています。
function handle_records(iterable $iterable)
{
if (count($iterable) === 0) {
return handle_empty();
}
foreach ($iterable as $value) {
handle_value($val);
}
}
iterable
は array
および Traversable
を実装するオブジェクトの両方を引数として受け入れます。PHP 7.1 で導入されました (RFC)。同時に is_iterable
関数も導入されました。
このコードでは $iterable
が Generator の場合、count($iterable) === 0
の行が常に false
になるので、handle_empty()
は常に実行されません。
この RFC は Countable
を実装しないオブジェクトに対して PHP は警告を発するべきではないかと提案しています。
コードの例
かんたんな Generator を定義して、count
の戻り値が 1 になるか確認してみましょう。
function xrange($start, $limit, $step = 1) {
for ($i = $start; $i <= $limit; $i += $step) {
yield $i;
}
}
var_dump(
1 === count(xrange(1, 5))
);
対策
count
関数を適用する前に配列もしくは Countable
を実装するオブジェクトであることをチェックします。複数の箇所で count
を使うのであれば、次のような関数を定義するとよいでしょう。
function is_countable($array_or_object) {
return is_array($array_or_object) || $array_or_object instanceof Countable;
}
var_dump(
true === is_countable(range(1, 5)),
false === is_countable(xrange(1, 5))
);
一番最初に示したコードは次のように修正されます。
if (is_countable($iterable) && count($iterable) === 0) {
return handle_empty();
}
count
の引数の改善も考えましょう。最初に Generator を iterator_to_array
で配列に変換してから count
に渡す方法を挙げます。
var_dump(
5 === count(iterator_to_array(xrange(1, 5), false))
);
次に IteratorAggregate
と Countable
を実装するクラスを用意する方法を挙げます。クラスを再利用しないのであれば、PHP 7.0 で導入された匿名クラスを利用します。
$gen = new class implements IteratorAggregate, Countable {
public function getIterator() {
return xrange(1, 5);
}
public function count() {
$count = 0;
foreach ($this as $value) { $count++; }
return $count;
}
};
var_dump(
5 === count($gen)
);