例外処理はあった方がいいのでしょうか。 への返答

投稿で使用できる特殊コードの説明。(別タブで開きます。)
本名は入力しないようにしましょう。
投稿した後で削除するときに使うパスワードです。返答があった後は削除できません。
返答する人が目安にします。相手が小学生か社会人かで返答の仕方も変わります。
最初の投稿が質問の場合、質問者が解決時にチェックしてください。(以降も追加書き込み・返信は可能です。)
※「過去ログ」について書くときはその過去ログのURLも書いてください。

以下の返答は逆順(新しい順)に並んでいます。

投稿者 パル36  (中学生) 投稿日時 2011/8/21 13:09:02
shuさん、魔界の仮面弁士さん回答ありがとうございます。

わざわざ、それぞれのサンプルコードを作って下さりありがとうございます。

今回、の質問「例外処理はあったほうがいいのか」「On error」を使っていいのかについての答えが自分の中にできました。

自分は、on errorを乱用してしまったプログラムにtryで置き換えてみることにしました。
また、例外を作るということもしようと思います。

今回の質問で例外について再確認、学び直せました。

本当に感謝しています。
今回の質問で得た答えを今後に生かしていきたいと思います。


ありがとうございました!!
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2011/8/19 18:30:11
うわーっ。 ごめんなさい、On Error 版の修正案、今見たらバグってました。
ということで、先の回答は Try~Catch 案の方を参考にしてください…。

------

On Error 版については、本来の流れとしては、
 ・Err.Number = 0 はもちろん正常処理。
 ・Err.Number = 5 が今回行う例外処理。
 ・それ以外のエラーは例外のままとする。
を書こうとしていたのですが、先のコードだと、Err.Number ≠ 5 なエラーが発生した場合に、
エラーが握りつぶされてしまいますので、正しい処理にはなっていませんでした。すみません。
(たとえば、FileNotFoundException が発生した場合は、Err.Number が 53 を返します)

とりあえず、If Err.Number <> 0 Then に変更すれば、エラーを握り潰してしまうことは無くなりますが、
今度は想定外のエラーまでも処理対象になってきてしまうので、これも避けておきたいところ。


今回の処理では、想定外の例外はトラップしていなかったことにしておきたいので、
呼び出し元にそのまま例外を渡せるよう、できれば例外を投げ直したいところですが、
残念ながら、引数なしでの「Throw」呼び出しは、Catch ブロックでないと使えないので、
ElseIf Err.Number <> 0 Then
    Dim ex As Exception = Err.GetException()
    On Error GoTo 0
    Throw New InvalidOperationException("想定外のエラーです。", ex)
End If
として代用するぐらいしか思いつきませんでした。
この点では Catch を使う方がスマートですね。

なお、上記のコードを
ElseIf Err.Number <> 0 Then
    Dim ex As Exception = Err.GetException()
    On Error GoTo 0
    Throw ex
End If
にしてしまうと、エラー発生個所に関する情報(StackTrace)が失われてしまう事になります。

元の例外(ex)から得られる StackTrace には、Check 内でエラーが発生したことが示されていますが、
それを再度 Throw ex してしまうと、投げ直した箇所(Button1_Click 内)でエラーが生成されたという
情報へと StackTrace が変化してしまい、情報の一部を欠落させてしまいます。
(引数なしの Throw 呼び出しでは、StackTrace は変化しません)

まぁ、「Throw New InvalidOperationException("想定外のエラーです。", ex)」としても
StackTrace が Button1_Click 内を指してしまうという点では変わらないのですが…この場合は
InnerException 経由で本来の StackTrace を得られるため、情報は *一応* 残せます。
投稿者 パル36  (中学生) 投稿日時 2011/8/19 18:13:07
たくさんの回答ありがとうございます。

少し理解するのに時間がかかりそうなので後日また書きたいと思います。
すみません;

ありがとうございました。
投稿者 shu  (社会人) 投稿日時 2011/8/19 14:35:52
 魔界の仮面弁士さん
補足ありがとうございます。

例外が発生しているのに例外が発生しなかったかのごとく
正常処理が継続されてしまうことを表現したかったので
メッセージ内容が変になってしまいました。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2011/8/19 12:08:46
shu さんの回答に補足。

> 以下を実行すると
> On Error Reume Next
> If Check() Then
>       Console.WriteLine("例外が発生しなかったので削除処理をします。")
> Else
>       Console.WriteLine("例外が発生したので削除処理を中止します。")
> End If

