Visual Basic 初級講座 [改訂版]
VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

第23回 例外を上手に扱う

2020/10/18

この記事が対象とする製品・バージョン

VB2019 Visual Basic 2019 対象です。
VB2017 Visual Basic 2017 対象です。
VB2015 Visual Basic 2015 対象です。
VB2013 Visual Basic 2013 対象です。
VB2012 Visual Basic 2012 対象です。
VB2010 Visual Basic 2010 対象です。
VB2008 Visual Basic 2008 対象です。
VB2005 Visual Basic 2005 対象です。
VB.NET 2003 Visual Basic.NET 2003 対象外ですがほとんどの説明があてはまるので参考になります。
VB.NET 2002 Visual Basic.NET (2002) 対象外ですがほとんどの説明があてはまるので参考になります。
VB6対応 Visual Basic 6.0 × 対象外です。

 

目次

1.どこにどのような例外処理を書くべきか

1-1.3種類の例外処理の比較

前回説明したように例外処理には次の3種類があります。

このうち、自分で例外処理の内容をプログラムできるのは Catch と 未処理の例外処理の2箇所です。

違いをまとめてみましょう。

  Catchでの例外処理 未処理の例外処理 フレームワークの既定の例外処理
ポイント きめ細かい例外処理 包括的な例外処理 既定の例外処理
カスタムな例外処理は記述できない。
使いどころ ・機能ごとに固有のきめ細かい例外処理が必要な場合
・通常のフローに復帰させられることがわかっている場合
・ログの記録やメッセージの表示など、どこで処理しても同じような例外処理の場合
・通常のフローに復帰できるかわからない場合
・まったく想定していない例外が発生した場合
何箇所でも記述できる。 通常は1箇所 1箇所
通常のフローへの復帰 通常のフローに復帰できる。(復帰しないこともできる。) 通常のフローには復帰できない。プログラムは終了する。
(Webアプリの場合は、プログラム本体ではなく当該リクエストの処理が終了)
通常のフローには復帰できない。プログラムは終了する。
(Webアプリの場合は、プログラム本体ではなく当該リクエストの処理が終了)
処理できる例外の指定 対象の例外を指定できる。
(Catch As XXXException)
対象の例外は指定できない。 対象の例外は指定できない。
カスタムな発動条件 自由に条件を記述できる(When xxxx) 条件は記述できない。 条件は記述できない。

この表は少し簡略化しており、下記の点は厳密には正しくありません。が、この点の考慮が必要なことはまずないので、この表の理解で考えていけばよいと思います。

・アプリケーションの種類によっては未処理の例外処理から通常のフローに復帰できる場合もあります。(ただし、End Try の下から復帰するのではなく、呼び出し元から復帰します。)

 

1-2.練習問題

■の部分にあてはまるものを選択してください。

問1.作業中のデータをファイルに保存する処理で例外が発生した場合の例外処理はどうしたらよいでしょうか?
Catch
未処理の例外処理
既定の例外処理
問2.例外発生時に例外の情報をログに記録してプログラムを終了させたい場合どうしたらよいでしょうか?
Catch
未処理の例外処理
既定の例外処理
問3.作成中のチャットプログラムには、1人でチャットの練習をする練習モードと、複数人で会議をする会議モードがあります。通信処理で例外が発生した場合、会議モードのときだけ例外処理を実行するにはどうしたらよいでしょうか?
Catch
未処理の例外処理
既定の例外処理

 

2.例外を発生させる

例外処理をある程度理解して使いこなすようになると、自発的に例外を発生させたくなる場合があります。

あえて、待ち構えている(かもしれない)Catchや未処理の例外処理を実行させたいことがあるからです。

Throw (読み方:Throw=スロー) を使うと自発的に例外を発生させることができます。

たとえば、次の通りです。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019


Throw New
NullReferenceException

Try ~ Catch していない場合は、この行が実行されるとプログラムの実行は停止します。通常の例外が発生した場合と同じ反応です。

 

どういう場合に、自発的に例外を発生させたくなるのか、 BMIを計算するプログラムを題材に、説明します。

BMIは肥満度を表す指数であり、体重と身長を使って次の計算できます。

体重 ÷ (身長 × 身長)

※体重はキログラム単位、身長はメートル単位で指定します。

 

たとえば、体重80Kg、身長 1.7メートル の場合、BMIは 80 ÷ (1.7 × 1.7) = 約 27.7 です。BMI 27.7 は肥満(1度) に相当します。

BMIを計算するプログラムは次の通りです。XMLコメントにBMIの数値の評価も書いておきました。普通体重は18.5~25の範囲です。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

''' <summary>
''' 身長(m)と体重(kg)からBMI(ボディマス指数)を計算します。
''' BMIは高校生以上の肥満度を表す指標で、18.5未満が痩せ型、18.5~25未満が普通体重、
''' 25~30未満が肥満(1度)、30~35未満が肥満(2度)、35~40未満が肥満(3度)、
''' それ以上が肥満(4度)を表します。
''' </summary>
''' <param name="height">身長をメートル単位で指定します。</param>
''' <param name="weight">体重をキログラム単位で指定します。</param>
''' <returns>BMI</returns>
''' <remarks>3ヶ月から5歳までのの肥満度はカウプ指数、中学生以下の肥満度はローレス指数で表します。</remarks>
Private Function CalcBMI(height As Double, weight As Double) As Double

    Return weight / (height * height)

End Function

このプログラムは問題なく動作するはずです。

呼び出し例は次の通りです。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'身長 1.7メートル、体重 80キログラム の場合の BMI を求めます。
Dim bmi As Double = CalcBMI(1.7, 80)

ところで、私たちは普段身長をセンチメートル単位で扱うことが多く、身長を聞かれると 165cm ですのように cm で答えるのが普通のように思います。

