PHPにおける文字列比較の2a問題
PHPの文字列比較に関しては古の時代から皆がいばら道を通っているので今更何か語ることも無いんだけどやっぱり良くハマっちゃうということで軽くメモを残しておく。
とりあえずは先人達の苦悩ということでこのあたりの記事は見ておいた方がいいだろう。
素晴らしき自動的な世界〜或いは「型のない」世界〜 - がるの健忘録
PHPでの入力値チェックのすり抜け
上記記事の方が名づけたと思われる(w)いわゆる「2a問題」というものについて書いていく。
この2a問題というのはもう一度再確認すると
<?php $a = '2a'; $b = 2; if ( $a == $b ) { // ここを通る }
という問題のことだ。
何故こうなるかは元記事やマニュアル等に詳しく書かれているが、カンタンに説明すると「2a」という文字列を数値評価すると「2」として扱われるということだ。
実はコレはPerlでもまったく同じ問題を含んでいるが、Perlであまりこの問題が取りざたされないのは数値比較する演算子と文字比較する演算子の二種類が用意されているからだ。
以下、Perlのコード
my $a = '2a'; my $b = 2; if ( $a == $b ) { # ここを通る } if ( $a eq $b ) { # ここを通らない }
Perlにおいて文字列として比較したい場合、明示的にeq演算子を使うという習慣が完全に行き渡っているため2a問題が問題として露呈しないというわけなのである。(それに文字列を数値評価すると警告も出る。)
やり方として色々あるにはある。
例えば===を使う方法だ。
<?php $a = '2a'; $b = 2; if ( $a === $b ) { // ここを通らない }
===とは値の比較だけでなく、型の比較まで行ってくれるので上記の例で言うと、$aが文字列型で$bが数値型なので一致せず、if文を通過しないという結果になるわけだ。
しかし===にも問題はある。
それは厳格すぎるという点だ。
型までキッチリ比較するということは下記のような例までNGになってしまう。
<?php $a = '2'; $b = 2; if ( $a === $b ) { // ここを通らない }
値は両方とも同じ「2」なのに、型が違うせいでif文を通過しなくなってしまう。
ではどうすればよいのか?
・・・思い出して欲しい。
PHPには豊富で便利な標準関数が鬼のようにあることを。(むしろ多すぎて収集つかなくて使いづれーよ!という突っ込みはさておき。)
まぁ、ようするに文字列比較を行う標準関数が存在するわけだ。
ズバリstrcmp関数。
<?php $a = '2'; $b = 2; if( strcmp($a,$b) == 0 ) { // ここを通る } $a = '2a'; $b = 2; if( strcmp($a,$b) == 0 ) { // ここを通らない }
このように型が違っても文字列として同じ値であればif文を通るようになるというわけだ。
これで一件落着♪
・・・だったら良かったのだが。
実は問題もある。
いや、正確にいうとstrcmp関数は悪くない。
では何が問題なのかというと下記の記事を見て欲しい。
<?php $a = "2a"; switch ( $a ){ case 2: echo 'orz orz orz'; break; case 0: echo '(・ω・)'; break; case NULL: case false: echo 'orz'; break; }でどこ通るかなんてぇのは自明の理だよね?
もちろん「case 2」だw
2a問題再びw - がるの健忘録
いわゆる「2a&switch問題」(勝手に命名)だ。
一応上記の処理は「case 2:」と書いている部分を「case "2":」といったように明示的に文字列にしておけば問題ないといえばないのだが、switchに渡される$aの値が数値型だった場合に結局同じ問題にブチ当たってしまう。
<?php $a = 2; // 数値型 switch ( $a ){ case "2a": // ここを通る break; case "2": // ここを通らない break; }
if文であればstrcmpを使って問題解決できるが、switchだと使えない。
いや、使えないというのは語弊だな。switchでも下記のようにすれば使えるには、使える。
<?php $a = 2; // 数値型 switch ( true ){ case strcmp($a,"2a") == 0: // ここを通らない break; case strcmp($a,"2") == 0: // ここを通る break; }
switchにtrueを渡してcaseで比較するという手法だ。
しかしこれではswitchを使うメリットが完全に殺されてしまっている。こんな書き方するくらいならもうif文でいいじゃんと。
ではどうすべきなのか?
ここでPHPの型キャストというものが役に立ってくる。
型キャストとは何か?
その名の通り変数の型を別の型にキャストすることである。
<?php $a = 2; // 数値型 switch ( (string)$a ){ // string型にキャストする case "2a": // ここを通らない break; case "2": // ここを通る break; }
strcmp関数がやってることも(憶測ですが)結局はstring型にキャストして===してるだけなので、
<?php $a = '2'; $b = 2; if( (string)$a === (string)$b ) { // ここを通る } $a = '2a'; $b = 2; if( (string)$a === (string)$b ) { // ここを通らない }
といったように型キャスト方式に置き換えることが可能です。
strcmp関数を使うか型キャストを使うかはプログラマの好みでどうぞという感じでしょうか。
とまぁ長々と書きましたが文字列比較をする場合は必ずstrcmpなり型キャストなりを使うと意識していればよいかと思います。
というよりも、むしろ値の比較をする場合は常にstrcmpなりを使い、数値として100%保障されている場合のみ数値比較をするという方針でいれば2a問題は今後発生しないでしょう。
実際に僕がPerlでeqではなく==を使う場合というのは事前に正規表現で数値が保障されているとか、ある関数の戻り値に数値しか返ってこないとかそんな場合のみですしね。
<?php // どんな値が来るか保障されていない場合 $param = $_GET['param']; if ( (string)$param == '2' ) {} // これはOK! if ( $param == 2 ) {} // これはNG! // 値が数値であることが保障されている場合 if ( preg_match('/^\d+$/',$param) ) { if ( $param == 2 ) {} // これはOK! } // ある関数の戻り値が必ず数値の場合 $array = array(1,2,3); if ( count($array) == 2 ) {} // これはOK!
これで2a問題に悩まされることなく安らかな睡眠を過ごせることでしょう。
最後にこれは余談ですが、実はstrcmpや型キャストでやってもダメなケースがあります。
以下に例を示します。
<?php $a = ''; $b = false; $a = 1; $b = true; $a = ''; $b = null; $a = null; $b = false;
これら全てマッチしてしまいます。
まぁ殆どの場合、空文字やnullやfalseが同じ扱いでも問題になることは無いと思いますが、これらの違いもちゃんと比較したいような時が訪れた場合は以下の関数を試してみてください。
<?php function truecmp ($a,$b) { if ( strcmp($a,$b) == 0 && is_bool($a) == is_bool($b) && is_null($a) == is_null($b) ) { return true; } return false; }