EC studio EC studio 技術ブログ

PHPSPOTさんで
PHPで、文字列と数値0の比較は等価になるようです
PHPで「特定」文字列と数値0の比較が等価になるカラクリ

という記事が投稿されたことから、各所でPHPの文字列比較について
議論が行われているようです。

どういうことかというと、

  1. if ("3abc" == 3) {
  2.     echo "true";
  3. }else{
  4.     echo "false";
  5. }

PHPではこのコードで true が出力されます。

かなりギョッとしますが、PHPのバグではなく仕様です。。

これはPHPとしてどういう解釈がされているのかを理解しないと
とても怖い部分ですので、調査してみました。

※もし間違っている部分などがありましたらご指摘いただければと思います。

文字列型と数値型の比較

どうやら、PHPでは文字列型と数値型(int,floatなど)など
違う型同士での比較を行う場合、自動で型変換が行われるようです。

整数値を文字列と比較する際、文字列が 数値に変換されます。

というルールに従っているようです。
(ここには"整数値を文字列と比較"と書かれていますが、整数(int型)だけではなくfloatなど数値型全般みたいです)

つまり、内部的に

  1. if ((int)"3abc" == 3){
  2.     echo "true";
  3. }else{
  4.     echo "false";
  5. }

という処理が行われているようです。

(int)はPHPの強制型変換(キャスト)の構文で、
強制的に(型名)で指定した型に変換します。

文字列をintにキャストすると

  1. echo (int)"3abc";    //3 と出力される
  2. echo (int)"test";    //0 と出力される
  3. echo (int)"01abc";   //1 と出力される
  4. echo (int)"   01gd"; //1 と出力される

こう変換されます。(驚きますね・・・!)

変換のルール:
文字列の先頭が数値ならばそこが抜き出され、
そうでなければ0になる。
(先頭の空白や0は無視される)

というルールで変換されます。
(C言語のstrtodという関数の挙動だそうですが)
これで"3abc" と 3 が一致するわけですね。

float型などでも同じで、

  1. if ("3.4abc" == 3.4) {
  2.     echo "true";
  3. }else{
  4.     echo "false";
  5. }

これが true になり、内部的には

  1. if ((float)"3.4abc" == 3.4) {
  2.     echo "true";
  3. }else{
  4.     echo "false";
  5. }

こういうキャストが行われているようです。

ちなみに、

  1. if ("3abc" == "3") {
  2.     echo "true";
  3. }else{
  4.     echo "false";
  5. }

この様に、数値型の方をダブルクォーテーションでくくって文字列型にし、
型をそろえてやると、falseが出力されます。

なるほど、== を使う場合は型を意識して、
ダブルクォーテーションで型をそろえてやればいいんですね!

と、ここまではそれなりに(?)理解できるルールがあるのですが、
ここからがポイントです・・。

このルールには例外があります。

例外パターン - 数値形式の文字列の扱い

文字列が数値形式である場合、挙動が変わります。

  1. if ("3.0" == "3") {
  2.     echo "true";
  3. }else{
  4.     echo "false";
  5. }

これはtrueになります。

また、

  1. if ("3e0" == "3") {
  2.     echo "true";
  3. }else{
  4.     echo "false";
  5. }

これもtrueになります。

先ほどのパターンで言うと、文字列と文字列の比較になるので
型変換が行われずfalseになるはずなのですが・・

これはなぜかというと

数値形式の文字列を比較する場合、それは整数として比較されます。

というルールが適用されているからです。
(なお、ここでも"整数として比較される"と書かれていますが、
floatはfloatで比較されます)

つまり、文字列型と文字列型の比較の場合においてでも、
比較対象が両方とも数値形式(数値として解釈できる文字列)場合は、
整数型としての比較になります。

数値形式の文字列には、

"123" ← 数字だけ
"3.0" ← 小数点
"-6" ← 負数
"0003" ← 先頭に0と数字だけ
"1e3" ← 浮動小数点数
"0x3A" ← 16進数

などがあり、is_numeric()という関数でtrueになるものの様です。
↓下記で調べることができます

  1. $num = "調べたい文字列";
  2. if (is_numeric($num)){
  3.     echo "数値形式の文字列です";
  4. }else{
  5.     echo "数値形式の文字列ではありません";
  6. }

