hnwの日記

array_unique関数がPHP5.2.9から後方互換性を失いました


追記(2009/06/26):PHP 5.2.10以降、この問題は修正されています。「array_unique関数がPHP5.2.10から後方互換性を取り戻します」も併せてご覧ください。


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 で最初の関連する値のキーが保持されるという意味ではありません。


PHP: array_unique - Manual


要するに、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が腐った記念に手元のコードを見直してみてはいかがでしょうか。

まとめ

さようならarray_unique。君の事は忘れないよ。


PHPの中の人たちって、仕事でPHP使ってないのかなあ?マイナーバージョンアップでこの仕打ちは無いわー。


現状動いているコードのarray_uniqueを除去するのはやりすぎだと思いますし、使わないと苦しい場所もあるとは思いますが、今まで以上に注意を払わないと使えない関数になったのは確かだと思います。