array_uniqueの落とし穴、文字列化比較とSORT_REGULAR
array_uniqueとは配列から重複を除く関数ですが、既定では要素を文字列化して比較するため、型の違うものが同一視され要素が意図せず消えることがあります。
既定は文字列に変換して比較している
array_unique は重複を除く手軽な関数ですが、既定のフラグ SORT_STRING が曲者です。要素をいったん文字列に変換してから比較するので、型の違うものが同一視されることがあります。数値と数値文字列が同じ扱いになる、みたいな挙動ですね。
$a = [1, '1', 1.0, true];
var_dump(array_unique($a));
// [0 => 1] のみ残る
// 全部 "1" に文字列化されて同一とみなされる
逆に、文字列化した結果がたまたま等しくなる別物まで潰してしまうこともあります。意図せず要素が消える、という地味に厄介なバグにつながりがちです。
SORT_REGULARで型を意識した比較にする
第二引数に SORT_REGULAR を渡すと、文字列化せずに通常の比較で重複を判定します。とはいえこれも == 相当の緩い比較なので万能ではないんですが、少なくとも数値を無理やり文字列化することはなくなります。数値配列を扱うならこちらの方が直感に近い結果になることが多いですね。
$a = [1, '1', 2, 2.0];
var_dump(array_unique($a, SORT_REGULAR));
// [0 => 1, 1 => '1', 2 => 2] のような結果
// 1 と '1' は型が違うが緩い比較では等しく、挙動は要確認
オブジェクトや厳密な重複排除は別手段で
オブジェクトの配列や、型まで含めて厳密に重複を除きたい場合は、array_unique に頼らず自前でキーを作る方が確実だと思います。判定に使う値でキーを組み立てて連想配列に詰め直す、という定番のイディオムですね。計算量的にも O(n) で済むので、大きな配列ではこちらが速いです。
$users = [
['id' => 1, 'name' => 'A'],
['id' => 1, 'name' => 'A'],
['id' => 2, 'name' => 'B'],
];
$unique = [];
foreach ($users as $u) {
$unique[$u['id']] = $u; // id をキーにして自然に重複排除
}
$unique = array_values($unique);
まとめ
array_unique は既定だと文字列化して比較するので、型の違うものが混ざる配列では要素が意図せず消えることがあります。数値なら SORT_REGULAR を検討する、オブジェクトや厳密な排除は自前でキーを作って詰め直す、という使い分けが安全だと思います。手軽な関数ほど、比較のルールを一度確かめておく価値がある気がします。
よくある質問
Q. なぜ要素が意図せず消えるのですか?
A. 既定のSORT_STRINGが要素を文字列化して比較するため、1と’1’のように型の違うものが同一視されるからです。
Q. 数値配列ではどうすればいいですか?
A. 第二引数にSORT_REGULARを渡すと文字列化せず比較します。ただし==相当の緩い比較なので挙動は確認してください。
Q. オブジェクトや厳密な重複排除は?
A. array_uniqueに頼らず、判定値でキーを作って連想配列に詰め直すのが確実で、要素数nに対しO(n)で速く済みます。