最初に結論
in_array
には第三引数true
を指定しろ。絶対にだ。
はじめに
in_array
については、はじめにPHP: in_array - Manualをよく読んでおいてほしい。
この記事に書いたコード断片は、どうか読むだけではなく自分で手を動かして確認してほしい。
PHPで短いコードを動かすのはPsySHを利用すると、とても捗る。ローカルに動作環境がなければ、Ideone.comなどのオンラインサービスを利用しても差支ない。
static $fruits = ["apple", "orange", "banana"];
in_array("apple", $fruits); // => true
in_array("mikan", $fruits); // => false
これがin_array
のふつうの使ひかただ。
Webアプリケーションでの実例
では、次のような例を見てみよう。$_GET
から入力を受け取る、かなりシンプルなWebアプリケーションのような想定だ。
<?php
$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;
static $ids = [0, 1, 2, 5, 8];
if (in_array($input_id, $ids)) {
$status = 200;
$msg = "id = $input_id";
} else {
$status = 404;
$msg = "idはありません";
}
http_response_code($status);
echo "<p>" . htmlspecialchars($msg) . "</p>\n";
しかし、このコードには看過しがたいバグがある。?id=0.0
と入力したとき、「idはありません」ではなく「id = 0.0」と表示されてしまふ。
状況を整理しよう。上記のコードをさらに簡略化したバージョンで説明する
何も入力しないとき
/** $_GET["id"] がないとき */
$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;
// $input_id は -1
in_array($input_id, [0, 1, 2, 5, 8]); // false
これは意図通りだ。ちゃんと弾けてる。
整数を入力したとき
/** $_GET["id"] = "5" のとき */
$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;
// $input_id は "5"
in_array($input_id, [0, 1, 2, 5, 8]); // true
この動作は、果たしてあなたの想像通りだろうか。あるいはPHPでは当然の柔軟さだと感じるだろうか。
確認しておきたいのは、第一引数が文字列であること、第二引数は整数の配列であることだ。
小数を入力したとき
/** $_GET["id"] = "5.0" のとき */
$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;
// $input_id は "5.0"
in_array($input_id, [0, 1, 2, 5, 8]); // true
もう一度聞くが、この動作は、果たしてあなたの想像通りだろうか。あるいはPHPでは当然の柔軟さだと感じるだろうか。
注目したいのは、もちろん最後の行だ。どうしてこうなるのか。in_array
の比較は==
演算子の結果と同じだ。
5 == 5 ; // true 当然
"5" == "5" ; // true 当然
"5" == 5 ; // true これも
5 == "5" ; // true 良いよね
5 == 5.0 ; // true まあ
5.0 == 5 ; // true わかる
5 == "5.0"; // true えっ
"5" == "5.0"; // true ええっ
これでもあなたは「PHPはシンプルな言語」だと呼び続けられるだろうか。
文字列
/** $_GET["id"] = "apple" のとき */
$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;
// $input_id は "apple"
in_array($input_id, [0, 1, 2, 5, 8]); // true えええっ
簡略してない最初のバージョンに突っ込むと、「id = apple」と表示される。
例によって==
演算子での比較を見てみよう。
"apple" == 0 // true
0 == "apple" // true
あなたはこれを見ても、本当に「PHPはシンプルな言語」だとry
改善
では、どうすればサンプルのWebアプリケーションが改善されるか検討しよう。
==
を知る
PHPで仕事するなら型の相互変換とPHP 型の比較表を穴が空くまで読んでおくことで、今回のような問題を「匂い」で察知することができるようになります。
in_arrayに第三引数を渡す
in_array
(PHP 4, PHP 5)
in_array
— 配列に値があるかチェックする説明
bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] )
needle
でhaystack
を検索します。strict
が設定されていない限りは型の比較は行いません。
http://php.net/manual/ja/function.in-array.php 2015年2月8日の版より引用
in_array
には第三引数がある。型の比較をするとかしないとか表現がわかりにくいのだけれど、実際はfalse
のときは ==
で、true
のときは ===
で比較されると覚えておけばいい。
文字列が(十進)整数かどうか調べる
ここでは0-9
の文字だけを使って十進数の整数を表現した文字列のことだ。
PHP: PHP 型の比較表 - Manualには「注意: HTMLフォームは整数、浮動小数点数、booleanを渡してはくれず、文字列を渡します。文字が数値であるかどうか確認するには、 is_numeric()を使うとよいでしょう。」などと書いてあるのだが、実はそれでは不十分だ。上の「小数を入力したとき」のような場合で弾くことができない。
逆に、is_numeric()
のチェックをせずにint
にキャストすればいいじゃない、との発想もあるだろう。
(int)"a"; // 0
(int)1.0; // 1
「大は小を兼ねる」の発想で「?id=1.0」と「?id=1」は同一視していいじゃないか、と見ることもできるかもしれない。しかし、筆者はWebアプリケーションとしては、意図しない状態は極力減らしておくべきだと考へる。SEOのような観点からも、ふとしたきっかけで無駄にクローリングされることになるかもしれず好ましい状態ではない。
ある文字列が整数を表現するかどうかを判定するには、以下のようなイディオムが有名だ。
$input = "0";
$input_is_int = (string)$input === (string)(int)$input;
あるいは次のような正規表現でもチェックできる。自分の直感でわかりやすい方を選んでいい。(性能差などは瑣末な問題だ)
$input = "0";
$input_is_int = preg_match('/\A(?:0|-?[1-9][0-9]*)\z/', 1) > 0;
次のような函数を用意しておけば、値がint
でもstring
でもfloat
でも汎用的に利用できるのでべんりかもしれない。
/**
* @param int|float|string $value
* @return boolean
*/
function is_intval($value)
{
return !is_float($value) && (string)$value === (string)(int)$value;
}
改良バージョン
<?php
$is_intval = function ($value)
{
return !is_float($value) && (string)$value === (string)(int)$value;
};
$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;
$input_id = $is_intval($input_id) ? (int)$input_id : $input_id;
static $ids = [0, 1, 2, 5, 8];
if (in_array($input_id, $ids, true)) {
$status = 200;
$msg = "id = $input_id";
} else {
$status = 404;
$msg = "idはありません";
}
http_response_code($status);
echo "<p>" . htmlspecialchars($msg) . "</p>\n";
そもそもの話
in_array()
が良くないって意見もある。
以下のようなコードでは、in_array()
バージョンほど厄介な問題はおこらない。 (整数と小数の問題は健在)
static $ids = [
0 => true,
1 => true,
2 => true,
5 => true,
8 => true,
];
if (isset($ids[$input_id])) {
$status = 200;
$msg = "id = $input_id";
} else {
$status = 404;
$msg = "idはありません";
}
また、in_array()
は線形探索するため、配列の要素数によっては深刻な性能劣化を引き起こすことがあるだろう。
もちろん、ケースバイケースで良い点と悪い点が存在するのは当然のことだ。おのおののケースでisset
とin_array()
のどちらが良いかは読者への課題としたい。
そのほかの話
なぜかストックが増えたことだし、せっかくなのでちょっぴり追記する。
-
array_search()
はin_array
と同じ問題がある -
switch
文も同様だが、in_array()
と違ってstrictオプションに相当するものはない- 少々過激ではあるが、筆者は
switch
ではなくelseif
で書くことを推奨したい - ちなみにPythonには
switch
に相当する構文は存在しない
- 少々過激ではあるが、筆者は
まとめ
筆者はin_array()
が大好きだ。