Visual Basic 初級講座 [改訂版] |
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
Visual Basic 中学校 > 初級講座[改訂版] >
2020/10/18
この記事が対象とする製品・バージョン
![]() |
Visual Basic 2019 | ◎ | 対象です。 |
![]() |
Visual Basic 2017 | ◎ | 対象です。 |
![]() |
Visual Basic 2015 | ◎ | 対象です。 |
![]() |
Visual Basic 2013 | ◎ | 対象です。 |
![]() |
Visual Basic 2012 | ◎ | 対象です。 |
![]() |
Visual Basic 2010 | ◎ | 対象です。 |
![]() |
Visual Basic 2008 | ◎ | 対象です。 |
![]() |
Visual Basic 2005 | ◎ | 対象です。 |
![]() |
Visual Basic.NET 2003 | △ | 対象外ですがほとんどの説明があてはまるので参考になります。 |
![]() |
Visual Basic.NET (2002) | △ | 対象外ですがほとんどの説明があてはまるので参考になります。 |
![]() |
Visual Basic 6.0 | × | 対象外です。 |
前回説明したように例外処理には次の3種類があります。
このうち、自分で例外処理の内容をプログラムできるのは Catch と 未処理の例外処理の2箇所です。
違いをまとめてみましょう。
Catchでの例外処理 | 未処理の例外処理 | フレームワークの既定の例外処理 | |
---|---|---|---|
ポイント | きめ細かい例外処理 | 包括的な例外処理 | 既定の例外処理 カスタムな例外処理は記述できない。 |
使いどころ | ・機能ごとに固有のきめ細かい例外処理が必要な場合 ・通常のフローに復帰させられることがわかっている場合 |
・ログの記録やメッセージの表示など、どこで処理しても同じような例外処理の場合 ・通常のフローに復帰できるかわからない場合 |
・まったく想定していない例外が発生した場合 |
数 | 何箇所でも記述できる。 | 通常は1箇所 | 1箇所 |
通常のフローへの復帰 | 通常のフローに復帰できる。(復帰しないこともできる。) | 通常のフローには復帰できない。プログラムは終了する。 (Webアプリの場合は、プログラム本体ではなく当該リクエストの処理が終了) |
通常のフローには復帰できない。プログラムは終了する。 (Webアプリの場合は、プログラム本体ではなく当該リクエストの処理が終了) |
処理できる例外の指定 | 対象の例外を指定できる。 (Catch As XXXException) |
対象の例外は指定できない。 | 対象の例外は指定できない。 |
カスタムな発動条件 | 自由に条件を記述できる(When xxxx) | 条件は記述できない。 | 条件は記述できない。 |
この表は少し簡略化しており、下記の点は厳密には正しくありません。が、この点の考慮が必要なことはまずないので、この表の理解で考えていけばよいと思います。
・アプリケーションの種類によっては未処理の例外処理から通常のフローに復帰できる場合もあります。(ただし、End Try の下から復帰するのではなく、呼び出し元から復帰します。)
■の部分にあてはまるものを選択してください。
例外処理をある程度理解して使いこなすようになると、自発的に例外を発生させたくなる場合があります。
あえて、待ち構えている(かもしれない)Catchや未処理の例外処理を実行させたいことがあるからです。
Throw (読み方:Throw=スロー) を使うと自発的に例外を発生させることができます。
たとえば、次の通りです。
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の範囲です。
''' <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 |
このプログラムは問題なく動作するはずです。
呼び出し例は次の通りです。
'身長 1.7メートル、体重 80キログラム の場合の BMI を求めます。 Dim bmi As Double = CalcBMI(1.7, 80) |
ところで、私たちは普段身長をセンチメートル単位で扱うことが多く、身長を聞かれると 165cm ですのように cm で答えるのが普通のように思います。
この感覚だと次のように間違った引数を指定してしまうことがありそうです。
Dim bmi As Double = CalcBMI(165, 70) |
このプログラムは身長165cm、体重70kgの場合のBMIを求めるつもりなのですが、CalcBMIの第1引数である身長はメートル単位を前提としているため、身長 165メートル という意味になってしまいます。この結果BMIは 約 0.003 というありえないほど小さな値になります。
165メートルのようなありえない身長が指定された場合、指示されたとおりに通常のフローを実行してBMIを計算するよりも、何かおかしいことが起こっているという「例外」的な状況を知らせたほうが親切ですよね?
プログラムに即して言うと 引数 height の値が例外的状況を作り出しているということになります。
そこで身長に 3以上の値が指定された場合、例外を発生させるようにしてみましょう。引数がおかしいので使用する例外クラスは ArgumentException です。
※引数の範囲がおかしいと考えてArgumentOutofRangeExceptionを発生させるという考え方もあります。
''' <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 であり、おかしい理由は (おそらく)単位がセンチメートルになっているからであり、メートル単位で指定するべきである という情報を付加します。次のようになります。
''' <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上では次のメッセージが表示されます。
とても親切ですよね。何がおかしくてどうすればいいのかわかります。
おかしい引数は height であるという情報はスクロールしないと見えないところに隠れています。メッセージを1行スクロールすると パラメーター名: 'height と表示されます。
実は.NETの他の例外でもマイクロソフトはかなり親切に解決方法のヒントをちりばめているので、例外が発生したらメッセージをよく読んでください。 ところで、BIMの例では「センチメートル」ではなく「メートル」にしてくださいというようなメッセージになっており、一見わかりやすいですが、もし、「センチメートル」や「メートル」のことをよく知らない人がいたとしたら、わけがわからないメッセージに見えてしまうかもしれません。それはもうVBの問題ではないので、申し訳ないですが、「センチメートル」や「メートル」についてはもしわからなければ自分で調べてくださいということになってしまいます。(メッセージに「もし、センチメートルを指定しているのならその百分の一値を指定してください。」と追記するともっと親切になるでしょうか?センチメートルのことがわからない人がもしいたとして、この追記が親切なのかかえって混乱を招くのか難しいところです。) この考え方を広げてみると、たとえば、「SSLの代わりに TLS 1.2 を使ってください。」のような例外メッセージが表示された場合、わけがわからないと思う人がいることでしょう。でも、もはやVBの問題ではないので「SSL」、「TLS 1.2」などについては自分で調べるべきかもしれません。何を調べればよいかというキーワードを教えてくれているだけで親切な世の中になったのだなと私は感じます。(メッセージに、「TLS 1.2 を使用するには、ServicePointManager.SecurityProtocolプロパティに TLS 1.2を指定してください。」と追記されているともっと親切になるでしょうか?) |
初級講座では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アプリケーションとして公開して大人数に使ってもらうなどする負荷が積もって目に見えるボトルネック(性能が悪い原因)になるかもしれません。
Try ~ Catch で捕まえられる例外は 通常のフローの実行を停止してしまうような何か「例外的なできごと」を表しています。If文でチェックすれば済むようなことや、戻り値で表現できるようなことは例外的なできごとではありません。つまり、Try ~ Catch はそういうものの代わりに使うものとして用意されている機能ではないのです。そのような使い方をすると保守性が低下し、後で苦労するようなことが起こりがちです。
以上を理解したうえで、それでも、ここでは Try ~ Catch が必要だ と思える場所があるなら、そこで Try ~ Catch を使用することはもちろんOKです。でも、そのような場所はそれほど多くないはずです。
いくつか具体例を紹介します。
プログラムを大きく囲む Try ~ Catch は使用しないでください。大きな範囲で例外をCatchする場合、そのCatchでの例外処理はいろいろな事態にあてはまるようなものになります。いろいろな処理にあてはまるような汎用の例外処理はTry ~ Catch ではなく、未処理の例外処理を使用します。
次のダメな例では、大きな Try ~ Catch を使ってどんな例外が発生してもログ書き込みなど何らかの例外処理を実行します。
'これはダメな例です。 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フォームアプリケーションの未処理の例外処理の実装方法は前回説明しています。
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を返します。 |
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 |
次の例では例外クラスの持っている情報がどこにも残りません。後で問題を調査して解決するのにとても苦労することになります。
ダメな例は、Catchの例外処理内で例外情報をどこにも記録しません。
良い例は、未処理の例外処理を使って例外情報を使ってログ出力などします。
Try 何かの処理 Catch ex As Exception WriteLog("問題が発生したのでシステムを終了します。") End Try |
例外が発生したときに、例外をThrowしないでください。ほとんどの場合意味がありません。下手につくると例外情報が失われ問題解決を困難にします。
ダメな例は、例外処理の中で、例外をThrowします。
良い例は、未処理の例外処理を使います。(良い例は後述の よくある例外処理の ログ出力 を参照してください。)
例外処理として新しい例外を生成しています。このプログラムだともとの例外(変数ex)の情報がなくなってしまうので、原因追求が困難になります。
Try 何かの処理 Catch ex As Exception Throw New ApplicationException("xxxx処理で例外発生。緊急連絡先 xxxxx@xxx.com ") End Try |
次のように書いてもダメです。
Try 何かの処理 Catch ex As Exception Throw ex End Try |
この場合、オリジナルの例外をそのまま投げているように見えますが、スタックトレースの情報が更新されオリジナルのスタックトレースが失われます。単純に「Throw」とだけ書くことで、スタックトレースを残したままオリジナルの例外を投げることも可能ですが、それならば、Catchせずに既定の処理などに任せたほうがシンプルです。
Try~Catch~End Tryは不要です。未処理の例外処理を使用しましょう。
どうしても例外処理内で例外をThrowしたい場合、オリジナルの例外情報が失われないようにする方法が2つあります。1つは単純に Throw だけ記述する方法です。
この方法だと、オリジナルの例外が情報もそのままで最Throwされます。
もう1つは、新しい例外のInnerExceptionに元の例外情報を残すほう方法です。どうしてもThrowが必要なら私はこちらの方がお奨めです。
この2つは、オリジナルの例外情報は失われないので、それほど悪いプログラムではありません。ただ、特に目的がないのならば何もせず既定の例外処理や未処理の例外処理に任せたほうがシンプルです。 |
事前にIfなどでチェックすれば済む場合、Try ~ Catch ではなく、事前チェックを使用しましょう。
次の例では、変数 value が Nothing の場合、NullReferenceExceptionが発生します。
ダメな例は、このために Try ~ Catch を使用するプログラムです。
良い例は、このために、変数 value が Nothing であるか事前にチェックするプログラムです。
''' <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 |
''' <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 |
何でも完璧に事前チェックしようとすると、かなりの手間が発生するので、効果の高いチェックを優先的に実装するのが現実的です。
よくある例外処理プログラムの具体例を紹介しておきます。
ログ出力は最もよくある例外処理です。通常は未処理の例外処理にこれを記述することが多いですが、特別な処理が必要な場合 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です。
次のようなクラスにしてみました。
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点ピックアップして説明します。
exeファイルなどのプログラム本体があるフォルダーを取得するために、 AppDomain.CurrentDomain.BaseDirectory を使用しています。これを使うとプログラムのあるフォルダーを取得できると思っておいて良いのですが、これの意味や理屈は少し複雑なのでこれ以上は深入りしないことにします。
Path.GetTempPathメソッドはOSの 一時フォルダーパスを取得します。
一時フォルダーとは、その名の通り、アプリケーションが一時的にファイルを作成する場合に使用してよいことになっているフォルダーで、たいていの場合、Windowsでは、C:\Users\ユーザー名\AppData\Local\Temp フォルダー、Linuxでは、/tmp/ユーザー名 というフォルダーです。(Windowsで良く目にする C:\temp というフォルダーではありません。)
これらの一時フォルダーは、OSが機能として用意しているフォルダーで、必ず存在し、ほとんどの場合書き込み可能なので一時的なファイルを作成する場所としてしばしば使われます。Linuxの/tmp/ユーザー名は再起動などのタイミングでファイルが削除されますが、Windowsの場合は、明示的に削除しない限りは残るようです。
ログには必ず日時を出力するものです。なぜなら、例外の多くは、1つのアプリケーションの中だけに原因があるのではなく、外部のアプリケーションや、外部のサーバーやネットワークなどに関係するからです。だからログに記録された日時を元に、そのときWindowsで何か異常は発生していなかったか、使用しているデータベースに問題は発生していなかったかなど周辺を調査していくことがよくわります。このとき、例外が発生した時間(できれば秒単位)がわからないと、調査が必要な範囲が広がってなかなか大変です。
ところが、日本だと西暦2020年と令和2年は同じ年なので、単純に年を出力しても 2020 の場合 と 2 の場合があります。どちらになるかはWindowsのコントロールパネルの地域の設定で変更できます。さらに、日本以外の国も考慮に入れると、時差を考慮しないとエラーが発生した正確な時間がわかりません。そのため、このプログラムでは InvariantCulture を指定して、必ず西暦で出力するようにしています。そして、書式指定子 K を使うことで、協定世界時(昔の言葉で言うグリニッジ標準時)との時差も出力します。日本は国際標準時より9時間の時差があるため、日本で実行すると 2020/10/11 20:16:11+09:00 のようになります。
さて、 これをたとえば、Windowsフォームアプリケーションの未処理の例外処理から呼び出すには次のようにします。
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 から呼び出すには次のようにします。ただし、特別な処理がないのであれば未処理の例外処理から呼び出すことをお奨めします。
Try '通常のフロー Catch ex As IO.IOException '何か特別な例外処理 Dim logger As New ExceptionLogger logger.WriteLog(ex) End Try |
次のプログラムの TestErrorLog メソッドを使うと実際に試すことができます。
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 |
メッセージの表示方法はアプリケーションの種類やデザインによって異なります。この初級講座では今のところアプリケーションの種類としてはWindowsフォームアプリケーションしか説明しておらず、この場合は簡単にメッセージを表示するには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月が最後のリリースとなった技術であり、今後は新規アプリケーションでは使用されない見込みです。
ユーザーに通知するメッセージは、エラーが発生したことの通知、ユーザーに向けた問題解決のヒント、開発者・サポート担当者に向けた問題解決のヒントの3つから構成されることが一般的です。
この画像はWindowsフォームアプリケーションやWPFアプリでMsgBoxやMessageBox.Showを使ってメッセージを表示した例です。このほかにユーザー名やコンピューター名など問題解決に役立つ可能性がある定型的な内容をを表示する場合もあります。
MsgBoxまたはMessageBoxで追加の引数を指定するとアイコンを表示できます。
この方法だと、オリジナルの例外が情報もそのままで最Throwされます。
もう1つは、新しい例外のInnerExceptionに元の例外情報を残すほう方法です。どうしてもThrowが必要なら私はこちらの方がお奨めです。
この2つは、オリジナルの例外情報は失われないので、それほど悪いプログラムではありません。ただ、特に目的がないのならば何もせず既定の例外処理や未処理の例外処理に任せたほうがシンプルです。 |
この画像のメッセージは具体的な問題点(通信エラー)や、解決方法の提案が記載されていることから、あらかじめ想定されたメッセージであることがわかります。つまり、このレベルの具体的なメッセージは Catch から表示します。何が起こるかあらかじめ想定していない場合は、このような具体的なメッセージは表示できませんので、エラーが発生したのでアプリケーションを終了する、ログはここにあるというレベルの情報だけを表示することになります。その場合は、未処理の例外処理を使ってメッセージを表示します。
Try している場合は、アプリケーションを明示的に終了させないと、End Tryから通常のフローの復帰します。
エラーを回復できず通常のフローに復帰できない場合は、アプリケーションを明示的に終了させることになります。
※未処理の例外処理が実行される場合、特に何もしなくてもアプリケーションは終了します。
アプリケーションを終了させる方法もアプリケーションの種類により異なります。Windowsフォームアプリケーションの場合、既定ではフォームを閉じるとアプリケーションが終了します。
※初級講座では説明していませんが、複数のフォームを使うアプリケーションの場合、既定では最初のフォームを閉じるとアプリケーションが終了します。
フォームを閉じるにはフォーム内で Me.Close のように記述します。
次の例をWindowsフォームアプリケーションのフォーム内に記述するとCatch時にフォームが閉じます。これが唯一のフォームまたは最初のフォームの場合アプリケーションは終了します。
Try '通常のフロー Catch ex As IO.IOException '何か特別な例外処理 'フォームを閉じる Me.Close End Try |
初級講座ではまだ説明していない他の種類のアプリケーションではおおむね次のような方法でアプリケーションを終了させます。
WPFアプリ: Me.Close (既定では最後のウィンドウを閉じるとアプリケーションが終了します。)
コンソールアプリケーション: Environment.Exit(0),Mainメソッド内で Return 。
ASP.NETのWebアプリケーションのライフサイクル(=アプリケーションが開始してから終了するまでの流れ)はWindowsフォームアプリケーションとは大きく違うため、ここでは簡単にまとめることができません。
以上で説明したように例外処理としていろいろなプログラムを実行することになりますが、 例外処理中に新しい例外が発生することを想定すると Catch の中にまたTry ~ Catch を書くなどキリがありません。
重要な例外処理では例外処理内で 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個もありえると思っています。 |
VBで「例外」という考え方が登場したのは 2002年のVB.NET(2002)からで、Try ~ Catch や未処理の例外処理などはこれ以降に導入されました。それ以前は On Error(オンエラー)を使ったエラー処理の構文が使われていました。今でも On Error を使うこともできますが、これは2002年より前のプログラムとの互換性を重視する場合だけ使用すべきもので、つまり、2020年現在、これを使うことはまずありません。 発想は Try ~ Catch にしていますが、指定した例外を捕まえたり、条件を付けるということはできません。何か例外(エラー)が発生した場合に、決められたエラー処理にジャンプするだけです。
On Error Goto の代わりに On Error Resume Nextを使うと、どのようなエラーが発生しても無視して次の行を実行するという意味になります。これはTry ~ Catch などの構造化例外処理では簡単には真似できない機能なので、もしそのような必要があれば便利なのですが、エラーが発生してもどんどん実行を続けてよいという具体的な状況が思いつかないです。不用意に使用すると途中で発生した例外を無視したせいで取り返しの付かない結果を招くようなこともありますのでよほどのことがない限り使用しないようにご注意ください。 Try と On Error を同じプロシージャ内で混在させることはできません。 |