PHPの配列結合演算子で気をつけること

PHPには配列の結合方法として array_merge() 関数と、配列同士を + 演算子でつなぐ方法の2種類があります。単純な添字配列(整数をキーとした配列)同士の結合をする場合に、タイプ数が少ないからと + 演算子を使って失敗したのでメモしておきます。

以下のような2つの配列を結合して、一つの配列にしたいと思いました。

$one = [
    'hoge',
    'fuga',
];

$two = [
    'piyo',
    'foo',
];

今までは、array_merge() は、結合する配列同士で同じ添字を持つ場合は2つ目の引数の配列が1つ目の配列を上書きする、+ 演算子の場合は上書きしない、という雑な覚え方をしていました。いわゆる連想配列しか使わない場合はそれでも問題なさそうなのですが、添字を明示的に指定しない「普通の配列」を結合したい場合はかなり異なる結果になります。具体的には以下のような違いが出ます。

$merged = array_merge($one, $two);
var_dump($merged);
/*
array(4) {
  [0]=>
  string(4) "hoge"
  [1]=>
  string(4) "fuga"
  [2]=>
  string(4) "piyo"
  [3]=>
  string(3) "foo"
}
*/

$merged = $one + $two;
var_dump($merged);
/*
array(2) {
  [0]=>
  string(4) "hoge"
  [1]=>
  string(4) "fuga"
}
*/

array_merge() のほうは意図した結果が出ているのに対し、+ 演算子のほうは意図しない結果になってます。全然結合できてません。

この仕様は公式のドキュメントでは以下のように説明されています。

 両方の配列に存在するキーについては左側の配列の要素が優先され、 右側の配列にあった同じキーの要素は無視されます。

https://www.php.net/manual/ja/language.operators.array.php

そして、PHPでは整数をキーとした「普通の配列」と文字列をキーとした「連想配列」に本質的な違いはありません。

PHP においては添字配列と連想配列の間に違いはなく、配列型は 1 つだけで、 同じ配列で整数のインデックスと文字列のインデックスを同時に使えます。

https://www.php.net/manual/ja/language.types.array.php

つまり、今回の例でいうと $one も $two も「0」「1」という整数のキーのみを持つ配列なので、「左側の配列」である $one の要素が優先され「右側の配列」である $two の要素は無視されてしまったというわけです。

しかし、ここがPHPの摩訶不思議なところなのですが、配列の項で「PHP においては添字配列と連想配列の間に違いはなく」と言っておきながら array_merge() は「普通の配列」と「連想配列」を区別して扱います。

入力配列が同じキー文字列を有していた場合、そのキーに関する後に指定された値が、 前の値を上書きします。しかし、配列が同じ添字番号を有していても 値は追記されるため、このようなことは起きません。
入力配列の中にある数値添字要素の添字の数値は、 結果の配列ではゼロから始まる連続した数値に置き換えられます。

https://www.php.net/manual/ja/function.array-merge.php

つまり、文字列をキーとする「連想配列」であれば後に指定された配列の要素が優先されて前の配列の要素を上書きするのに対し、整数をキーとする「普通の配列」であれば、たとえ添字が同じであっても前の配列の要素を上書きせず、要素を追加して返す、ということになります。厳密には文字列のキーも整数のキーも持っている配列があり得るので、キーの型によって挙動が変わるということになります。

一貫性という点からすると array_merge() のほうが「PHPにおいては配列型は一つだけ」という原則を破っている(ように見える)ので変なことをしているということになりそうですが、直感に近い結果を返してくれるのは + 演算子ではなく array_merge() の方です。他のプログラミング言語に慣れている人の場合はなおさらそうだと思います。

+ 演算子はタイプ数も少ないし、見た目的に「普通の配列」同士を結合するときに使えそうな感じなのですが、事実は全く逆でした。むしろ array_merge() のほうが大抵の場合において意図通りの結果を返してくれます。

+ 演算子を使う場合は、その振る舞いをよく把握した上でないとバグを仕込みそうな感じがします。弊社では基本使わない方針にしていこうかと思います。。。

#