つまり、比較対象が両方とも数値形式の文字列の場合、

  1. if ("3e0" == "3") {
  2.     echo "true";
  3. }else{
  4.     echo "false";
  5. }

これは↓下記の様に型変換が行われ、

  1. if ((float)"3e0" == (float)"3") {
  2.     echo "true";
  3. }else{
  4.     echo "false";
  5. }

trueになります。

片方だけが数値形式の場合は

  1. if ("3.0" == "3abc") {
  2.     echo "true";
  3. }else{
  4.     echo "false";
  5. }

これは型変換がfalseになります。

※重要 両方とも数値形式の文字列の場合だけ型変換が行われる!

まとめ&結局どうすればいいのか・・?

何かもうわけがわからなくなってきましたが、

まとめると、== を使った比較では

・文字列型と数値型の比較の場合、
 数値型と同じ型にキャストされる
・文字列型と文字列型の比較でも、両方とも数値形式の場合、
 数値型へキャストされる

こうなります。

ではどうすればいいのかを私なりに考えてみました。

■A案:文字列型の比較はすべて === を使う。

これが一番安全です。

Perlだと文字列の比較は == ではなく eq を使うので、
同じように === を使おうというものです。

ただ、文字列同士の比較で==を使ったらNoticeエラーでも
出てくれればいいのですが、エラーも何も出ないので
すべてのソースコードで文字列比較の場合は ===、
それ以外は == みたいな使い分けを徹底するのは大変です・・・
(PEARなども含めてほとんどのPHPコードでは
そういったコード規約を採用していませんし)

かといって、== は使わずすべて === で統一!
としてしまうと、せっかくの動的型付け言語のPHPの意味が・・・。

strcmpstrval、(string)へのキャスト などでも対応できます。
が、=== が一番簡潔に書けるかと思います。

■B案:数値型に解釈されると困るところだけ === を使う。

というわけで、いまのところ妥協案としては
こっちかなと思っています。(まだ検討中ですが・・)

※注意:この対策がベストというわけではありません!
あくまでPHPの良さを活かしつつ、
問題が起きないようにする為の妥協案です。
どういった対策をされるかは各個人でご判断ください。

例えば、

  1. if ($_POST['action'] == "confirm"){
  2.     //確認画面を表示
  3. }

この様なところで、

  1. if ($_POST['action'] === "confirm"){
  2.     //確認画面を表示
  3. }

=== とするのは意味がありません。

おそらく、ほとんどのケースで数値と解釈されても
問題ないことが多いかと思います。
(いまのいままで問題として大きく取り上げられていませんし)

この問題は条件式のどちらかが数値、もしくは
数値で解釈できる形式の文字列である場合に発生するので、

1.どちらかが数値で解釈できる文字列になりうるか
2.なりうる場合、数値と解釈された場合に問題が起きるか

という基準で === を使えばよいかと思います。

具体的に === を使うべきところとしては、認証など厳密な一致が必要な場合。

例えば "3e0" というパスワードがユーザー登録されていた場合、
"3"でも"3e00"でも"3.0"でもログインできてしまいます。

XSS対策でhtmlspecialcharsを使うように、
要所要所で === を使っていく、というイメージでしょうか。

--------------------------------------------------------

いやはや、、もう長いことPHPやってますが、勉強になりました。

いままで、 === や !== は、

  1. if (0 === false){
  2.    
  3. }

みたいな 0、""、負数、false、nullなどの
特別な値を判定するために使うものだと思ってたんですが、
文字列の比較でも必要になるんですね。

この問題ではPHPに対するネガティブな意見が多いですが、
PHPの簡単さ、わかりやすさを実現する為に開発側は
あえて採用した仕様なんだろうなと私なりに思っています。

PHPなど動的型付け言語をさわっていると、
どうしても内部ではCで動いていることを忘れてしまいがちですが、
実際にどうやって動的な挙動が実現されているかを知っておかないと、
思わぬ問題に当たってしまいそうですね。


関連した記事:
投稿者
人気のエントリー
カテゴリー
最近のエントリー
アーカイブ
Copyright© ChatWork, All Rights Reserved. secured by ESET.