# そのメッセージだと、Check は例外を返さない事が期待されているように読めるので、
# 呼び出し側の処置以前に、そもそもの Check メソッドの実装自体が間違っているという
# 印象を受けてしまいますが、それはとりあえず置いといて…。


この例の Button1 の処理を書き直すとすれば、こんな感じで如何でしょうか。
他にも書き方はありますが、一例という事で。

On Error を使った実装例
On Error Resume Next
Dim canDelete As Boolean = Check()
If Err.Number = 5 Then    'エラー番号が事前に分かっている場合は、Select Case の場合もアリ。 
   'エラー5 に相当する例外が発生した場合の処理 
   Console.WriteLine("例外が発生したので削除処理を中止します。")
   '※可能であれば、Err.Number の値に応じた処理を記述しておく。 
   Return
End If
On Error GoTo 0

If canDelete Then
   '戻り値として True が返却された場合の処理 
   Console.WriteLine("データは削除可能です。削除処理を開始します。")
Else
   '戻り値として False が返却された場合の処理例 
   'Console.WriteLine("データがロックされています。削除できません。") 
   'Console.WriteLine("データがありません。削除処理は中止されました。") 
   'Console.WriteLine("削除確認画面でキャンセルが選択されました。") 
   Return
End If

DeleteAll()  '←この部分に例外処理が必要かも考慮が必要 




Try~Catch を使った実装例
Try
   If Check() Then
       '戻り値として True が返却された場合の処理 
       Console.WriteLine("データは削除可能です。削除処理を開始します。")
   Else
       '戻り値として False が返却された場合の処理例 
       'Console.WriteLine("データがロックされています。削除できません。") 
       'Console.WriteLine("データがありません。削除処理は中止されました。") 
       'Console.WriteLine("削除確認画面でキャンセルが選択されました。") 
       Return
   End If
Catch ex As FooException   'Check が返す可能性のある例外 
   '例外が発生した場合の処理 
   Console.WriteLine("Fooの例外が発生したので削除処理を中止します。")
   '※可能であれば、例外内容に応じた処理を記述しておく。 
   Return
End Try

DeleteAll()  '←この部分に例外処理が必要かも考慮が必要 
投稿者 shu  (社会人) 投稿日時 2011/8/19 11:06:38
また

> if Check() then
の部分を

       Dim ret As Boolean
       ret = Check()

       If ret Then

とか

       Dim ret As Boolean
       ret = False
       ret = Check()

       If ret Then

にした場合は中止しますの表示になりますが、

       Dim ret As Boolean
       ret = True
       ret = Check()

       If ret Then

にすると削除処理を行う表示になってしまいます。
これは
ret = Check()
の代入自体が行われていないことを表しておりCheck処理の意味がなくなってしまいます。
投稿者 shu  (社会人) 投稿日時 2011/8/19 11:02:20
> >resume nextは処理によっては大変なことになりかねないので避けた方がいいです。
> そんなことが起こったりする可能性があるんですか!?

以下を実行すると

例外発生前
例外が発生しなかったので削除処理をします。

と表示されます。これは例外が発生しているのに削除を行うということになります。

    Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
        On Error Resume Next

        If Check() Then
            Console.WriteLine("例外が発生しなかったので削除処理をします。")
        Else
            Console.WriteLine("例外が発生したので削除処理を中止します。")
        End If
    End Sub

    Public Function Check() As Boolean
        Check = False

        Console.WriteLine("例外発生前")
        Throw New Exception("例外")
        Console.WriteLine("例外発生後")
        Check = True
    End Function
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2011/8/19 11:00:31
先の投稿の続き。

>> resume nextは処理によっては大変なことになりかねないので避けた方がいいです。
> そんなことが起こったりする可能性があるんですか!?

旧方式を避けた方が良いというか、できるだけ Try ステートメントを使った方が良いというか。

そもそも On Error ~ や Resume ~ は旧方式という事もあってか、Try に比べるとデメリットも多いです。
たとえば、Exception継承クラスと Err オブジェクトを比べると、情報量が多いのは圧倒的に前者です。
(Exception なら、どのメソッド内でエラーが発生したのかという情報までも追跡できます)

それでも旧方式を好むのであれば、個々の動作を十分に理解した上で使うようにしましょう。
個人的には Try を強く推奨していますが、旧方式の利用を否定するつもりもありません。


さて、旧方式について軽くおさらい。