この感覚だと次のように間違った引数を指定してしまうことがありそうです。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019


Dim
bmi As Double = CalcBMI(165, 70)

このプログラムは身長165cm、体重70kgの場合のBMIを求めるつもりなのですが、CalcBMIの第1引数である身長はメートル単位を前提としているため、身長 165メートル という意味になってしまいます。この結果BMIは 約 0.003 というありえないほど小さな値になります。

165メートルのようなありえない身長が指定された場合、指示されたとおりに通常のフローを実行してBMIを計算するよりも、何かおかしいことが起こっているという「例外」的な状況を知らせたほうが親切ですよね?

プログラムに即して言うと 引数 height の値が例外的状況を作り出しているということになります。

そこで身長に 3以上の値が指定された場合、例外を発生させるようにしてみましょう。引数がおかしいので使用する例外クラスは ArgumentException です。

※引数の範囲がおかしいと考えてArgumentOutofRangeExceptionを発生させるという考え方もあります。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

''' <summary>
''' 身長(m)と体重(kg)からBMI(ボディマス指数)を計算します。
''' BMIは高校生以上の肥満度を表す指標で、18.5未満が痩せ型、18.5~25未満が普通体重、
''' 25~30未満が肥満(1度)、30~35未満が肥満(2度)、35~40未満が肥満(3度)、
''' それ以上が肥満(4度)を表します。
''' </summary>
''' <param name="height">身長をメートル単位で指定します。</param>
''' <param name="weight">体重をキログラム単位で指定します。</param>
''' <returns>BMI</returns>
''' <remarks>3ヶ月から5歳までのの肥満度はカウプ指数、中学生以下の肥満度はローレス指数で表します。</remarks>
Private Function CalcBMI(height As Double, weight As Double) As Double

    If height >= 3 Then
        Throw New ArgumentException
    End If

    Return weight / (height * height)

End Function

これで、何か引数がおかしいということは呼び出し元に伝わるようになります。

もう少し親切にしてみましょう。2つある引数のうちおかしいのは height であり、おかしい理由は (おそらく)単位がセンチメートルになっているからであり、メートル単位で指定するべきである という情報を付加します。次のようになります。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

''' <summary>
''' 身長(m)と体重(kg)からBMI(ボディマス指数)を計算します。
''' BMIは高校生以上の肥満度を表す指標で、18.5未満が痩せ型、18.5~25未満が普通体重、
''' 25~30未満が肥満(1度)、30~35未満が肥満(2度)、35~40未満が肥満(3度)、
''' それ以上が肥満(4度)を表します。
''' </summary>
''' <param name="height">身長をメートル単位で指定します。</param>
''' <param name="weight">体重をキログラム単位で指定します。</param>
''' <returns>BMI</returns>
''' <remarks>3ヶ月から5歳までのの肥満度はカウプ指数、中学生以下の肥満度はローレス指数で表します。</remarks>
Private Function CalcBMI(height As Double, weight As Double) As Double

    If height >= 3 Then
        Throw New ArgumentException("値がメートル単位で指定されていません。センチメートルやインチではなくメートル単位で指定してください。", NameOf(height))
        '↓VB2013以前の場合はこちらを使う
        'Throw New ArgumentException("値がメートル単位で指定されていません。センチメートルやインチではなくメートル単位で指定してください。", "height")
    End If

    Return weight / (height * height)

End Function

このようにほとんどの例外にはコンストラクタで必要な情報を付加できるような設計になっています。ArgumentExceptionの場合第1引数で自由なメッセージを設定して、第2引数で問題のある引数の名前を指定できます。

この例に登場する NameOf (読み方:NameOf=ネームオブ)は引数や変数などプログラム中の識別子の名前を文字列として取得できる便利な命令です。VB2015から導入されたため、それ以前の場合は、直接文字列を指定して"height"と記述することになります。後で引数の名前を変更するときにNameOfを使っていると変更忘れを防げたり、変数や引数の名前変更機能で連動して変更してくれるのが便利な点です。

 

これで間違って、CalcBMI(165, 70)のような呼び出しをするとVisual Studio上では次のメッセージが表示されます。

ThrowしたArgumentException

とても親切ですよね。何がおかしくてどうすればいいのかわかります。

おかしい引数は height であるという情報はスクロールしないと見えないところに隠れています。メッセージを1行スクロールすると パラメーター名: 'height と表示されます。

メモ メモ  -  .NETの例外も親切ですよ

実は.NETの他の例外でもマイクロソフトはかなり親切に解決方法のヒントをちりばめているので、例外が発生したらメッセージをよく読んでください。

ところで、BIMの例では「センチメートル」ではなく「メートル」にしてくださいというようなメッセージになっており、一見わかりやすいですが、もし、「センチメートル」や「メートル」のことをよく知らない人がいたとしたら、わけがわからないメッセージに見えてしまうかもしれません。それはもうVBの問題ではないので、申し訳ないですが、「センチメートル」や「メートル」についてはもしわからなければ自分で調べてくださいということになってしまいます。(メッセージに「もし、センチメートルを指定しているのならその百分の一値を指定してください。」と追記するともっと親切になるでしょうか?センチメートルのことがわからない人がもしいたとして、この追記が親切なのかかえって混乱を招くのか難しいところです。)

この考え方を広げてみると、たとえば、「SSLの代わりに TLS 1.2 を使ってください。」のような例外メッセージが表示された場合、わけがわからないと思う人がいることでしょう。でも、もはやVBの問題ではないので「SSL」、「TLS 1.2」などについては自分で調べるべきかもしれません。何を調べればよいかというキーワードを教えてくれているだけで親切な世の中になったのだなと私は感じます。(メッセージに、「TLS 1.2 を使用するには、ServicePointManager.SecurityProtocolプロパティに TLS 1.2を指定してください。」と追記されているともっと親切になるでしょうか?)

 

