isset()と!is_null()の値は常に等しいか

こんばんは!今日はPHPのisset()と!is_null()(is_null()を反転したもの)に同じ値を与えたとき*1、返されるbool値は常に等しいかどうかという小さいお話です。小さい話なのに長くなってしまいましたが…。物分かりが悪く、物忘れが激しく、読解力がない、そんな自分に説明するとなればこれでもかというくらい丁寧に書くしかないですよね。はい。ありがとう。面目ねぇ。そんな感じでやっていきたいと思いますが、まずisset()とis_null()の返す値と条件についておさらいしましょう。

isset() ([http
//www.php.net/manual/ja/function.isset.php:title]):変数がセットされており、それが NULL でないならTRUEを返す
is_null() ([http
//www.php.net/manual/ja/function.is-null.php:title]):指定した変数がNULLならTRUEを返す

こんな感じでした。これに加えて未初期化変数など(上の引用文に言葉を合わせるとセットされていない変数)は暗黙に変換される状況でなければNULLを返すようになっているので(PHP: 基本的な事 - ManualPHP: 配列 - Manual #構文など)、isset()と!is_null()の値は等しくなるように思えました。

<?php
// 未初期化変数の値を調べる場合
var_dump(isset($foo));
var_dump(!is_null(@$foo)); // 一応E_NOTICEが出ないようにエラー制御演算子(@)を付けています

// array内の未定義の要素の値を調べる場合
$bar = array();
var_dump(isset($bar["x"]));
var_dump(!is_null(@$bar["x"]));

この結果は次のようになります。

bool(false)
bool(false)
bool(false)
bool(false)

同じですね。

さて、本題の「isset()と!is_null()に同じ値を与えたとき、返されるbool値は常に等しいか」についてですが、結論から書くと違いました(常に等しければ書き留める意味がないのですけれども!)。異なる値になるのは次のような場合がありました。

  • 文字列に範囲外の配列アクセスをした場合
  • __get()を意図的に定義したクラスのオブジェクトのアクセス不能プロパティ*2を読んだ場合
  • __isset()を意図的に定義したクラスのオブジェクトのアクセス不能プロパティを読んだ場合

順番に確認しましょう。

文字列に範囲外の配列アクセスをした場合

マニュアルのisset()のUser Contributed Notesを見て知ったのですが、これは次のような場合です。

<?php
$s = 'foo';
var_dump(isset($s[9]));
var_dump(!is_null(@$s[9]));

PHPでは(PHPでも)文字列に配列のようにアクセスすることができ、指定した数値が文字列の範囲内の値であれば対応する文字(1byte)が返されます(PHP: 文字列 - Manual #文字列への文字単位のアクセスと修正)。
上の例は"foo"という長さ3の文字列に対して9という範囲外の値で配列アクセスを行っている式にisset()と!is_null()をかけて比べているものですが、結果は次のようになります。

bool(false)
bool(true)

異なる値になりました。これは文字列への範囲外の配列アクセスがNULLではなく空文字を返すからです(意図はよくわかりませんが…)。

__get()を意図的に定義したクラスのオブジェクトのアクセス不能プロパティを読んだ場合

これは次のような場合です。

<?php
class A { } // 通常のクラス
class B // __get()を定義してNULL以外が返るようにしたクラス
{
  public function __get($name) { return ""; }
}

$a = new A();
echo '-- A' . PHP_EOL;
var_dump(isset($a->foo));
var_dump(!is_null(@$a->foo));

$b = new B();
echo '-- B' . PHP_EOL;
var_dump(isset($b->foo));
var_dump(!is_null(@$b->foo));

文字列の例を踏まえると、未定義と判定されつつNULL以外の値が返る場面を作ればisset()と!is_null()は異なる値になるはずですね。
この結果は次のようになります。

-- A
bool(false)
bool(false)
-- B
bool(false)
bool(true)

期待通り異なる値になりました。

__isset()を意図的に定義したクラスのオブジェクトのアクセス不能プロパティを読んだ場合

これは次のような場合です。

<?php
class A { } // 通常のクラス
class B // __isset()を定義して常にTRUEが返るようにしたクラス
{
  public function __isset($name) { return true; }
}

$a = new A();
echo '-- A' . PHP_EOL;
var_dump(isset($a->foo));
var_dump(!is_null(@$a->foo));

$b = new B();
echo '-- B' . PHP_EOL;
var_dump(isset($b->foo));
var_dump(!is_null(@$b->foo));

そういえば__isset()…と思いisset()が常にTRUEを返すような__isset()を定義してみました。文章がややこしいな。この結果は次のようになります。

-- A
bool(false)
bool(false)
-- B
bool(true)
bool(false)

異なる値に…当然と言えば当然ですね。
そういえばマニュアルには__isset() は、 isset() あるいは empty() をアクセス不能プロパティに対して実行したときに起動しますとありますが、そうすると__isset()がisset()によって呼ばれたかempty()によって呼ばれたかをどうやって__isset()内で区別するのでしょうか…。

まとめ

変数などが未定義であるときにNULLが返されなければ当然isset()と!is_null()は異なるわけですが、そういう場面がいくつか出来るということがわかりました。isset()をNULLチェックに使うのは少し危うい場面があるので考えて使いましょうということですね。
あれ、時間が…。それではー。

*1:!is_null()に値を与えるという書き方は変ですね

*2:アクセス不能プロパティの意味はPHP: オーバーロード - Manualに準じます