「Resume」は、エラー発生個所を再度実行するもの。「Resume 復帰場所」は指定位置からの再開。
これらはたとえば、ダウンロード処理でタイムアウトした場合にリトライさせたり、
別のファイル名で再保存を試みたりするような状況で使えるでしょう。

このような Resume 処理は意図的に使われる事が多いため、問題となる可能性はやや低いかと思います。
(エラー回避処理が不十分だと、無限ループの様相を呈してしまう可能性はありますけれどね)


一方、問題になりやすいのは、今回話題となっている「On Error Resume Next」や「Resume Next」です。
前者は、エラーがあっても実行を止めず、Err オブジェクトとして保持するだけですし、
後者は、エラーのあった次の行から実行を再開させるステートメントです。

特に前者は、ある意味ではお気楽なステートメントに見えてしまい、乱用されがちなのですが、
結局のところ、エラーのあった行をスキップして処理を続行しているだけなのですから、
『なぜエラーが発生したのか』あるいは『その行を実行せずに次の行に進んで良いのか』などを
きちんと把握しておかないと、問題が出てしまうことになります。

たとえば、一度使用した変数値をうっかりリセットし忘れて、二回目以降の演算結果が桁あふれしたとします。
そのエラーをログに残すだけで、そのまま計算処理を続行したとして、正しい演算結果が得られるでしょうか。
あるいはデータベースを読み書きする処理があったとして、途中でエラーが発生したことを無視していた場合、
そのまま処理を続けて、異常値でデータを上書きしてしまう可能性は無いでしょうか。単純に言えば、そういう話です。


逆にいえば、エラーとなった処理をスキップしても大きな問題にはならない事が分かっている場合や、
Err.Number 判定等を行い、何らかの発生時のエラー対処処理が作りこまれているのであれば、
On Error Resume Next を使っても、堅牢なアプリケーションを作ることはできます。

なお、盲目的にすべてのエラーをただ捕まえるだけということの危険性は、
「On Error GoTo ~」や「Try」であっても同じ事です。
http://msdn.microsoft.com/ja-jp/library/ms182137%28VS.90%29.aspx


>> 絶対に例外が発生しないと分かっている箇所以外はあった方が良いと
>> 思います。
いろいろな考え方があると思いますが、実のところ、私はその逆だと思っていたりします。
ケースバイケースではありますが、基本は「書かない」と割り切った方が良い、と。

(とはいえ本当に必要なのは、例外を Catch すべきかどうかという議論ではなく、
 どんな例外を Catch し、それを Catch した後どうすべきか、という話なのですけれどね)


私の場合、Catch するのは事前に対処できない部分(ファイル入出力、ネットワーク接続処理など)に
絞っています。最低限必要なものだけを Catch し、それ以外は基本的に Catch しないという方法です。

これだと想定外のエラーに関しては取りこぼされることになりますが、デバッグ時には
むしろ問題箇所を発見するための手助けとなりますので、開発者にとっては分かりやすいです。

