掲示板お問い合わせランダムジャンプ



この広告は30日以上更新がないブログに表示されております。 新しい記事を書くことで広告を消すことができます。

Top Index

2014年07月27日
例外処理入門(その2)
例外処理入門(その1)の続き。

例外の再発生(その1:ThrowとThrow exの違い)


「例外発生時の事後処理を書き、エラー処理は呼び出し元に任せる」ということはよくあります。
エラーのメッセージ処理がそうですね。
例を挙げると、こんな感じ。

※例外処理入門その1で「Exceptionクラスを直接発生させたり、捕まえたりするな」と言っておりましたが、
 毎回あれを考慮したコードを載せていては情報量が不必要に多くなるので敢えてこのように書いております。
 ご了承ください。

ここで23行目に注目!
例外を再発生させるコードは、
Throw
です。
たまに
Throw ex
と記述する人がいます。
たぶん、例外の任意発生(Throw New Exception)の構文からこう書いてしまったのでしょうが、
Throw とだけ書くようにしてください。

強調するのにはわけがありまして、「Throw だけ」と「Throw ex」ではエラーが発生した位置情報(StackTrace)が異なってくるからなのです。
以下にStackTraceの実例を挙げます。

Throw ex と書いた場合のメッセージとStackTrace


ファイル 'c:\data.txt' が見つかりませんでした。
場所 ConsoleApplication1.Module2.ExecuteHoge() 場所 C:\...\Module2.vb:行 23
場所 ConsoleApplication1.Module2.Main() 場所 C:\...\Module2.vb:行 6


エラー発生場所が「23行目」となっていますが、23行目は
Throw ex
ですので、こんなところでエラー起きてますといわれても「絶対そこじゃない」というわけです。

Throw と書いた場合のメッセージとStackTrace


ファイル 'c:\data.txt' が見つかりませんでした。
場所 System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
場所 System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)
場所 System.IO.File.Copy(String sourceFileName, String destFileName)
場所 ConsoleApplication1.Module2.ExecuteHoge() 場所 C:\...\Module2.vb:行 23
場所 ConsoleApplication1.Module2.Main() 場所 C:\...\Module2.vb:行 6


行数は「23行目」と同じですが、それより前の情報が取得できます。
行数はここまでしか特定できませんが、「System.IO.File.Copy」メソッドでこけたことが推察できます。


例外の再発生(その2:内部例外)


Throwと書く以外にも例外を再発生方法があります。
内部例外です。
ExceptionクラスにはInnerExceptionプロパティというものがあり、これを使用します。

ポイントは29行目のコードです。
Throw New Exception("Hogeに失敗しました", ex)
第2引数が内部例外(InnerException)に代入されます。

 exのメッセージとStackTrace


Hogeに失敗しました
場所 ConsoleApplication1.Module2.ExecuteHoge()
場所 C:\...\Module2.vb:行 23
場所 ConsoleApplication1.Module2.Main() 場所 C:\...\Module2.vb:行 6


 ex.InnerExceptionのメッセージとStackTrace


ファイル 'c:\data.txt' が見つかりませんでした。"
場所 System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
場所 System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)
場所 System.IO.File.Copy(String sourceFileName, String destFileName)
場所 ConsoleApplication1.Module2.ExecuteHoge()
場所 C:\...\Module2.vb:行 15


この方法であればInnerExceptionプロパティをみることで、15行目が原因であることまでトレースできます。
ただ、例外をラッピングしないといけないため、エラーメッセージがわかりづらくなりがちです。
ここら辺は「例外の再発生(その1)、(その2)どっちにすべき」と縦割りすることはできませんので、
用途に応じて使い分けてください。

エラーメッセージの取得


ex.Message
と記述すればいいと思うかもしれませんが、実際には誤り(というか不十分)です。
先程挙げた内部例外があるかどうかを見て、内部例外まで評価しなけば正しい情報を取得できません。

こちらは機械的に内部例がNothingじゃなければ再帰して取得すればいいだけの話ですので、
拡張メソッドのコードを置いておきます。



Catchブロックに「Throw」の1行だけ書くのはNG


例えば、「例外処理はないけど、事後処理だけはしたい」場合、こういうコードを書く人がいます。


文法上間違いではないですが、

例外を再発生すると、StackTraceが変わる。
Catchブロックは必須ではない。

ので、
Catchブロックが「Throw」の1行しか書いてないのであれば、そもそもCatchする必要がありません。
以下のようにしましょう。


コード量も減らせられるし、StackTraceもきれいに出ます。
最後に修正前と修正後のStackTraceを比較してみましょう。

修正前のメッセージとStackTrace


ファイル名 'c:\data.txt' です。'c:\data.txt'
場所 System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
場所 System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)
場所 System.IO.File.Copy(String sourceFileName, String destFileName)
場所 ConsoleApplication1.Module3.ExecuteHoge() 場所 C:\...\\Module3.vb:行 19
場所 ConsoleApplication1.Module3.Main() 場所 C:\...\\Module3.vb:行 6


修正後のメッセージとStackTrace


ファイル 'c:\data.txt' が見つかりませんでした。
場所 System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
場所 System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)
場所 System.IO.File.Copy(String sourceFileName, String destFileName)
場所 ConsoleApplication1.Module3.ExecuteHoge() 場所 C:\...\\Module3.vb:行 15
場所 ConsoleApplication1.Module3.Main() 場所 C:\...\\Module3.vb:行 6


