hnwの日記

PHPで==の代わりにstrcmp関数を使うことによる問題点


補足(2010/12/01 03:00):floatからstringへのキャストで丸められる桁数についてですが、php.iniの設定値「precision」の影響を受けるようです。

僕は以前から「PHPの==はキモいから===を使おうよ」と言っているつもりです(参考:「PHPの==がキモい件」)。しかし、ネット上には==を使った比較での不慮の事故を防ぐ目的で、「安全な==」としてstrcmp関数を使って比較している人が居るようです。このやり方について問題点を指摘します。


strcmpで比較するというのはstring型にキャストをして比較するのと同じですから、キャストして何が起こるか熟知していないと比較結果は想像がつきません。僕は全ての型からstring型へのキャストで何が起こるかスラスラ言えるわけではありませんから、何でもstrcmpするのは==を使うのと同じように怖いと感じます。


今回、strcmpを==の代わりに使うと問題が起こると思われる例を何個か見つけましたので、それについてまとめてみます。結論としてはやはり===演算子を使おうよ、ってことです。

有効桁数が10進で15桁以上ある浮動小数点数

$ php -r 'var_dump(pow(2,52)==pow(2,52)+10);'
bool(false)
$ php -r 'var_dump(strcmp(pow(2,52),pow(2,52)+10));'
int(0)

2の52乗と、それに10加えた数を比較しています。==は両者を異なるものとして認識しますが、strcmpは同じだと判断しています。


float型からstring型へキャストすると、10進で有効桁数14桁までの表現になります。つまり、15桁から先の精度は失ってしまうことになります。浮動小数点数の精度は53bit、10進で言えば15.9桁ほどですから、10進で2桁近い情報を失ってしまうわけです。

浮動小数点数と整数

PHP5.2.2〜5.2.6では、それほど大きくない浮動小数点数を文字列に直した際に、たまに指数表記になるようです(参考:「PHPの浮動小数点数の表示」)。これを応用して、非常に奇妙な挙動を見つけました。

$ php-5.2.6 -r 'var_dump(strcmp(1100000,1100000.0));'
int(0)
$ php-5.2.6 -r 'var_dump(strcmp(1200000,1200000.0));'
int(4)
$ php-5.2.6 -r 'var_dump(strcmp(1300000,1300000.0));'
int(0)
$ php-5.2.6 -r 'var_dump(strcmp(1400000,1400000.0));'
int(6)


strcmpで比較すると、110万と130万は整数も浮動小数点数も同じだけど、120万と140万は違う、という結果になりました。キモいですね。

-0.0

浮動小数点数は、プラスから0.0に近付いた場合とマイナスから0.0に近付いた場合を区別するため、計算結果が0.0でも符号がつきます。PHP5.2.3からは、この-0.0をstring型にキャストすると"-0"になるようになりました。-0.0も数としては0.0と同じなので、==や===で比較すると同じ数になりますが、strcmpで比較すると異なる文字列として扱われてしまいます。

$ php -r 'var_dump(-1/1e1000);'
float(-0)
$ php -r 'var_dump(-1/1e1000==0.0);'
bool(true)
$ php -r 'var_dump(-1/1e1000===0.0);'
bool(true)
$ php -r 'var_dump(strcmp(-1/1e1000,0.0));'
int(-3)


strcmpによれば、-0.0と0.0とは別物ということになります。===で比較して同じ数なものを違うと判断してしまうわけですから、数値の比較として考えるとおかしな挙動なのではないでしょうか。

∞と-∞とNaN

浮動小数点数では∞や-∞が表現できます。これをstring型にキャストすると"INF"や"-INF"となります。

$ php -r 'var_dump(1e1000);'
float(INF)
$ php -r 'var_dump(1e1000=="INF");'
bool(false)
$ php -r 'var_dump(strcmp(1e1000,"INF"));'
int(0)


∞を==演算子で比較した場合は全ての文字列と異なるものとして判断しますが、strcmp関数で比較すると"INF"3文字と同じになってしまいます。大した問題にはならないと思いますが、キモいですね。


NaN(Not a Number)についても同様です。

arrayåž‹

マニュアルにも書いてあるんですが、arrayをstring型にキャストすると全て"Array"になります。つまり、どんな中身のarrayでもstrcmpで比較すると同じということになります。

結論

==の代わりにstrcmp関数で比較すると、float型とarray型を扱う場合に==を使うのとは別の問題が発生することを示しました。


そんな型が来るならstrcmp使わないよ、という意見もあるとは思いますが、来る型がわかっているなら===を使えばいいと思うんですよね。strcmpを使いたい状況が僕にはわかりません。何にせよ、「安全な==」として使うにはstring型へのキャストを熟知している必要があると思いますよ、というのが本記事の主張です。strcmpは文字列同士の大小比較にしか使えないと僕は思います。


念のため補足しておくと、PHPが勝手に変数の型を変えることはありません。演算子や関数が必要に応じてキャストすることはありますが、呼び出し元の変数の型が勝手に書き変わるわけではありません。明示的に代入した場合にしか変数の型が変わることはありませんので、自分の書いたプログラムで変数の型が予測できない状況というのは有り得ないと思います。

'); $entries_chunk.insertBefore(sections[0]); } else { chunk_id += 1; var $prev_entries_chunk = $entries_chunk; var $read_more_link = $('

これ以前の記事を表示する

'); $read_more_link.on('click', {chunk_id: chunk_id}, function(e){ $(e.target).hide(); $(this).remove(); $('#entries-chunk-' + e.data.chunk_id).fadeIn("slow"); }); $prev_entries_chunk.append($read_more_link); var $entries_chunk = $('
'); $entries_chunk.hide(); $entries_chunk.insertAfter($prev_entries_chunk); } } $(sections[i]).appendTo($entries_chunk); } });