「文字列 "php"
とゼロが等しくなる」。そんな世にも奇妙なことが PHP では起こりえます。
問題
まず、以下のようなプログラムを考えてみます。
関数 outputAge($name, $age)
は、引数 $age
が 0
かどうかで出力内容を変化させています。
$children = [
[ 'name' => '田中太郎', 'age' => 20 ],
[ 'name' => '田中次郎', 'age' => 0 ],
[ 'name' => '田中三郎', 'age' => 'あいうえお' ],
];
foreach ($children as $child) {
outputAge($child['name'], $child['age']);
}
// 出力用関数
function outputAge($name, $age)
{
if ($age == 0) {
print "{$name}さんはまだ赤ちゃんです。\n";
} else {
print "{$name}さんは{$age}歳です。\n";
}
}
さて、ここで出力結果を予想してみてください。
「田中太郎」さんは 20 歳なので、 田中太郎さんは20歳です。
と表示されそうですね。
「田中次郎」さんは 0 歳なので、 田中次郎さんはまだ赤ちゃんです。
と表示されるでしょう。
では、年齢が「あいうえお」になっている「田中三郎」さんはどうなるでしょうか?
↓
↓
↓
↓
↓
答えは、 田中三郎さんはまだ赤ちゃんです。
です。
田中三郎さんはあいうえお歳です。
となると予想した方は、この記事の続きをぜひお読みください。
当たっていた人もなんで当たっていたか気になりますよね? ぜひぜひ。
緩やかな比較
おさらい
PHP で値を比較する際、緩やかな比較(Loose comparisons)と 厳密な比較(Strict comparisons)の 2 つの方法があります。
型を厳密にチェックしない 緩やかな比較 では、 0 == NULL
は TRUE
になります。一方、型を厳密にチェックする 厳密な比較 では、 0 === NULL
は FALSE
になります。
緩やかな比較 は、弱い動的型付け言語である PHP らしい動作です。しかし、正確に理解していないと、予想していない動作(人はそれをしばしば「バグ」と呼びます)を招くことになります。いやしくも PHP を扱うのであれば、公式マニュアルの 比較表 には、一度は目を通しておくべきでしょう。
とはいえ、 TRUE
と FALSE
がズラッと並んだ表を見るのは目に悪いですし、演算子の優先順位 と同じで、必要最低限 だけ記憶しておけばいいと思います。1
私が考える 必要最低限 とは、以下のとおりです。
例外も多いので 場合がある という微妙な表現にとどめておきます。2
-
以下の値同士を比較すると
TRUE
になる場合がある-
0
(整数型) -
"0"
(文字列型) -
NULL
(NULL型) -
""
(文字列型)
-
-
以下の値同士を比較すると
TRUE
になる場合がある-
TRUE
(論理型) -
1
(整数型) -
"1"
(文字列型) -
-1
(整数型) -
"-1"
(文字列型) -
"php"
(文字列型)
-
とくに、0
のほうは、empty()
関数(マニュアル)が空かどうかを判断するときの条件でもあるので、覚えておきましょう。
種明かし
さて、ここでようやく種明かしができます。
先程、 例外も多いので と言いました。
実は、冒頭の問題は例外パターンなのです。
比較表で、縦軸が "php"
、横軸が 0
であるところを探してみてください。
なんと、 文字列 "php"
と整数 0
の比較結果は TRUE なのです
原因
どうしてこうなるのでしょうか?
答えは、比較表の コメント欄 にありました。
The way PHP handles comparisons when multiple types are concerned is quite confusing.
For example:
"php" == 0This is true, because the string is casted interally to an integer. Any string (that does not start with a number), when casted to an integer, will be 0.
(意訳)
複数の型がからむときの比較方法はとても紛らわしい。例えば:
"php" == 0こいつは正しい。なぜなら、文字列は内部で整数にキャストされるからだ。どんな文字列(数字で始まらない文字列ならなんでも)も整数にキャストされると 0 になるんだ。
なるほど・・・。
まず、比較演算子はどのように比較するのでしょうか?
公式マニュアルには、以下のようにあります。
整数値を文字列と比較したり、比較に数値形式の文字が含まれる場合は、文字列が数値に変換され、 数値としての比較を行います。これらのルールは、
switch
文にも適用されます。===
あるいは!==
による比較では型変換は発生しません。 この場合は値だけでなく型も比較します。
整数を文字列と比較するときは、文字列を数値に変換してから比較するんですね。
では、文字列から数値へはどのように変換されるのでしょうか。
公式マニュアルには、以下のようにあります。
文字列の最初の部分により値が決まります。文字列が、有効な数値データから始まる場合、この値が使用されます。その他の場合、 値は 0 (ゼロ) となります。
var_dump(intval('3 children'));
// --> int(3)
var_dump(intval('one in 3 children'));
// --> int(0)
1 つ目は、文字列が数値で始まっているので、数値に変換すると 3
になります。
一方、2 つ目は、文字列が数値で始まっていないので、数値に変換すると 0
になります。
これは PHP の落とし穴として有名ですね。
if ('3 children' == 3) {
print "比較結果: TRUE\n";
} else {
print "比較結果: FALSE\n";
}
この結果は 比較結果: TRUE
になります。
真の原因
ここまで来れば、おわかりかと思います。
outputAge('田中三郎', 'あいうえお');
function outputAge($name, $age)
{
if ($age == 0) {
print "{$name}さんはまだ赤ちゃんです。\n";
} else {
print "{$name}さんは{$age}歳です。\n";
}
}
'あいうえお' == 0
を処理するとき、PHP は以下のように処理します。
- 文字列
あいうえお
を数値に変換する-
あいうえお
は数値で始まっていないので、整数0
になる
-
-
'あいうえお' == 0
は0 == 0
となる- 比較結果は
TRUE
になる
- 比較結果は
意外な盲点
「緩やかな比較」はいろんなところで使われています。
もう一度、比較演算子の 公式マニュアル を見てみましょう。
整数値を文字列と比較したり、比較に数値形式の文字が含まれる場合は、文字列が数値に変換され、 数値としての比較を行います。これらのルールは、
switch
文にも適用されます。===
あるいは!==
による比較では型変換は発生しません。 この場合は値だけでなく型も比較します。
比較演算子
===
あるいは!==
による比較では型変換は発生しません。
逆に言うと、===
と !==
以外の比較をする場合は、「緩やかな比較」になります。
例えば、<
の場合はどうでしょうか?
$age = 'かきくけこ';
if ($age < 18) {
print "18歳未満です。\n";
} else {
print "18歳以上です。\n";
}
結果は 18歳未満です。
になります。
switch文
これらのルールは、
switch
文にも適用されます。
switch
文が「緩やかな比較」を行うことは意外と知られていないかもしれません。
$age = 'さしすせそ';
switch ($age) {
case 0:
print "乳児\n";
break;
case $age <= 7:
print "幼児\n";
break;
case $age <= 18:
print "児童\n";
break;
case $age <= 20:
print "未成年者\n";
break;
default:
print "成年者\n";
break;
}
結果は 乳児
になります。
回避方法
厳密な比較
型までチェックする 厳密な比較 を使えば、この問題は回避することができます。
==
と !=
をそれぞれ ===
と !==
にするだけです。
outputAge('田中三郎', 'あいうえお');
function outputAge($name, $age)
{
if ($age === 0) {
print "{$name}さんはまだ赤ちゃんです。\n";
} else {
print "{$name}さんは{$age}歳です。\n";
}
}
数値チェック
「厳密な比較」では数値の大小比較などを行うことができません。
そこで、比較する前に、 is_numeric()
関数(マニュアル)でチェックするようにしましょう。3
$age = 'かきくけこ';
if (!is_numeric($age)) {
exit("数値を指定してください。\n");
}
if ($age < 18) {
print "18歳未満です。\n";
} else {
print "18歳以上です。\n";
}
まとめ
今回の記事をまとめると、以下のとおりです。
-
緩やかな比較 では
'php' == 0
はTRUE
になる- この問題を回避するには以下のいずれかを行う
- 厳密な比較 をする
- 数値チェック(
is_numeric()
関数)をする
- この問題を回避するには以下のいずれかを行う
また、あわせて以下のことにも触れました。
- 緩やかな比較は全部覚える必要はないが必要最低限は覚えておくべき(例外があることに注意)
- 文字列を数値と比較するとき、文字列が数値で始まるかどうかで判断される
後記
この問題に気づいたのは、以前、PHPUnit を 勉強 していたときに、緩やかな比較 を一通り調べたときでした。
当時、注釈に以下のように書いていました。
それにしてもなんで "test" == 0 としてしまったのだろうか。恐らく条件文で使うことなどを考えて "test" == TRUE としたのだろう(これはまだ理解できる)。しかし、その結果、本来、0 == FALSE であるのに "test" == 0, "test" == TRUE という矛盾した動作をするようになった。そもそも "test" == 0 とするべきではなかったのではないか?
最近になって、職場で PHP の型について話題になり、以前調べたことをふと思い出して、
'php' == 0
ってTRUE
になるんだよね
と言ったところ、すごく驚かれたので、記事にしてみました。
それにしても、PHP のこういう一貫性のないところは、やっぱり好きになれませんね...
-
「いや、こういうのもちゃんと全部覚えるべき」と言う人もいらっしゃるとは思います。それを否定するつもりはありません。ただ、私は「こういうのに頼らなくてもいいようにする」のがベストだと思いますし、大まかに理解しておいて、詳細を知る必要が出てきたときに参照すれば十分だと思います。 ↩
-
例えば、
0 == "0"
と0 == NULL
はともにTRUE
になりますが、"0" == NULL
はFALSE
になります。 ↩ -
is_numeric()
関数は、符号・小数・指数表記を許容します。もっと単純に整数のみを考慮したいという場合にはctype_digit()
関数(マニュアル)を使うことも可能です。ただし、ctype_digit()
関数は引数が文字列型であるときのみ使えることに注意が必要です。 ↩