それでも僕が、エラー制御(抑制)演算子"@"を使う理由

PHP プログラマが "@" を使うべきでない 5 つの理由 - 肉とビールとパンケーキ by @sotarok
というエントリーを読みました。このエントリー自体は有意義なんで同意なんですが、"@"を使うべきでないなんてコンセンサスができたら残念なので、一応、逆説を提示しておきます。僕が書いても影響力なんてないですけど、一応、言うべきことは言っとこうかと。

始める前に、本質的なところ

終的に$qに入るものが同じであることと、コードとして同じ意味であるかは、別じゃないでしょうか。

が一番本質的な話で、それ以上の話ではありません。
つまり、発生する可能性があるとわかっているエラーを表示させないことと、エラーがあるかどうかをチェックして適切に処理をするのかは、意味が全然違うという意味です。

http://d.hatena.ne.jp/sotarok/20090721/1248112106

あるコードが"@"を誤用していることと、"@"を使うべきではないかどうかは、意味が全然違うと思います。
ここでは、せっかくなので"@" の意味と使い方について、もう少し踏み込んでみたいと思います。

エラーを制御するのが"@"の目的

エラー制御(抑制)演算子"@"はエラーを制御するために用いられます。

プログラム初心者の方が悩んでいたりしてコードを教えたりする時に「とりあえず日本語に直して説明できる?」みたいな質問をすることってあるじゃないですか。あの感覚って重要で、コードを書いていて大きくなった関数など分ける時は日本語に直して(いや英語でも別言語でもいいけど)考えたりするわけです。

http://d.hatena.ne.jp/cocoiti/20090721#1248115081

そうなんです。コードは意味がわかるように書くことが大切ですね。

<?php
$q = isset($_GET['q']) ? $_GET['q'] : NULL;

こちらは、

  • $_GET['q']が存在したら$qには$_GET['q']をセットする。
  • $_GET['q']が存在しなかったらNULLをセットする。
<?php
$q = @$_GET['q'];

こちらは、

  • $qに$_GET['q']をセットする。ただし、$_GET['q']がセットされていないときは、Noticeが出るけど、エラーレベルは0で。

まさに、意味が違うわけです。
ポイントは、"エラー制御"です。
アプリケーション内では、状況によってエラーの"重み"が違います。PHP的にはNoticeに該当する内容でも、状況によっては、Noticeの必要がないケースもあれば、Errorにして実行を停止したいケース、もしくはエラーではなく例外を投げてアプリケーションの上位層へ伝達したいケースもあるでしょう。
「ここはエラー処理する必要はないんだよ」という意味を表現できるのはissetの3項演算ではなく、"@"です。

"@"でエラー制御の例

複数のMySQLサーバーに順に接続を試して成功した接続を利用するとします。サーバーリストにあるMySQLサーバーに順に接続を試みますが、接続に失敗したとき、mysql_connectはFALSEを返しますが、同時にWarningを発生します。しかし、もともと失敗することを想定して次の接続を試したいときに、Warningが発生するのは不都合があります。返り値のFALSEをチェックして適切な対応コードを実行するべきときに、Warningは邪魔になります。
さて、問題は意味ですが、接続の失敗をあらかじめ想定している場合、PHPが想定しているWarningな事態ではありません。つまり、状況によってエラーの重みが変わることがあるのです。したがって、デフォルトと違うエラー制御をする方が適切です。

@を使うのはやむを得ない場面や、仕方がない場面(このPHP関数、関数内部でこういうWarningだしちゃうんだよ!的な)など、最低限に限るべし

という仕方がない場面のうちだと言えばそうかもしれませんが、エラーじゃないよというニーズに素直に対応するなら、ここはエラー制御演算子"@"の出番と考えるのが適切です。

"@"に対応したerrorハンドラーを書こう

もちろん、「error_reporting() の値をチェックして適切に処理するエラーハンドラを定義すればいいだろう」という主張はあるとは思いますが、"@"をつけたコードは、ハンドラの違いによってエラーの制御が適切にできるかできないかわからないコードとなってしまうのです。

http://d.hatena.ne.jp/sotarok/20090721/1248112106

『「error_reporting() の値をチェックして適切に処理するエラーハンドラを定義すればいいだろう」という主張はあるとは思いますが』とのことですが、まさにそうで、エラーハンドラを書くときに、error_reportingをチェックしないというのはまずあり得ないし、もし、無視しているなら、それでOKなシステムか、ダメなエラーハンドラなんじゃないでしょうか。
error_reportingを考慮しないエラーハンドラって「PHPセッションは危険だから、自前でセッション機構を作ったけど、PHPセッションより脆弱になった」みたいな、自己矛盾を抱えてます。

"@"をつけたコードは、ハンドラの違いによってエラーの制御が適切にできるかできないかわからないコードとなってしまうのです。

