Hatena::ブログ(Diary)

hnwの日記 このページをアンテナに追加 RSSフィード

[プロフィール]
 | 

2009年1月23日(金) 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が勝手に変数の型を変えることはありません。演算子関数が必要に応じてキャストすることはありますが、呼び出し元の変数の型が勝手に書き変わるわけではありません。明示的に代入した場合にしか変数の型が変わることはありませんので、自分の書いたプログラム変数の型が予測できない状況というのは有り得ないと思います。

通りすがり通りすがり 2010/10/28 09:14 strcmp使用者として参考になりました。
自分用のスクリプトを1から書き直すに当たり「strcmp」で本当に良いだろうかと、調べ始めた直後にこのページにたどり着きました。
型は常に意識してスクリプトを書いているのでstrcmpで不具合に陥ったことはないですが、スクリプトでは基本的な「比較」を行うのにわざわざ関数を使うことに少し違和感を覚えていました。
この記事はそういう意味でとても興味深く拝見しました。「===」演算子導入も検討してみたいと思います。

MC6800MC6800 2011/07/21 14:27 ブラボーです。
貴重な検証結果、大変参考になりました。
ありがとうございます。

あるあるあるある 2012/06/23 06:35 書いてくれていて、助かります、ついつい他人のソースを見逃して、放置してしまうところでした☆
オブジェクト指向型のインタープリタエンジンが挙動していると推定しますので→APやライブラリで処理をするよりも、演算子での比較の方が、期待挙動を裏切らない、かつ、各所においてセーフティであると推定しています(=実際にソースは追っていません、ベンチマークプログラム等を用いてメモリやCPU利用をモニタリングしていません、あくまで推定ですが)☆
どのようにセーフティかというと、無駄なインスタンスを作らない、無駄なフック:=サブコールの戻り設定などのオーバーヘッド、無駄な挙動が無くスピーディ☆
と推定できますので、違った挙動を防止するだけではなく、OSに対してセーフティだと思われます(プラットホーム依存が虚無という点を含む)、そして巨大ループ内であれば、CPU利用を低減=省電力、メモリ使用量が低減化=HDDクラッシュ危険度が低くなります=サーバなどで安定稼動が期待できます☆
現在、各ホスティングで正しく、高速稼動するシステムを書いているので、OSの違い、ローカライズ状況、PHPのバージョンやライブラリ有無などの違いを吸収するように心がけていますが、ついつい見逃す事がありました☆
貴重なデータを公開していただき、大変、ありがとうございます☆

 | 
ページビュー
2086687