3.Try を使わない

Try を使わない

初級講座では3回にわたって例外処理を説明してきました。例外処理をはじめて知ったプログラマーは Try ~ Catch を書かないといけないんだなと誤解してしまうことが多いので、この段階で明言しておきます。Try ~ Catch は書かないほうが良い場合の方が多いです。

もっというと 私が特に指摘したいポイントは原則として Try を使わない ということです。

私は いろいろな人が作ったプログラムをレビューしたり評価したりする機会があり、良くない Try の使い方をしているプログラムをたくさん見てきました。そのため特にこのポイントを強調してお伝えしたいと思います。

「原則として」なので、Try を使うべきときはもちろんありますが、どう考えたらよいかわからないのであれば、使わないほうを選択するほうが正解であることが多いです。

Try ~ Catch の使用が「原則として」ダメな理由は次の通りです。

 

実行すべき例外処理がないことがほとんど

例外のほとんどは事前に予測していなかったり、例外に対応した特別な処理がないものばかりです。

特別な例外処理がないのに Catch してしまったら、既定の例外処理や未処理の例外処理が実行されなくなってしまうので Catch しないほうが良いのです。

たとえば、メモリの不足を表すOutOfMemoryExceptionはいつでも発生する可能性があります。これをCatchで捕まえてやるべき処理など通常はありません。だから Catch するべきではないのです。

Try ~ Catch を使うときは、 その Try ~ Catch の間で例外が発生したら他の場所で例外が発生したときとは違った何か特別な例外処理が必要どうかを考えて見ましょう。もし、特別な例外処理が必要なら、Catch で例外処理を記述してよいでしょう。ログ出力やメッセージの表示はどこにでも当てはまる機能なので、「その」Try ~ Catch で必要な特別な例外処理とはいえません。特別な例外処理が不要ならTry ~ Catch の代わりに 未処理の例外処理を使うべきです。

メモ メモ  -  ユーザーにエラーを見せないという価値観 は ダメ

乗っている電車が急に止まった。もう1時間経っている。10分おきくらいにアナウンスで「安全確認中です。しばらくお待ちください。」と流れる。・・・こんな状況はすごくストレスですよね。長くかかるなら線路の途中でも降ろしてもらえないか、とりあえず最寄の駅まで走っておろしてもらうことはできないのか、せめて何が起こってどういう状況なのか教えて欲しい。こんな風に思うと思います。(最近はスマホで能動的に情報収集やコミュニケーションできますが。)

ユーザーにとって、エラーなしでアプリケーションが使えることは良い体験ですが、この価値観を重視するあまり、想定外の「例外」が発生しているのに、何事もなかったように見せかけたり、簡単なメッセージを表示する程度で済まそう考えるプログラマーがいます。これはたいてい悪いことです。(これが冒頭の電車の比喩で伝えたかったことです。)

想定外の例外が発生した場合、ユーザーのデータの保護を最優先に考える必要があります。ここで言う「ユーザーのデータ」とはそのアプリケーションでユーザーが編集している未保存のデータのことです。たとえば、ExcelやWordで保存前にエラーが発生した状況を考えると良いと思います。ユーザーのデータの保護が保証できない状況であれば、極力作業中のデータが失われない対策を実施したうえで、素直にその旨のメッセージ等を表示してアプリケーションは終了するようにしましょう。(Excelの場合、リカバリー用の特別なファイルに保存できるデータをいったん保存して、Excel自体は終了させてしまうようです。なにしろ、アプリケーションを継続したらユーザーのデータを壊してしまうかもしれないのですから。)

次に重視すべきことは、問題を解決するための情報をどこかに残して、問題解決を担当する人(開発したプログラマーやサポートを行っている担当者)にそれが伝わるようにすることです。このためにユーザー向けのメッセージにエラー情報を含むことも多いです。何しろ問題が発生したことに一番最初に気が付くのはユーザー自身であることが多く、かつ、そのせいで困るのもユーザー自身であることが多いので、ユーザーから開発者やサポート会社に連絡があるからです。そのときに、ユーザーと「画面に表示されているエラーメッセージを教えてください。」などという会話ができればスムーズです。「Cドライブの○○フォルダーに保存されているログファイルを送ってください。」という会話だと、コンピューターに慣れていないユーザーは対応できないか対応するのにストレスを感じする場合があります。それでも、どこにも情報がないのに比べれば合格の部類です。

ユーザーのデータを保護したり、問題解決のための情報を表示/記録したりといった処理は、想定外の例外が発生した場合には、その例外が何であれ、たいてい必要になりますから、未処理の例外処理 に記述すべきです。用もないのに Try ~ Catch すると、未処理の例外処理が実行されなくなってしまい邪魔なのです。

 

例外情報を捨てる(プログラマーがいる)

例外をCatchすることで、有用なデバッグ情報を捨ててしまうプログラマーがたくさんいます。.NETが生成する例外クラスには例外のメッセージや、発生した場所、スタックトレースなどさまざまな情報が含まれており、問題を解決するのに役立つので、ログに記録したりメッセージに表示したりする必要があります。未処理の例外処理はこのような処理に最適です。Catchでの例外処理でもできなくはありませんが、Catchに例外処理を記述するときにこのことを考慮しないプログラマーがたくさんいるのです。Catch せずに未処理の例外処理を使うようにすれば、一括して面倒を見ることができるのでその方が良いです。

 

コストが高い

例外をCatchするコストが高い。例外のCatchは通常のプログラムに比べて実行速度が遅くマシンに負荷がかかります。1つや2つ程度では負荷を実感できないかもしれませんが、Webアプリケーションとして公開して大人数に使ってもらうなどする負荷が積もって目に見えるボトルネック(性能が悪い原因)になるかもしれません。

 