修正後の方がわかりやすいですね。


例外処理が不要なら、そもそも書く必要はない


例外処理を覚えると、今度はやたらと組み込み始める人がいます。
極端な例だとすべてのサブルーチンに組み込み始めるのですが、処理がないなら書く必要はありません。
既に例を挙げてしますが、
サブルーチンで例外が発生しても、呼び出し元にTry〜Catch文が書かれていれば、呼び出し元のCatchブロックまでぶっ飛びます。

※サブルーチンでエラーが出てもちゃんとメッセージ処理はされます。

一般的には(メッセージ処理を行う関係上)呼び出し元のルートにはTry〜Catch文を書く必要はありますが、
それ以外は例外処理、事後処理が存在しないならTry〜Catch文を書く必要はありません。


積極的に例外を発生させよう(任意)カスタム例外を使ったエラーメッセージ処理


処理結果を戻り値で判定するコードはよく見ます。
例を挙げるとこんな感じ。

※戻り値をBoolean型でやっていますが、String型(エラーメッセージ)を返すパターンもあります。

「処理失敗なら必ずエラー処理をして抜ける」という前提であれば、以下の点でよろしくありません。
・isSuccess(実行結果を管理する変数)への代入忘れに気を付ける必要があり、成功失敗判定が狂いやすい。
・例外のメッセージ処理を都度実装しており、冗長。

改善方法はいろいろありますが、「異常が見つかったらカスタム例外で返す」のがよいでしょう。
修正後のコードはこちら。

※今回は「FileNotFoundException」を使用しましたが、カスタム例外で読み替えてください。

以下の点が改善されました。
・サブルーチンは失敗した場合、失敗の理由(data.txtがファイルがありません。)がわかるようになった。
・サブルーチンは戻り値管理が不要になった。
・メインルーチンは成否判定が不要になった。
・メインルーチンの例外メッセージ処理がTry〜Catch文1つに集約された。


備考:
今回は「処理失敗なら必ずエラー処理をして抜ける」前提でのコードでの話です。
[ 投稿者:mk3008 at 10:57 | VB.NET | コメント(3) | トラックバック(0) ]

この記事へのコメント
積極的に例外を発生させよう(任意)が気になりました。
MSDNの例外のスロー(http://msdn.microsoft.com/ja-jp/library/ms229030(v=vs.100).aspx)を見ると「通常の制御フローでは、できれば例外を使用しないでください。 システム エラーや競合状態が発生する可能性がある操作を除き、フレームワークのデザイン時には、例外をスローしないコードをユーザーが記述できるように API をデザインする必要があります。 たとえば、メンバーを呼び出す前に状態をチェックできるようにすると、例外をスローしないコードをユーザーが記述できるようになります。」とあるので、積極的にってのは言いすぎな気がしました。
投稿者: karuakun at 2014-07-31 17:44:36
Re:積極的に例外を発生させよう(任意)が気になりました。
karuakun さん、コメントありがとうございます。
>積極的にってのは言いすぎな気がしました
この辺りはデリケートな内容ですので、
「初学者向け」という名目で上げたのはまずかったですね。
もっともなご指摘ありがとうございます。
ストライクしておきます。

それだけでは何ですので、初学者向けという前提を取っ払って、
私の個人的な意見を申し上げると…
「フレームワークではすべきでないが、アプリケーションレベルならいいんじゃない?」
と考えます。

例えば、
・これのときはX例外が飛ぶけど、これはエラーにはしないで処理続行して
・あれのときはY例外が飛ぶけど、これは処理続行して
なんていうフレームワークを広く万人に公開したとしても、まともに使える人はいないでしょう。
ミスを誘発するので仕様バグ。
(文法の例でCatchブロックを連発している段落がありますが、
(実際のところあんなコードはお目にかからないです。

では、アプリケーションレベル、各実装単位でみた場合はどうか。
開発者が限定的、かつ教育も十分にできる範囲であれば、
入力チェックエラーなどをカスタム例外で対応することは問題だとは思いません。
少なくとも「Module4」の例であれば、
カスタム例外を利用したコードの方が不具合を生じにくいです。
(「以下の点が改善されました。」のくだり参照)

今の私の理解度はこんな感じです。
投稿者: mk3008 at 2014-07-31 23:31:38
無題
返信有難うございます。
そうですね。結局はアプリケーション全体の設計の話になるので、チーム内で例外を投げる条件と補足すべき条件が共有されているのであればビジネス処理に関わる検査も例外で処理するという方法も間違っていないと思います。

>「フレームワークではすべきでないが、アプリケーションレベルならいいんじゃない?」
メソッドの条件が満たされない場合はフレームワーク側でも例外を投げますね。防御的にプログラミングするという観点ではアサーションを検討してもいいですね。
投稿者: karuakun at 2014-08-04 09:40:40

この記事へのトラックバック

この記事へのトラックバックURL
http://shinshu.fm/MHz/88.44/a02480/0000446295.trackback

この記事の固定URL
http://shinshu.fm/MHz/88.44/archives/0000446295.html

記事へのコメント
 
簡単演算認証: 5 x 7 + 5 =
計算の答えを半角英数字で入力して下さい。
名前: [必須]
URL/Email:
タイトル:
コメント:
※記事・コメントなどの削除要請はこちら