2/26にPHP5.2.9がリリースされましたが、このバージョンからarray_unique関数が後方互換性を失いました。この関数を利用しているアプリケーションは、PHP5.2.9以降のバージョンでは新たなバグに悩まされるかもしれません。
5.2.9RC1の頃にこの仕様変更に気づいて「PHP Bugs: #47370: array_unique has backward compatibility problem, and SORT_REGULAR is confusing」で指摘してみたんですが、相手にされませんでした。
その後もid:moriyoshiさんが元の動作をデフォルト動作にするよう、中の人と闘ってくれたりしたんですが、残念ながら仕様変更はくつがえりませんでした。
array_uniqueの新仕様と変更内容
array_uniqueのマニュアルは現在下記のようになっています。
array_unique ― 配列から重複した値を削除する
説明
array array_unique ( array $array [, int $sort_flags= SORT_REGULAR ] )
array を入力とし、値に重複のない新規配列を返します。
キーは保持されることに注意してください。 array_unique() はまず文字列として値をソートし、 各値の最初のキーを保持し、2回目以降の全てのキーを無視します。 これは、ソート前の array で最初の関連する値のキーが保持されるという意味ではありません。
要するに、unixコマンドで言えばsortしてuniqするような関数です。
PHP5.2.9から第二引数が増えて、sortの比較方式を指定できるようになりました。また、sortの比較関数が今まではSORT_STRING相当だったものが、SORT_REGULARになりました。現時点のマニュアルはPHP5.2.9の変更に追従できていない部分が残っていますが、「文字列として値をソートし」というのは過去の挙動です。
array_uniqueの新仕様の何が問題か
array_uniqueの新仕様の何が問題かというと、uniqueとみなす基準が変わったことです。PHP 5.2.8まではstrcmpで比較していたのを==で比較するようになったため、array_uniqueの結果が変わってしまうことがあります。
<?php $a = array("100", "0x64", "1e2", ".1E3"); var_dump(array_unique($a));
$ php-5.2.8 hoge.php array(4) { [0]=> string(3) "100" [1]=> string(4) "0x64" [2]=> string(3) "1e2" [3]=> string(4) ".1E3" } $ php-5.2.9 hoge.php array(1) { [0]=> string(3) "100" }
PHP5.2.8までは4個とも違うよって言われてたのが、PHP5.2.9からは全部同じだよって言われるようになりました。
SORT_REGULARはいたずらっ子
今までと挙動が変わるだけでも問題ですが、このSORT_REGULARはカオスな挙動も提供してくれます。SORT_REGULARの問題点はPHPのsort関数は相当おかしい - hnwの日記でも指摘した通り、<、==、>で比較することです。この結果、同じ要素からなる配列のソート結果が異なることがあります。
array_uniqueに関して言うと、sort関数よりも影響が大きい部分があります。ソートの結果次第で==となる要素が隣接したりしなかったりするので、同じ要素からなる配列をarray_uniqueした結果の件数が異なることがあります。
<?php $a=array("10","1az", "1e1"); var_dump(array_unique($a)); $a=array("1e1","10", "1az"); var_dump(array_unique($a));
$ php-5.2.9 hoge.php array(3) { [0]=> string(2) "10" [1]=> string(3) "1az" [2]=> string(3) "1e1" } array(2) { [0]=> string(3) "1e1" [2]=> string(3) "1az" }
配列をuniqueしたら3要素とも違うよ、でも順番を入れ替えたら1個重複があったよ、と言われました。もう何がなにやらって感じですね。
一応捕足しておくと、"10"<"1az"(文字列比較)、"1az"<"1e1"(文字列比較)ですが、"1e1"=="10"(数値比較)なので、上のようなことが起こります。
ちょっとだけ擁護
SORT_REGULARで少しだけ結果が良くなる例もあります。array_uniqueする配列の中身が整数と浮動小数点数(NaNなどは除く)だけであれば、<、==、>には推移律が成り立ちますので、特に矛盾は起きません。それどころか、「PHPで==の代わりにstrcmp関数を使うことによる問題点 - hnwの日記」で指摘したような問題点が無くなりますので、例えば15桁以上の浮動小数点数について重複を除去したい場合に、正確な結果が得られるようになります。
また、配列の要素が整数または整数とみなせる文字列だけであれば、PHP5.2.8までとまったく同一の結果を返します。このような配列が対象の場合にはarray_uniqueを使い続けて問題はありません。
解決策
配列の要素が数値のみであれば、前述の通り大きな問題はありません。
配列の要素が数値形式以外の文字列を含む場合は、sort関数と同様に第2引数をSORT_STRINGやSORT_NUMERICにすれば多少平和になると思います。とはいえ、第2引数を明示的に書くと5.2.8以前で動かないソースコードになってしまいます。並外れて格好悪いですが、全バージョンで動作させたい場合はPHP_VERSIONを見て第2引数をつけるかどうか決めるしかないでしょう。
ただ、個人的にはarray_uniqueは利用頻度の低い関数です。重複チェックや「1回以上出現したか」のチェックをしたい場合、連想配列のキーに格納すれば用が足りることが多いのではないでしょうか。例えば、1回以上出現したユーザーIDが必要な場合に、下記のようなコードを書いている人は居ないでしょうか。
<?php $users = array(); foreach ($objs as $obj) { $users[] = $obj->getUserId(); } $users=array_unique($users); foreach ($users as $user_id) { //何かの処理 }
僕なら次のように書きます。
<?php $user_exists = array(); foreach ($objs as $obj) { $user_exists[$obj->getUserId()] = true; } foreach ($user_exists as $user_id => $val) { //何かの処理 }
趣味の問題のようにも思いますし、このような変形ができない場合も多いかもしれませんが、array_uniqueが腐った記念に手元のコードを見直してみてはいかがでしょうか。