例外的なことではないのCatchする

Try ~ Catch で捕まえられる例外は 通常のフローの実行を停止してしまうような何か「例外的なできごと」を表しています。If文でチェックすれば済むようなことや、戻り値で表現できるようなことは例外的なできごとではありません。つまり、Try ~ Catch はそういうものの代わりに使うものとして用意されている機能ではないのです。そのような使い方をすると保守性が低下し、後で苦労するようなことが起こりがちです。

 

以上を理解したうえで、それでも、ここでは Try ~ Catch が必要だ と思える場所があるなら、そこで Try ~ Catch を使用することはもちろんOKです。でも、そのような場所はそれほど多くないはずです。

いくつか具体例を紹介します。

 

ダメな例1:実行すべき例外処理がない - 大きな Try ~ Catch

プログラムを大きく囲む Try ~ Catch は使用しないでください。大きな範囲で例外をCatchする場合、そのCatchでの例外処理はいろいろな事態にあてはまるようなものになります。いろいろな処理にあてはまるような汎用の例外処理はTry ~ Catch ではなく、未処理の例外処理を使用します。

次のダメな例では、大きな Try ~ Catch を使ってどんな例外が発生してもログ書き込みなど何らかの例外処理を実行します。

 

ダメな例

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

'これはダメな例です。
Try
    Dim states(5) As String
    states(0) = "カリフォルニア"
    states(1) = "テキサス"
    states(2) = "ニューヨーク"
    states(3) = "ニューヨーク"
    states(4) = "カリフォルニア"
    states(5) = "ミシシッピ

    Dim index As Integer
    index = Array.IndexOf(states, "ニューヨーク") '2を返します。

    Dim lastIndex As Integer
    lastIndex = Array.LastIndexOf(states, "カリフォルニア") '4を返します。

    Dim noneIndex As Integer
    noneIndex = Array.LastIndexOf(states, "兵庫") '-1を返します。
Catch ex As Exception
    '何か例外発生時の処理。(よくあるのはログに例外を記録する。エラーメッセージを表示するなど)
End Try

■プログラム:これは悪い例です。(何百行もあるプログラムを大きな Try ~ Catch で囲むプログラマーもいます。もちろん悪い例です。)

 

良い例

上記の悪い例を改善するには、Try ~ Catch ではなく、未処理の例外処理を使います。

たとえば、Windowsフォームアプリケーションの場合、次のようになります。

Windowsフォームアプリケーションの未処理の例外処理の実装方法は前回説明しています。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim states(5) As String
states(0) = "カリフォルニア"
states(1) = "テキサス"
states(2) = "ニューヨーク"
states(3) = "ニューヨーク"
states(4) = "カリフォルニア"
states(5) = "ミシシッピ

Dim index As Integer
index = Array.IndexOf(states, "ニューヨーク") '2を返します。

Dim lastIndex As Integer
lastIndex = Array.LastIndexOf(states, "カリフォルニア") '4を返します。

Dim noneIndex As Integer
noneIndex = Array.LastIndexOf(states, "兵庫") '-1を返します。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Imports Microsoft.VisualBasic.ApplicationServices

Namespace My
    ' 次のイベントは MyApplication に対して利用できます:
    ' Startup:アプリケーションが開始されたとき、スタートアップ フォームが作成される前に発生します。
    ' Shutdown:アプリケーション フォームがすべて閉じられた後に発生します。このイベントは、アプリケーションが異常終了したときには発生しません。
    ' UnhandledException:ハンドルされない例外がアプリケーションで発生したときに発生します。
    ' StartupNextInstance:単一インスタンス アプリケーションが起動され、それが既にアクティブであるときに発生します。
    ' NetworkAvailabilityChanged:ネットワーク接続が接続されたとき、または切断されたときに発生します。
    Partial Friend Class MyApplication
        Private Sub MyApplication_UnhandledException(sender As Object, e As UnhandledExceptionEventArgs) Handles Me.UnhandledException
            '何か例外発生時の処理。(よくあるのはログに例外を記録する。エラーメッセージを表示するなど)
        End Sub
    End Class
End Namespace

 

ダメな例2:例外情報を捨てる - 例外を握りつぶす

次の例では例外クラスの持っている情報がどこにも残りません。後で問題を調査して解決するのにとても苦労することになります。

ダメな例は、Catchの例外処理内で例外情報をどこにも記録しません。

良い例は、未処理の例外処理を使って例外情報を使ってログ出力などします。

 

ダメな例

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Try
    何かの処理
Catch
ex As Exception
    WriteLog("問題が発生したのでシステムを終了します。")
End Try

 

 

ダメな例3:例外情報を捨てる - 例外を投げ返す(つもりで情報を失う)

例外が発生したときに、例外をThrowしないでください。ほとんどの場合意味がありません。下手につくると例外情報が失われ問題解決を困難にします。

ダメな例は、例外処理の中で、例外をThrowします。

良い例は、未処理の例外処理を使います。(良い例は後述の よくある例外処理の ログ出力 を参照してください。)

 

ダメな例

例外処理として新しい例外を生成しています。このプログラムだともとの例外(変数ex)の情報がなくなってしまうので、原因追求が困難になります。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Try
    何かの処理
Catch
ex As Exception
    Throw New ApplicationException("xxxx処理で例外発生。緊急連絡先 xxxxx@xxx.com ")
End Try

次のように書いてもダメです。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Try
    何かの処理
Catch
ex As Exception
    Throw ex
End Try

この場合、オリジナルの例外をそのまま投げているように見えますが、スタックトレースの情報が更新されオリジナルのスタックトレースが失われます。単純に「Throw」とだけ書くことで、スタックトレースを残したままオリジナルの例外を投げることも可能ですが、それならば、Catchせずに既定の処理などに任せたほうがシンプルです。

 