そうじゃなくて、"@"をつけたコードは、エラーを出さなくていいよという意味がわかるコードなのに、エラーハンドラで正しく制御できていないだけです。
"@"はエラー制御演算子です。エラー制御をおこなうと決めた時から、エラー処理関数の正しい実装とセットで正しく実装しなければなりません。
「エラーのハンドリングによっては意味をなさない」から"@"を使うなというのでは、問題の本質と対応個所がずれてしまいますよね。
と思ったら、set_error_handlerのマニュアルに出ている例では、error_reportingをチェックしていません。サンプルなんてそんなもんです。スポットを当てたところが表現できていればよくて、実践的かどうかで突っ込むのは野暮ってことですね。
ここではそれをちょっと書き換えます、error_reportingをチェックすると、こんな感じ。

<?php
function myErrorHandler($errno, $errstr, $errfile, $errline)
{
//error_reportingとの論理積で処理を分岐
    switch ($errno & error_reporting()) {
    case E_USER_WARNING:
        // USER_WARNINGに沿った処理
        break;
    case E_NOTICE:
        // NOTICEに沿った処理例:
        error_log( "My PHP Error: $errno, $errstr, in $errfile, on $errline\n");
        break;
    case 0:
	//処理対象外なので、何もしないで返す。
	//デバッグ用エラーハンドラーならここでログを残す。
        return true;
    default:
	/* 処理対象だが、ここでは定義していないので、PHP の内部エラーハンドラに任せる */
       return false;
    }

    /* PHP の内部エラーハンドラを実行しません */
    return true;
}

"@"を使わないクセがつくと、適切なエラーハンドリングやデバッグが身に付かない。

エラーハンドラを上記のように書くと、@を使うメリットがわかります。
元ネタに戻りますけど、

<?php
// isset
$q1 = isset($_GET['hoge']) ? $_GET['hoge'] : NULL;
// @
$q2 = @$_GET['hoge'];

通常であれば$q1,$q2に入る内容は同じとして、デバッグするシチュエーションで考えます。デバッガを使ったりassertを使用してデバッグということであれば、優位は見られないので、ここではエラーハンドラを使ったデバッグです。
上記したエラーハンドラのcase 0で単純にreturn true;していますが、アプリケーションでデバッグレベルに応じたエラーハンドラをセットして、case 0でもログを吐くように書くことができます。
すると、issetで書かれたコードでは、ログが拾えませんが、@の方では、$_GET['hoge']がセットされなかったというログを確認することができます。
"@"はエラーを吐かない演算子ではなく、エラー制御を最小化するための演算子だと理解するとわかりやすいです。

それでも"@"を使うとき

  1. @を使わないで代入
  2. issetで対処
  3. @と標準のPHPエラーハンドラ
  4. @と不適切なエラーハンドラ
  5. @と適切なエラーハンドラ

さて、どれにするか。状況によって使い分ければよいわけですが、4はないですね(笑)
$q1のようにissetでNULLを代入するのは、hogeが入ってこない可能性が十分にあるときで、入ってこないことが全く問題にならないときはこのように書きます。
一方$q2のように、@で制御するのは、共有機能内で代入時にチェックする必要はなく、利用側でチェックするが、デバッグ時には情報を取得したいとき。
え、そんなことあるの?というささやきが聞こえそうですが、issetで書かれたコードを@に書き直したら、サクッとデバッグできたなんてこともあるかもしれませんよ。
適切なエラーハンドラを書く習慣がある人からすると、せっかくのデバッグ情報を捨てるメリットは逆に少ないんじゃないでしょうか。*1少なくとも、コードの断片に@$_POST['hoge']と書いてあったからといって、それがダメなコードと断定できる理由はないです。
"@を笑う者は、人生の楽しみの半分を捨てているようなもの"*2

余談

もうひとつ、エラーハンドラっていうと、エラーが出たらなんでもそこで処理するなんていう誤解がないようにしたいです。エラー機構は例外機構とちがって、極限定された情報しか得られません。もし、エラーハンドラ側ですべてを制御することになると、どの部分で発生したどんなエラーかを常にトラックするソースを別途組み込む必要があります。これは現実的ではありません。どのファイルのどの行でエラーが発生したかというのはログに記録する情報としては使えますが、処理を分岐するために使うには不便ですね。エラーメッセージも条件分岐に使えるほど詳細で安定したものではありません。errnoで分岐するの程度以上のことはアプリケーションの実装に絡んでくるのでエラーハンドラには書かない方がいいと思います。(書くのは自由ですけど)
エラーの重みづけを決定するのは、エラーハンドラー側ではなく、エラーを起こす側です。エラーハンドラに、ここはミニマムなエラーだよと伝えるのが"@"の役割ということになります。
また、既存のエラーレベルを打ち消してより重大なユーザーエラーで対処したいときには"@"が使えます。

"@"を使わないっていうのは、PHPerがかかる"はしか"みたいなものです。一度はかかっておいた方がいいですが、いつまでもそれに固執しないほうがいいですね。

適材適所で"@"を多いに活用していただきたいです。適切なエラーハンドラとともに。

*1:スピード?

*2:うそ