PHPのarray_fillにオブジェクトを渡すと全要素が同じインスタンスになる理由と作り分け方

array_fillとarray_fill_keysは、渡した値をそのまま複製せずに並べる関数です。値が配列やintなら問題ありませんが、オブジェクトを渡すと全要素が同じ1つのインスタンスを指します。

「3人分の空のオブジェクトを用意しよう」と思って array_fill を使い、1つ目だけ書き換えたはずなのに全員が変わっていた、という経験はないでしょうか。私は一度やりました。エラーも出ないので、気づくまでに少し時間がかかりますね。

array_fillにnewを渡すと何が起きる?

array_fill の第3引数は一度だけ評価されます。new stdClass と書いても生成は1回きりで、そのオブジェクトへの参照が全要素に入るだけです。

$a = array_fill(0, 3, new stdClass);
$a[0]->n = 1;

var_dump(isset($a[1]->n)); // bool(true)
var_dump($a[0] === $a[2]); // bool(true)

PHPのオブジェクトは代入しても同じ実体を指す(オブジェクトハンドルがコピーされる)ので、=== で比べると全部同一です。1つ書き換えれば、全要素から見えます。

array_fill_keysでも同じ罠にはまる

キーを先に決めてから値を詰める array_fill_keys も同じ仕組みです。

$c = array_fill_keys(['a', 'b'], new ArrayObject());
$c['a']->append(1);

var_dump(count($c['b'])); // int(1)

‘a’ にしか追加していないのに、’b’ の件数も増えています。集計用のバッファを項目ごとに持たせたいときなどに、うっかり踏みやすいパターンだと思います。

配列なら大丈夫なのはなぜ?

一方、空配列やintを渡す分には問題ありません。配列は値渡しで、書き換えた時点で別のコピーに分かれる(コピーオンライト)からです。

$d = array_fill(0, 2, []);
$d[0][] = 'x';
echo json_encode($d); // [["x"],[]]

$grid = array_fill(0, 2, array_fill(0, 2, 0));
$grid[0][1] = 5;
echo json_encode($grid); // [[0,5],[0,0]]

二次元配列の初期化は array_fill の入れ子で素直に書けます。「配列は安全、オブジェクトは危険」と覚えておくと、迷いが減ります。

オブジェクトを別々に作るにはどう書く?

要素ごとに new を実行する必要があるので、コールバックで生成します。array_map と range の組み合わせが短くて読みやすいと思います。

$b = array_map(fn() => new stdClass, range(1, 3));
$b[0]->n = 1;

var_dump(isset($b[1]->n)); // bool(false)

この書き方なら、コールバックが要素の数だけ呼ばれるので、インスタンスはそれぞれ別物になります。キーを指定したいときは、キーの配列を array_flip してから array_map に渡せば、キーが引き継がれます。

$f = array_map(fn() => new ArrayObject(), array_flip(['a', 'b']));
// ['a' => ArrayObject, 'b' => ArrayObject](別インスタンス)

もちろん、素直に foreach で詰める手もあります。どれを選んでも構いませんが、「生成を要素の数だけ走らせる」ことだけ押さえておけば十分です。

array_fillのそのほかの注意点

array_fill(int $start_index, int $count, mixed $value) の引数順は、開始インデックス、個数、値です。array_fill(5, 3, ‘x’) なら、キーは 5, 6, 7 になります。個数が0なら空配列が返りますが、負の数を渡すとPHP 8以降は ValueError になります。

echo json_encode(array_fill(5, 3, 'x')); // {"5":"x","6":"x","7":"x"}

try {
    array_fill(0, -1, 'x');
} catch (ValueError $e) {
    echo $e->getMessage();
    // array_fill(): Argument #2 ($count) must be greater than or equal to 0
}

キーが0始まりでないので、json_encode するとリストではなくオブジェクトになる点も覚えておくと、あとで困りません。

まとめ

array_fill と array_fill_keys は値を1回しか評価しないので、オブジェクトを渡すと全要素が同一インスタンスになります。配列やスカラー値なら気にしなくて大丈夫で、オブジェクトを別々に持たせたいときだけ array_map などで生成し直します。地味ですが、エラーが出ない分、気づきにくい罠だという気がしています。

よくある質問

Q. array_fillでオブジェクトを別々にしたいときはどうしますか?
A. array_fill は使わず、array_map(fn() => new Foo, range(1, $n)) のように要素ごとに生成します。foreach で詰めても構いません。

Q. 配列を渡した場合も全要素で共有されますか?
A. 共有されません。配列は値渡しで、書き換えた時点で別のコピーになるので、array_fill(0, 3, []) は安全に使えます。

Q. 全要素が同じインスタンスかどうかはどう確認できますか?
A. === で比べると、同一インスタンスなら true になります。spl_object_id() で ID を見比べる方法もあります。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です