良い例

Try~Catch~End Tryは不要です。未処理の例外処理を使用しましょう。

 

メモ メモ  -  例外情報を温存して、例外をThrowする

どうしても例外処理内で例外をThrowしたい場合、オリジナルの例外情報が失われないようにする方法が2つあります。1つは単純に Throw だけ記述する方法です。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Try
    何かの処理
Catch
ex As Exception
    Throw
End Try

この方法だと、オリジナルの例外が情報もそのままで最Throwされます。

 

もう1つは、新しい例外のInnerExceptionに元の例外情報を残すほう方法です。どうしてもThrowが必要なら私はこちらの方がお奨めです。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Try
    何かの処理
Catch
ex As Exception
    Throw New ApplicationException("xxxx処理で例外発生。緊急連絡先 xxxxx@xxx.com ", ex)
End Try

 

この2つは、オリジナルの例外情報は失われないので、それほど悪いプログラムではありません。ただ、特に目的がないのならば何もせず既定の例外処理や未処理の例外処理に任せたほうがシンプルです。

 

 

 

ダメな例4:例外的なことではないのにCatchする - 事前チェックをしない

事前にIfなどでチェックすれば済む場合、Try ~ Catch ではなく、事前チェックを使用しましょう。

 

次の例では、変数 value が Nothing の場合、NullReferenceExceptionが発生します。

ダメな例は、このために Try ~ Catch を使用するプログラムです。

良い例は、このために、変数 value が Nothing であるか事前にチェックするプログラムです。

ダメな例

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

''' <summary>
''' 文字列内から最後の1文字を削ります。
''' 使用例
''' Chop("ABCあいう") は "ABCあい" を返します。
''' </summary>
Private Function Chop(value As String) As String

    Dim length As Integer

    'これはダメな例です。
    Try
        '文字列の長さ - 1 を取得します。
        '★ value の Length プロパティを呼び出しているので、value が Nothin のとき NullReferenceExceptionが発生します。
        length = value.Length - 1
    Catch ex As NullReferenceException
       
Return ""
   
End Try


    '先頭から length 分の文字数分を取得します。
    Dim result As String = value.Substring(0, length)

    Return result

End Function

 

良い例

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

''' <summary>
''' 文字列内から最後の1文字を削ります。
''' 使用例
''' Chop("ABCあいう") は "ABCあい" を返します。
''' </summary>
Private Function Chop(value As String) As String

    If value Is Nothing Then
        Return ""
    End If


    Dim length As Integer

    '文字列の長さ - 1 を取得します。
    '★ value の Length プロパティを呼び出しているので、value が Nothin のとき NullReferenceExceptionが発生します。
    length = value.Length - 1

    '先頭から length 分の文字数分を取得します。
    Dim result As String = value.Substring(0, length)

    Return result

End Function

 

何でも完璧に事前チェックしようとすると、かなりの手間が発生するので、効果の高いチェックを優先的に実装するのが現実的です。

 

4.よくある例外処理

4-1.ログ出力

よくある例外処理プログラムの具体例を紹介しておきます。

ログ出力は最もよくある例外処理です。通常は未処理の例外処理にこれを記述することが多いですが、特別な処理が必要な場合 Catch 内でこれを実行することもあります。そのため、ログ出力処理はクラスやメソッドとして独立させて、未処理の例外処理やCatch内ではこれを呼び出すだけというようにすると良いでしょう。

例外情報を確実にログに記録するには下記のことを考慮する必要があり、なかなか難しいです。

こういった面倒な問題に時間をとられなくて済むように、業務用のアプリケーションではlog4net や NLog などのさまざまな既に完成されたログ書き込みのライブラリを使うことが多いようです。Windowsの場合、テキストファイルではなく、Windowsのイベントログを使用することこういった問題を解決する方法もあります。興味がある方のために参考のリンクを紹介しておきます。

参考

Log4net: https://www.nuget.org/packages/log4net/

NLog: https://www.nuget.org/packages/NLog/

 

ここでは、こういった面倒なことはひとまずおいて、単純に例外情報をテキストファイルに記録する方法を紹介します。

Windowsフォームアプリケーションのようにローカルで使用するようなアプリケーションであれば、このような単純な方法で十分な場合もあります。ログが巨大化して空き容量がなくなる可能性もゼロではありませんが、よほどのことがなければ心配するほどにはならないでしょう。(それでも心配なら上記のようなlog4netなどを検討することになります。)

例外クラス自身は ToString メソッドを使うことで例外メッセージやInnerExceptionやスタックトレースを文字列化してくれますので、例外情報の取得はこれだけでだいたいOKです。