ただしデバッグ時以外だと、ユーザーに生のエラー画面を見せてしまう可能性があるため、
あまり望ましくありません。これに関しては、最上位のメソッド(いわゆる「Sub Main()」や、
集約例外ハンドラ(UnhandledException イベントや ThreadException イベントなど)で
対処するという手法が利用できます。
http://jehupc.exblog.jp/9247467/


> おっしゃるとおり、絶対確信がある場所以外は例外処理をおいたほうがいいですね。

ここで、もう一度考えてみて欲しい事があります。

確信の無い処においたとして、そこにはどのような対処コードを記述すべきでしょうか。
確信の無いまま記述された処理は、正しく動作するのでしょうか。
そもそも、それらの例外処理はどこに配置されるべきでしょうか。
確信の無きエラー処理が、不具合を隠す/見えにくくする可能性はないのでしょうか。
エラーをログに記録するとして、そのログには、エラーがどんな時にどのメソッドで
発生していたのかを追跡できるだけの情報を備えさせているでしょうか。


例外発生のダイアログをユーザーの目に触れさせてしまうのは、もちろん避けねばなりませんが、
だからといって無策の例外処理を施しただけなら、それは問題の解決とはなりえません。ゆえに

・そもそも、できるだけ例外が起きないようにする(例外処理以前の話)
・例外の発生を抑えられない場合は、個々の例外に対処するコードを書く(Try ~ End Try)
・続行不能な例外や、想定していないエラーが発生した場合には、それを通知したり、
 安全に中断/終了させるための処理を講ずる
・ログとして記録するのであれば、後から解析するに足る十分な情報を残すようにする

という心構えが大切となってきます。

それらをきちんと理解した上で、あえてエラーを無視しているケースにおいては、
それはそれで正しいコードであろうと思います。しかし、そうした考慮を行うことなしに、
闇雲にただ処理を Catch しているだけだとしたら問題です。それならむしろ例外処理を
書かない方が、原因不明の問題を作りこんでしまう事態を避けやすくなるでしょう。

ということで、むやみに Try~Catch や On Error を書くべきでは無く、書くのであれば、
処理の流れを意識した上で、本当に必要な場所に絞って記述することをおすすめしておきます。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2011/8/19 10:48:17
# 長文になってしまったので、2 回にわけて投稿します。

>>> そもそも例外処理は必要なのでしょうか。
不要か必要かと問われれば必要と答えることにはなりますが、
それがどんな時に必要なのかは考慮せねばなりません。

.NET における「例外」とは、基本的には下記の状態を表すために使われる物です。
(Java の場合は、例外に対する考え方が微妙に異なっていたりしますが)
http://blogs.msdn.com/blogfiles/nakama/WindowsLiveWriter/.NETPart.1_E714/image5%5B1%5D.png

上記にあるように「異常事態」なのですから、開発者が責任を持って対処することが望ましいです。
ただしその対象方法とは、先述したように必ずしも例外処理を指すとは限りません。

ライブラリの設計者は、異常事態発生時に 例外をスローさせるように作りこみますが、
利用者としては、その例外をキャッチして対処するという選択肢の他に、そもそも
そうした異常事態を引き起こさないような作り方をするという選択肢があるからです。

たとえば、平均値を求めるメソッド(たとえば Enumerable.Average )に対して
0 個の値を渡した場合、当然ながら平均値を求めることはできません。
この場合 Averaga メソッドの作成者は、それを例外として返すように設計しますが、
利用する側としては、その例外を Catch して制御するという対処療法では無く、
そもそも 0 個の値を渡さないようにコーディングする事が求められるわけです。

Exception とはそもそも、本来の処理から逸脱した例外的な事態を
報告するための特殊な流れなので、そうした例外処理が乱発されてしまうと、
本来のプログラムが分かりにくくなる要因ともなります。その点はご注意を。


> ここでのスローとは何でしょうか。
単純に言えば、エラーを再発生させることです。

エラーを発生させるための「Err.Raise エラー番号」とか、「Throw 何某」と似ていますが、
「Throw」は、新たなエラーを通知するのではなく、現在処理中の例外を再通知させるために使います。


> 例外について調べていると、「スロー」という言葉が出てくるのですがよくわかりません。
「投げる」を意味する Throw です。
Catch ブロック内で「Throw」とだけ書きます。

この場合、捕まえた(Catch した)例外を再度投げなおす、という意味になります。
(具体的なサンプルを踏まえて書いた方が良いのかな…?)


なお、“ある程度理解が進んできたら”下記のblog記事が参考になるかと思います。
初心者向けの記事ではありませんし、C# 前提に書かれた記事ではありますが、
内容的には VB.NET 開発にも通ずるものがありますので。

[エラーチェックの体系的な分類方法]
http://blogs.msdn.com/b/nakama/archive/2009/09/29/9900569.aspx

[.NETの例外処理 Part.1]~[.NETの例外処理 Part.4]
http://blogs.msdn.com/b/nakama/archive/2008/12/29/net-part-1.aspx
http://blogs.msdn.com/b/nakama/archive/2009/01/02/net-part-2.aspx
http://blogs.msdn.com/b/nakama/archive/2009/01/18/net-part-3.aspx
http://blogs.msdn.com/b/nakama/archive/2009/01/23/net-part-4.aspx 
投稿者 (削除されました)  () 投稿日時 2011/8/19 10:46:32
(削除されました)
投稿者 パル36  (中学生) 投稿日時 2011/8/18 21:42:30
魔界の仮面弁士さん、shuさん
ご返答ありがとうございます。

>resume nextは処理によっては大変なことになりかねないので避けた方がいいです。
そんなことが起こったりする可能性があるんですか!?
おっしゃるとおり、絶対確信がある場所以外は例外処理をおいたほうがいいですね。

>そういう意味においては、無暗やたらと  On Error や Catch すべきではありませんし、
>かといって、まったく例外処理を行わないというのもまた問題ですね。 
バグは必ずつくので、例外を何でも付けたくなってしまいます。
それで簡単なOn Errorを使ってしまいました。

>エラー回復のための手順を講ずるのが望ましいです。
参考になります。
実際、ログとして出しているのはエラー番号とその詳細ですから難しいのも出てきたりしました。
(例)「I/Oは同時サポートされていない」などです。自分もあまりよく分かりません。。

>例外処理が握りつぶされると、プログラムのバグや環境依存の問題などが
>見落とされますし、連鎖的に発生したエラーでは、StackTrace が失われて
>エラーの発生個所や原因が分かりにくくなるなどの問題を生じます。
なぜか自分のプログラムが怖くなってきました。このプログラム自体がバグではないかと思ってしまいます。

>それが不可能な場合は、そのまま再度 Throw する。
いいアドバイスをもらっていると思うのですが、ここでのスローとは何でしょうか。
やはりキーワードのことですか。
例外について調べていると、「スロー」という言葉が出てくるのですがよくわかりません。
よければ、簡単に教えて下さい。

わざわざ参考サイトまで提示してくださってありがとうございます。
そのサイトにはいつもお世話になっています。

回答ありがとうございました。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2011/8/18 16:36:10
> いつもエラーを無視し、ログで表示させています。

「例外をトラップするべきかどうか」も重要ですが、
それ以上に「例外処理で何を行うか」の方が大切ですね。

先程書いたような、プログラム的に回避できない例外については、ユーザーに何らかの操作を
期待することが多いですが(再入力を促すとか、ネットワーク環境を確認してもらうなど)、
エラーメッセージをわかりやすい文章に置き換えたり、データの再入力や再試行を促したり、
アプリの終了処理に入るなど、エラー回復のための手順を講ずるのが望ましいです。


一方、すべての例外を盲目的に Catch する事があるとすれば、それは
パル36さんが書かれたように、エラーを『ログ』として記録し、
開発者や管理者が後から現象を追跡できるようにする場合などが考えられます。

しかしそうした場合でも、
> いつもエラーを無視し、
というのは避けた方が良いでしょう。

例外処理が握りつぶされると、プログラムのバグや環境依存の問題などが
見落とされますし、連鎖的に発生したエラーでは、StackTrace が失われて
エラーの発生個所や原因が分かりにくくなるなどの問題を生じます。

なのでログを取得した後は、「無視」するのではなく、
 ・エラー回復のための何らかの処置を講じる。
 ・それが不可能な場合は、そのまま再度 Throw する。
のいずれかを行うことを検討してみてください。

http://dobon.net/vb/dotnet/beginner/exceptionhandling.html
投稿者 shu  (社会人) 投稿日時 2011/8/18 14:46:10
絶対に例外が発生しないと分かっている箇所以外はあった方が良いと
思います。

resume nextは処理によっては大変なことになりかねないので避けた方がいいです。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2011/8/18 14:32:31
たとえば、「0で除算」や「配列のインデックスに範囲外の値を指定」、「型変換エラー」などの
例外は、Catch するべきでは無いとされます。これらは、そういう処理が行われていること自体が
問題であると言えますので(バグと言えます)、むしろそういう例外が発生することがないよう、
値の事前チェックを行うなど、コードを書きなおすことで対応するべきです。

一方、「ディスクがいっぱいで保存できない」「ダウンロード先のサーバーと通信できなかった」等の
例外というのは、実行してみないと検出できない処理ですので、これらは積極的に Catch すべきです。
(ディスクの空き容量を調べてから保存するといった対処方法を取ることはできますが、
 調べてから実際に保存するまでの僅かな間に、ディスク容量やアクセス権が変化する可能性も
 ありますので、これらについては事前チェックすれば済むということにはなりません)

そういう意味においては、無暗やたらと  On Error や Catch すべきではありませんし、
かといって、まったく例外処理を行わないというのもまた問題ですね。
投稿者 パル36  (中学生) 投稿日時 2011/8/18 13:09:33
こんにちは。

プログラムをかいているときは、いつもエラーを無視し、ログで表示させています。

On Error Resume Next

If Err.Number <> 0 Then
Textbox1 += vbCrLf & "エラーが発生しました。 " & Err.Number & " " & Err.Description
End If

On Error ... は、あまり使わないほうがいいと思っています。
そうすると、try文などを使わざるを得ません。

そもそも例外処理は必要なのでしょうか。
初歩的な質問ですが、宜しくお願いします。