2013年8月7日水曜日

http://d.hatena.ne.jp/omoon/20111217/1324109105


A Small, Good Thing

2011-12-17

isset() 関数にご用心! ( #phpadvent2011 Day 17 )Add Starcaesarkazuhitoatkuratsuyok

PHP Advent Calendar 2011 の 17 日目です。昨日はhajikamiさんの「[PHP] == による文字列比較をしてはいけないことを理解する #phpadvent2011 16th | Prog Blog From 憩い場所」でした。
みなさんこんにちは。17 日目担当の omoon です。普段は大阪WEB アプリケーションを作ったりしている会社におりまして、PHP は触り始めて 10 年ぐらいです。
本当はもっと早く記事を上げる予定だったんですが、急遽下のような仕事が入りまして遅くなりました。ごめんなさい。
>
さて、みなさんは配列の要素の存在確認ってどうされてますか?
僕は、PHP を始めてからずっと無条件で isset() 関数 を使っていたのですが、ついこの間、えー、うそん!的なハマりを体験いたしましたので、もうご存知の方には面白くともなんともない話だとは思いつつ、恥を晒す覚悟でまとめてみたいと思います。

isset() 関数はその配列要素が存在しても値が null なら false

null でも false なんです!知らなかった。僕は単純に知らなかった。マニュアルの一番最初に書いてあるのに。。
isset ― 変数がセットされていること、そして NULL でないことを検査する
PHP: isset - Manual
<?php
$noodles = array(
    'udon'   => 'white',
    'soba'   => 'gray',
    'pasta'  => 'yellow',
    'malony' => null,
);
isset($noodles['udon']); // true (知ってた)
isset($noodles['malony']); // false (えー、知らんかった!)
isset($noodles['somen']); // false (知ってた)
つまり、単純に配列要素の存在だけを確認するために、isset() を使っていると、要素自体は存在するのに false が返ってくることがあるということです。つい先日、これでバグ出しました。ごめんなさい。
ということで、配列要素の存在の確認には、array_key_exists() が良いみたい。
<?php
$noodles = array(
    'udon'   => 'white',
    'soba'   => 'gray',
    'pasta'  => 'yellow',
    'malony' => null,
);
array_key_exists('udon', $noodles); // true
array_key_exists('malony', $noodles); // true
array_key_exists('somen', $noodles); // false
お恥ずかしい限りですが、またひとつ賢くなりました。

配列の要素の要素などに isset() 関数を使うときの注意

で、ついでにもうひとつ。
配列の要素の存在確認に isset() を使う癖が付いているとこういう勘違いもしてしまいがち、という例をお話しします。そして、これでもバグを出しました。ごめんなさい。
配列の要素の要素の存在を調べたいと思ってやってしまったこんな例です。
<?php
$noodles = array(
    'udon'  => 'white',
    'soba'  => 'gray',
    'pasta' => 'yellow',
    'malony' => null,
);

isset($noodles['udon']['color']); // true (えー、うそん!)
echo $noodles['udon']['color']; // 'w' が出力される (なにそれ?)
これはさっきの話よりは少し複雑ですので、詳しく見ていくことにします。今から説明する2つのことが関係します。

(1) 文字列へは、[] を使って文字単位でアクセスできる

これ、意外と知られていないと言うか、最初知ったときは、何それー、と思うと思うんですが、つまり、
<?php
$name = "Santa Claus";
echo $name[0];  // S
echo $name[1];  // a
echo $name[2];  // n
echo $name[3];  // t
echo $name[4];  // a
echo $name[5];  // ' ' 
echo $name[6];  // C 
echo $name[7];  // l
echo $name[8];  // a
echo $name[9];  // u
echo $name[10]; // s
ということ。
ちなみにこんな風に書き換えることも可能。
<?php
$name = "Santa Claus";
$name[0] = 'P';
$name[3] = 'd';
echo $name; // Panda Claus(誰?)
まずこれが関係することの 1 つ目です。

(2) 型の相互変換

PHP: 型の相互変換 - Manual のページには、
PHP は、変数定義時に明示的な型定義を必要と(または、サポート) しません。ある変数の型は、その変数が使用される文により定義されます。 これは、ある文字列変数 var に代入した場合には、 var は文字列になることを意味しています。 ある整数値を var に代入した場合には、 その変数整数になります。
とあります。要は、時と場合によって文字列が勝手に数値になることがあるよ、ってことですね。
で、その時と場合のひとつが文字列変数に続く[] 内の文字列の場合で、
<?php
$name = "Santa Claus";
echo $name['firstName']; // これは'S'と出力される
となります。
つまり、文字列変数 $name に続く [] 内の文字列「firstName」が数値「0」に変換されるため、$name[0] と同義となり、$name の 1 文字目の「S」が出力されることになります。
でも、なぜ「0」なのか。
実はこの挙動PHPマニュアルこのページの「文字列の数値への変換」セクションにきちんと書いてあります。
数値として文字列が評価された時、結果の値と型は次のように定義されます。
文字列の 中に '.' や 'e'、'E' といった文字が含まれず、 数値が integer 型の範囲内 (PHP_INT_MAX で定義されています) におさまる場合は integer として評価されます。それ以外の場合は、すべて float として評価されます。
文字列の最初の部分により値が決まります。文字列が、 有効な数値データから始まる場合、この値が使用されます。その他の場合、 値は 0 (ゼロ) となります。有効な数値データは符号(オプション)の後に、 1 つ以上の数字 (オプションとして小数点を 1 つ含む)、 オプションとして指数部が続きます。指数部は 'e' または 'E' の後に 1 つ以上の数字が続く形式です。
見落としがちですが、
文字列が、 有効な数値データから始まる場合、この値が使用されます。
とありますので確かめてみます。
<?php
$name = "Santa Claus";
echo $name['1stName']; // これは'a'と出力される
となるんですね。
ここでも文字列が数値に変換されるのは同じですが、今度は [] 内の文字列「1stName」が有効な数値データ「1」から始まっているため、$name[1] と同義となり、$name の 2 文字目「a」が出力されるというわけです。
これが、関係することの 2 つ目です。

で話をもどして、配列の要素の要素の存在確認は、、

さて、この 2 つのことを頭に入れて、先ほどの
<?php
$noodles = array(
    'udon'  => 'white',
    'soba'  => 'gray',
    'pasta' => 'yellow',
    'malony' => null,
);

isset($noodles['udon']['color']); // true (えー、うそん!)
echo $noodles['udon']['color']; // w (なにそれ?)
を順に説明してみると、
となります。
ということで、配列の要素の要素の存在確認は、
<?php
$noodles = array(
    'udon'  => 'white',
    'soba'  => 'gray',
    'pasta' => 'yellow',
    'malony' => null,
);

if (is_array($noodles['udon'])) {
    array_key_exists('color', $noodles['udon']);
}
のように、
  • 配列であるかどうかを確認した上で
  • array_key_exists() を使って確認
するのが良いようです。
と、色々話してきましたが、実はこのあたりの内容については、PHPマニュアルにも幾つかコメントがついています。
困ったら PHP マニュアルに帰れ!(コメント欄も含めてね)、という素晴らしい定説を確認できたところで、僕の話しを終わりたいと思います。
みなさん、良いクリスマスを!