次のようなクラスにしてみました。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Public Class ExceptionLogger

    ''' <summary>
    ''' 例外情報を プログラム(exeなど)のあるフォルダーに errorlog.txt という名前で出力します。
    ''' logFileName への出力に失敗した場合、代わりに tempフォルダーの myprogramerrorlog.txt に出力します。
    ''' tempフォルダーはたとえば次のようなパスです。
    ''' C:\Users\ユーザー名\AppData\Local\Temp
    ''' </summary>
    Public Sub WriteLog(exception As Exception)

        Try
            'このプログラムのあるフォルダーのパスを取得。末尾は \ で終わります。
            Dim appPath As String = AppDomain.CurrentDomain.BaseDirectory

            '書き込むべきログファイルのフルパスを生成
            Dim logFileName As String = appPath & "errorlog.txt"

            Me.WriteLog(logFileName, exception)

        Catch ex As Exception
            'ログファイルの情報を残すことに失敗した場合、tempフォルダーへの保存を試みる。
            'Windowsではtempフォルダーは必ず存在しており、書き込みが成功する可能性が最も高い。
            'この非常の場合のログのファイル名は、アプリケーションを識別できる固有の名前にするのが良い。
            '(そのような名前を取得するプログラムを書くこともできるが、ここは例外情報を保存する最後のチャンスなので
            '成功率が高くなるように固定のファイル名を指定しておく)
            Dim tempLogFileName As String = System.IO.Path.GetTempPath & "myprogramerrorlog.txt"
            Me.WriteLog(tempLogFileName, ex) 'ログファイルの書き込みに失敗した例外情報の書き込み
            Me.WriteLog(tempLogFileName, exception) 'そもそもログファイルに書き込もうとしていた例外情報の書き込み
        End Try

    End Sub

    ''' <summary>
    ''' 例外情報を logFileName に出力します。
    ''' </summary>
    Public Sub WriteLog(logFileName As String, exception As Exception)

        Dim message As String

        '現在の日時を西暦で書き込みます。書式指定子 K により、UTCとの差を +9:00 のように出力します。
        message = Now.ToString("yyyy\/MM\/dd HH\:mm\:ddK,", System.Globalization.CultureInfo.InvariantCulture)

        '例外情報を文字列化
        message &= exception.ToString

        '最後に改行
        message &= Environment.NewLine

        '書き込み実行
        System.IO.File.AppendAllText(logFileName, message)

    End Sub

End Class

このプログラムではファイル名を指定せず WriteLog メソッドを呼び出すと、アプリケーション(exeなど)のあるフォルダーに errorlog.txt という名前のファイルで例外情報を出力します。何かの理由でこの処理に失敗した場合は、代わりにWindowsのtempフォルダーに myprogramerrorlog.txt という名前で例外情報を出力します。

いくつか特徴的な処理を3点ピックアップして説明します。

AppDomain.CurrentDomain.BaseDirectory

exeファイルなどのプログラム本体があるフォルダーを取得するために、 AppDomain.CurrentDomain.BaseDirectory を使用しています。これを使うとプログラムのあるフォルダーを取得できると思っておいて良いのですが、これの意味や理屈は少し複雑なのでこれ以上は深入りしないことにします。

Path.GetTempPath

Path.GetTempPathメソッドはOSの 一時フォルダーパスを取得します。

一時フォルダーとは、その名の通り、アプリケーションが一時的にファイルを作成する場合に使用してよいことになっているフォルダーで、たいていの場合、Windowsでは、C:\Users\ユーザー名\AppData\Local\Temp フォルダー、Linuxでは、/tmp/ユーザー名 というフォルダーです。(Windowsで良く目にする C:\temp というフォルダーではありません。)

これらの一時フォルダーは、OSが機能として用意しているフォルダーで、必ず存在し、ほとんどの場合書き込み可能なので一時的なファイルを作成する場所としてしばしば使われます。Linuxの/tmp/ユーザー名は再起動などのタイミングでファイルが削除されますが、Windowsの場合は、明示的に削除しない限りは残るようです。

書式指定子 K と InvariantCulture

ログには必ず日時を出力するものです。なぜなら、例外の多くは、1つのアプリケーションの中だけに原因があるのではなく、外部のアプリケーションや、外部のサーバーやネットワークなどに関係するからです。だからログに記録された日時を元に、そのときWindowsで何か異常は発生していなかったか、使用しているデータベースに問題は発生していなかったかなど周辺を調査していくことがよくわります。このとき、例外が発生した時間(できれば秒単位)がわからないと、調査が必要な範囲が広がってなかなか大変です。

ところが、日本だと西暦2020年と令和2年は同じ年なので、単純に年を出力しても 2020 の場合 と 2 の場合があります。どちらになるかはWindowsのコントロールパネルの地域の設定で変更できます。さらに、日本以外の国も考慮に入れると、時差を考慮しないとエラーが発生した正確な時間がわかりません。そのため、このプログラムでは InvariantCulture を指定して、必ず西暦で出力するようにしています。そして、書式指定子 K を使うことで、協定世界時(昔の言葉で言うグリニッジ標準時)との時差も出力します。日本は国際標準時より9時間の時差があるため、日本で実行すると 2020/10/11 20:16:11+09:00 のようになります。

 

 

さて、 これをたとえば、Windowsフォームアプリケーションの未処理の例外処理から呼び出すには次のようにします。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Imports Microsoft.VisualBasic.ApplicationServices

Namespace My
    ' 次のイベントは MyApplication に対して利用できます:
    ' Startup:アプリケーションが開始されたとき、スタートアップ フォームが作成される前に発生します。
    ' Shutdown:アプリケーション フォームがすべて閉じられた後に発生します。このイベントは、アプリケーションが異常終了したときには発生しません。
    ' UnhandledException:ハンドルされない例外がアプリケーションで発生したときに発生します。
    ' StartupNextInstance:単一インスタンス アプリケーションが起動され、それが既にアクティブであるときに発生します。
    ' NetworkAvailabilityChanged:ネットワーク接続が接続されたとき、または切断されたときに発生します。
    Partial Friend Class MyApplication
        Private Sub MyApplication_UnhandledException(sender As Object, e As UnhandledExceptionEventArgs) Handles Me.UnhandledException
            Dim logger As New ExceptionLogger
            logger.WriteLog(e.Exception)
        End Sub
    End Class
End Namespace

Try ~ Catch の Catch から呼び出すには次のようにします。ただし、特別な処理がないのであれば未処理の例外処理から呼び出すことをお奨めします。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Try
    '通常のフロー
Catch ex As IO.IOException
    '何か特別な例外処理
    Dim logger As New ExceptionLogger
    logger.WriteLog(ex)
End Try

 

次のプログラムの TestErrorLog メソッドを使うと実際に試すことができます。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub TestErrorLog()
    Try
        RaiseException()
    Catch ex As IO.IOException
        Dim logger As New ExceptionLogger
        logger.WriteLog(ex)
    End Try
End Sub

Private Sub RaiseException()

    Dim ex As New IO.IOException("例外メッセージ1", New InvalidOperationException("例外メッセージ", New NotImplementedException("例外メッセージ3")))
    Throw ex

End Sub

実行すると次のようなログが出力されます。

2020/10/11 20:16:11+09:00,System.IO.IOException: 例外メッセージ1 ---> System.InvalidOperationException: 例外メッセージ ---> System.NotImplementedException: 例外メッセージ3
--- 内部例外スタック トレースの終わり ---
--- 内部例外スタック トレースの終わり ---
場所 WindowsApp1.Form1.RaiseException() 場所 C:\Users\rucio\Source\Repos\WindowsApp1\WindowsApp1\Form1.vb:行 23
場所 WindowsApp1.Form1.TestErrorLog() 場所 C:\Users\rucio\Source\Repos\WindowsApp1\WindowsApp1\Form1.vb:行 9

 

4-2.メッセージ表示

4-2-1.メッセージを表示する方法

メッセージの表示方法はアプリケーションの種類やデザインによって異なります。この初級講座では今のところアプリケーションの種類としてはWindowsフォームアプリケーションしか説明しておらず、この場合は簡単にメッセージを表示するにはMsgBoxが使用できます。他の種類のアプリケーションではまったく事情が異なります。たとえば、WebアプリケーションではMsgBoxの使用は厳禁です。

発展 発展学習  -  どうして Webアプリケーションでは MsgBox の使用が厳禁なのか

発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。

Webアプリケーションはユーザーのブラウザーに画面が表示されますが、Webアプリケーションが動作しているのは遠く離れたサーバーです。たとえば、Google検索を利用するときに、https://www.google.com/ にアクセスすることがありますが、このhttps://www.google.com/のURLを使って接続されるコンピューターが検索アプリケーションを実行しているコンピューターです。

MsgBox は アプリケーションを実行している場所にメッセージを表示するので、WebアプリケーションでMsgBoxを使用すると、ユーザーが使っているブラウザー上は何も表示されず、遠くはなれたサーバー上にメッセージが表示されるか、サーバーが画面を表示できない構成になっていればなんらかのエラーがサーバー上に記録されます。

MsgBoxは一度表示されると OK をクリックするまで、処理が先に進まないので、遠く離れた直接操作できないサーバー上にもしMsgBoxが表示されるようなことがあればいろいろ困ることになるでしょう。

アプリケーションの種類ごとのよくあるエラーメッセージの表示方法は次の通りです。デザインにこだわりがあればここに挙げていない方法でメッセージを表示してもまったく問題ありません。たとえば、ゲームのような高度なグラフィックを使ってメッセージを表示することもありえます。

Windowsフォームアプリケーション: MsgBox, MessageBox.Show, エラー表示用のコントロール(Labelなど)を配置する、ErrorProviderコンポーネント など

WPFアプリ: MsgBox, MessageBox.Show, エラー表示用のコントロールを配置する,

コンソールアプリケーション: Console.Error.WriteLine, Console.WriteLine

ASP.NET Webフォーム(※1):ValidationSummary, エラー表示用のコントロールを配置する, エラー表示専用のページ

※1:ASP.NET Webフォームは、2019年4月が最後のリリースとなった技術であり、今後は新規アプリケーションでは使用されない見込みです。

 

4-2-2.メッセージの内容

ユーザーに通知するメッセージは、エラーが発生したことの通知、ユーザーに向けた問題解決のヒント、開発者・サポート担当者に向けた問題解決のヒントの3つから構成されることが一般的です。

エラーメッセージの例

この画像はWindowsフォームアプリケーションやWPFアプリでMsgBoxやMessageBox.Showを使ってメッセージを表示した例です。このほかにユーザー名やコンピューター名など問題解決に役立つ可能性がある定型的な内容をを表示する場合もあります。

 

メモ メモ  -  メッセージボックスにアイコンを表示する

MsgBoxまたはMessageBoxで追加の引数を指定するとアイコンを表示できます。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Try
    何かの処理
Catch
ex As Exception
    Throw
End Try

この方法だと、オリジナルの例外が情報もそのままで最Throwされます。

 

もう1つは、新しい例外のInnerExceptionに元の例外情報を残すほう方法です。どうしてもThrowが必要なら私はこちらの方がお奨めです。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Try
    何かの処理
Catch
ex As Exception
    Throw New ApplicationException("xxxx処理で例外発生。緊急連絡先 xxxxx@xxx.com ", ex)
End Try

 

この2つは、オリジナルの例外情報は失われないので、それほど悪いプログラムではありません。ただ、特に目的がないのならば何もせず既定の例外処理や未処理の例外処理に任せたほうがシンプルです。

 

 この画像のメッセージは具体的な問題点(通信エラー)や、解決方法の提案が記載されていることから、あらかじめ想定されたメッセージであることがわかります。つまり、このレベルの具体的なメッセージは Catch から表示します。何が起こるかあらかじめ想定していない場合は、このような具体的なメッセージは表示できませんので、エラーが発生したのでアプリケーションを終了する、ログはここにあるというレベルの情報だけを表示することになります。その場合は、未処理の例外処理を使ってメッセージを表示します。

 

4-3.アプリケーションの終了

Try している場合は、アプリケーションを明示的に終了させないと、End Tryから通常のフローの復帰します。

エラーを回復できず通常のフローに復帰できない場合は、アプリケーションを明示的に終了させることになります。

※未処理の例外処理が実行される場合、特に何もしなくてもアプリケーションは終了します。

アプリケーションを終了させる方法もアプリケーションの種類により異なります。Windowsフォームアプリケーションの場合、既定ではフォームを閉じるとアプリケーションが終了します。

※初級講座では説明していませんが、複数のフォームを使うアプリケーションの場合、既定では最初のフォームを閉じるとアプリケーションが終了します。

フォームを閉じるにはフォーム内で Me.Close のように記述します。

次の例をWindowsフォームアプリケーションのフォーム内に記述するとCatch時にフォームが閉じます。これが唯一のフォームまたは最初のフォームの場合アプリケーションは終了します。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Try
    '通常のフロー
Catch ex As IO.IOException
    '何か特別な例外処理
    'フォームを閉じる

    Me.Close
End Try

 

初級講座ではまだ説明していない他の種類のアプリケーションではおおむね次のような方法でアプリケーションを終了させます。

WPFアプリ: Me.Close (既定では最後のウィンドウを閉じるとアプリケーションが終了します。)

コンソールアプリケーション: Environment.Exit(0),Mainメソッド内で Return 。

ASP.NETのWebアプリケーションのライフサイクル(=アプリケーションが開始してから終了するまでの流れ)はWindowsフォームアプリケーションとは大きく違うため、ここでは簡単にまとめることができません。

 

 

4-4.例外処理での例外

以上で説明したように例外処理としていろいろなプログラムを実行することになりますが、 例外処理中に新しい例外が発生することを想定すると Catch の中にまたTry ~ Catch を書くなどキリがありません。

重要な例外処理では例外処理内で Try ~ Catch することもありますが、プログラムが複雑になるので、例外処理として実行するプログラムでは極力例外が発生しないように他のプログラムよりも慎重に実装・テストをしてください。

まだ、例外処理内でいくつかの処理を実行する必要がある場合、リスクが低い順に実行すると良いでしょう。

 

5.まとめ

5-1.例外処理の考え方

 

5-2.例外処理の使い分け

 

私からのアドバイス:Try ~ Catch は原則として使用しないこと

その理由

 

メモ メモ  -  「Try ~ Catch は使わないでください。」と言ったこともあります。

私は仕事でいろいろな人が書いた業務用プログラムを見たり、調査したり、レビューしたりします。

VBでかかれた業務用プログラムで Try ~ Catch を見かけると ほとんど(感覚的には99%) はない方が良い Try ~ Catch でした。

はじめのうち、私は Try ~ Catch はこういう場合に使ったほうが良いというような使い方の説明をすることが多かったのですが、なかなか伝わらなかったので逆の言い方をしてみました。「Try ~ Catch は使わないでください。」。この言い方の方が伝わるみたいです。もちろん Try ~ Catch が 100% ダメという意味ではなく、言い方の違いだけなのですが、多くのプログラマーはどうも何かエラー処理(例外処理)を書かなくてはいけないという強迫観念のようなものがあるようで、まずはその考え方に対してノーだということを伝えるという趣旨です。

以上は、業務用プログラムの場合の話です。業務用プログラムは既にあるフレームワークやライブラリを活用して、何か仕事や日常の作業に役に立つ機能を実現するものです。

一方、フレームワークやライブラリのようなものを開発しているプログラマーの場合は業務用プログラムよりも Try ~ Catch を使う頻度は多くなると思います。このようなものはプログラムから呼び出されるプログラムであるので、何か問題が発生した場合に、呼び出し側に問題を伝える手段として例外を使いこなすことが求められます。とは言え、あっちこっちにTry ~ Catch があるのはやはりおかしいです。.NET Frameworkのソースコードを見ることもできるので、フレームワークの中にどのくらい Try ~ Catch があるか眺めてみてください。

たとえば、Windowsフォームアプリケーションの Form のプログラムは下記で見られます。

https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Form.cs

このプログラムは C# で作られていますが、VBのTry ~ Catch は C# でも try ~ catch なので見つけることができます。7973行のプログラムの中で、19個使われていました。平均すると 419行に1個です。もちろん、プログラムの性質によりこの数は全然変わると思いますのであくまで参考です。業務用プログラムの場合は、もっと少なくなるはずで、私は0個もありえると思っています。

 

メモ メモ  -  古いエラー処理 On Error

VBで「例外」という考え方が登場したのは 2002年のVB.NET(2002)からで、Try ~ Catch や未処理の例外処理などはこれ以降に導入されました。それ以前は On Error(オンエラー)を使ったエラー処理の構文が使われていました。今でも On Error を使うこともできますが、これは2002年より前のプログラムとの互換性を重視する場合だけ使用すべきもので、つまり、2020年現在、これを使うことはまずありません。

発想は Try ~ Catch にしていますが、指定した例外を捕まえたり、条件を付けるということはできません。何か例外(エラー)が発生した場合に、決められたエラー処理にジャンプするだけです。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    On Error GoTo ErrorHandle

    'わざと数値への変換エラーを発生させます。
    Dim i As Integer = CInt("ABC")

    Return '←エラー処理の直前で Return しないと、エラーがなくてもエラー処理が実行されます。
ErrorHandle:
    'エラー処理
    'エラー情報は Err オブジェクトから受け取れます。
    Dim exception As Exception = Err.GetException '発生したエラーを表す例外を取得します。
    Dim number As Integer = Err.Number 'エラー番号
    Dim message As String = Err.Description

    MsgBox("エラー発生。" & Environment.NewLine & "エラー番号:" & number & " " & message, vbExclamation)

End Sub

On Error Goto の代わりに On Error Resume Nextを使うと、どのようなエラーが発生しても無視して次の行を実行するという意味になります。これはTry ~ Catch などの構造化例外処理では簡単には真似できない機能なので、もしそのような必要があれば便利なのですが、エラーが発生してもどんどん実行を続けてよいという具体的な状況が思いつかないです。不用意に使用すると途中で発生した例外を無視したせいで取り返しの付かない結果を招くようなこともありますのでよほどのことがない限り使用しないようにご注意ください。

Try と On Error を同じプロシージャ内で混在させることはできません。