Visual Basic 初級講座 [改訂版] |
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
Visual Basic 中学校 > 初級講座[改訂版] >
2020/9/20
この記事が対象とする製品・バージョン
![]() |
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 | × | 対象外です。 |
プログラムの世界で「例外」(れいがい)とは、だいたい「エラー」と同じような意味です。この2つを区別しないで使うプログラマーもたくさんいますが、「例外」と「エラー」は厳密には異なるものです。
プログラマーは順番に処理が実行されていくようにプログラムします。まず、処理Aを実行し、次に処理Bを実行し・・・という具体です。If で条件分岐したり、For や Do で繰り返し実行したり、このプログラムの流れ(フロー)をいろいろ制御するプログラムをすることも良くあります。
これが「通常」です。
例外とは、この通常の流れ(フロー)を中断する、例外的なできごと を指しています。例外が発生すると、フローは中断されます。次の命令や繰り返し処理は実行されません。例外が発生したときのための特別な仕掛けがない場合、プログラムはこれで終了します。
たとえば、URLを指定して、最終更新日を取得するプログラムを題材に考えて見ましょう。
この例ではWebFileというクラスを作り、その中にWebページの最終更新日を取得する GetLastModifiedDateメソッドを作成しています。
このプログラムが使用している System.Net.Http.HttpClientクラスが存在しないというエラーになる場合、NuGetで System.Net.Http をインストールしてください。.NET Frameworkの場合は、NuGetの代わりに参照設定でSystem.Net.Httpを追加します。
Public Class
WebFile ''' <summary> ''' Webページの最終更新日を取得します。 ''' </summary> Public Shared Function GetLastModifiedDate(url As String) As Date 'セキュリティのあるプロトコル(TLS 1.2)を使用します。 Net.ServicePointManager.SecurityProtocol = Net.SecurityProtocolType.Tls12 Dim lastDate As Date 'HTTP通信機能のある HttpClientクラスをインスタンス化します。 Using client As New System.Net.Http.HttpClient 'HTTPリクエストを準備します。2000年1月1日以降に修正されたかをサーバーに質問する内容を用意します。 '→20年以上前なので修正されていることを期待しています。修正されている場合最終更新日を回答してくれるはずです。 Using request As New Net.Http.HttpRequestMessage(Net.Http.HttpMethod.Get, url) request.Headers.IfModifiedSince = New Date(2000, 1, 1) 'HTTPリクエストを送信し、回答を受け取ります。 Using response As Net.Http.HttpResponseMessage = client.SendAsync(request).Result '回答に最終更新日が含まれていれば抜き出します。 If response.Content.Headers.LastModified.HasValue Then '日時は国際標準時で回答されるので +9時間して日本時間にします。 lastDate = response.Content.Headers.LastModified.Value.DateTime.AddHours(9) End If End Using 'response End Using 'request End Using 'client Return lastDate End Function End Class |
このプログラムの詳細は本題ではないので説明しませんが、コメントで簡単な説明は書いておきました。
キーワード Shared (読み方:Shared=シェアード)をクラスのメンバーの宣言に付けるとそのメンバーは共有メンバーになります。
そのため、このプログラムのメソッド GetLastModifiedDate を使って、首相官邸のトップページの最終更新日を取得するには次のように記述します。
'首相官邸トップページの最終更新日を取得します。 Dim lastDate As Date = WebFile.GetLastModifiedDate("https://www.kantei.go.jp/") MsgBox(lastDate) |
Webサイトにプログラムから連続してアクセスすると 攻撃 とみなされます。ご注意ください。裁判所が認めればWebサイトの管理者はどの端末から攻撃されているか特定することができます。(本当に悪い人は、特定を困難にするように外国のサーバーを経由するなど巧みな偽装しているようですが) Webサイトやアプリケーションの正常な運用に支障が発生しなければ大丈夫だとは思います。私の感覚では単純なHTTP GETになら1分に1回アクセスするくらいは全然平気で、多分1秒に1回でも大丈夫なはずです。For ~ Next や Do ~ Loop でWebサイトにアクセスするとすごい数のアクセスが発生する場合があり、アウトかもしれません。 |
しかし、インターネットのことですから、このプログラムはいつも成功するかはわかりません。たとえば、何かの原因で首相官邸のWebサイトが公開を停止したり、URLが変わったり、あるいは単にパソコンや電波の調子が悪くてインターネットにつながらないということもあるかもしれません。
存在しないURLを使って実行してみるとどうなるか簡単に試すことができます。URLをわざと存在しない https://www.kantei2.go.jp/ に変更して実行してみると、次のような例外が発生し処理が中断します。
Visual Studioで実行していると、例外発生時に詳しい情報が表示され、何が起こっているのか調査することができますが、完成したexeを直接実行している場合、例外が発生したというメッセージが表示され、プログラムは終了します。
それを試すには、exeを直接実行するか、Visual Studio で [デバッグ]メニューの[デバッグなしで開始]をクリックします。次のようになります。
「続行」ボタンをクリックするとアプリケーションの終了は免れますが、処理は実行されません。メッセージボックスが表示されないことからも、続きが実行されていないことがわかります。
例外が発生して処理が中断した場合、通常のフローが実行されなかったということなので、プログラムの状態は想定どおりではありません。「続行」したときに、想定外の状態による2次被害が起こるかもしれません。特別な事情がなければ「終了」を選択してください。 私は未保存の重要な情報がある場合、「続行」を選択して、その情報を保存したり、コピペで逃がすということをやるときがあります。その操作も保証されないので、自己責任ということになります。 |
このように、Webサイトから応答が返ってくることを前提に通常のフローをプログラムしたのですが、Webサイトの応答が返ってこないという例外的なできごとが発生したため、通常のフローの実行が終了してしまいました。この場合、この例外の発生によって、エラーが発生したと言っても良いです。実際、上記のダイアログには「例外が発生しました。」とも書いてありますし、「エラーが発生しました。」とも書いてあります。
さて、ネットワークを使って通信するプログラムでは、何かの事情で相手が応答できないことは当然想定できることなので、相手が応答しなかったときのこともプログラムしておくべきでしょう。
たとえば、相手が応答しない場合「https://xxxxx は応答していません。時間をおいて再実行してください。」というようなメッセージを表示するという仕様にしてみましょう。
そのために、とりあえず例外が発生した場合、すべてこのメッセージを表示するようにプログラムを改造してみます。後で説明しますが、Try ~ Catch ~ End Try という構造を使うことで例外が発生した場合の処理を記述することができます。URLが間違っている以外にもいろいろな例外の原因があるので、なんでもかんでもこのメッセージを表示するのは乱暴なのですが、まずは例外プログラムのはじめの一歩なのでこの点には目をつむることにします。
Dim url
As String =
"https://www.kantei2.go.jp/" Try 'Webサイトの最終更新日を取得します。 Dim lastDate As Date = WebFile.GetLastModifiedDate(url) MsgBox(lastDate) Catch ex As Exception 'MsgBox(url & " は応答していません。時間をおいて再実行してください。", MsgBoxStyle.Information) '←VB2013以前の場合 MsgBox($"{url} は応答していません。時間をおいて再実行してください。", MsgBoxStyle.Information) '←VB2015以降の場合はこれが便利 End Try |
これで実行すると、例外は発生するのは変わりませんが、例外発生時にメッセージが表示されるようになっているので、使用者には通常の応答のように見えます。つまり、例外は発生するけれどもエラーではないという状態です。
※もっとも、「エラーとは何か」の考え方によってはエラーと言えなくはないです。このような認識の問題で、よく、ユーザーがエラーだというと、メーカーは仕様だという齟齬が発生します。ただ、これはもうプログラムの問題ではなく、人間の認知の問題です。
通常通りにフローが実行されていても、機能や結果が想定(仕様)と異なるならばエラーです。これを論理エラーと呼びます。
たとえば、四捨五入する(つもり)の下記のプログラムには論理エラーがあります。
''' <summary> ''' value を四捨五入して整数にします。 ''' </summary> Private Function Sishagonyu(value As Double) As Integer Return CInt(Math.Round(value)) End Function |
だいたいは期待通りに動作するのですが、引数 value が 2.5 のとき、戻り値が 2 になります。四捨五入であれば 3 になるべきです。
しかし、例外が発生するわけではありません。
正しい四捨五入のプログラムは、下記のサンプルを参照してください。 |
話が少しややこしいのでまとめておきます。
例外とは、通常のフローの実行が終了してしまう例外的なできごと。
エラーとは、仕様どおりにプログラムが動作しないこと。
そのため、例外が発生したときの仕様が決められていて、その仕様どおり動作するならば、例外が発生してもエラーではない。
Visual Studioは例外が発生すると調査用に詳細な情報やヒントを表示します。これを例外ヘルパー または 例外処理アシスタントと呼びます。
ためしに簡単な例外を発生させて見ましょう。
次のプログラムは存在しないファイルを読み込もうとして、例外が発生します。
※もし、何かの酔狂で C:\xxxxxxx というファイルがあなたのパソコンに実在するなら、別の存在しないファイル名を指定してください。
Dim result As String = IO.File.ReadAllText("C:\xxxxxxx") |
Visual Studio で普通に実行すると次のようになります。この表示はバージョンによって少し違います。これはVisual Studio 2019のものです。
これが例外ヘルパー (または 例外処理アシスタント)です。いろいろな表示があります。
まず、例外の種類を見ましょう。この例では System.IO.FileNotFoundException と書いてあります。プログラムではいろいろ例外が定義されており、どの例外が発生したかがこれでわかります。これは例外処理の最も基本的な情報です。先輩のプログラマーに助けを求めると「どんな例外が発生したんだ?」と聞かれるでしょう。その時、答えるべきなのはこれです。「先輩、System.IO.FileNotFoundExceptionです。」これだけで先輩は何が起こっているか理解して解決方法を教えてくれるかもしれません。「それは、指定したファイルがないということだよ。ファイル名が間違ってないか、存在するか確認してみな。」
例外メッセージは、最も人間にわかりやすいメッセージです。この例でもずばり何がいけないのかを示しています。
スタックトレースは、この例外がどこで発生したのかを示しています。Visual Studioも例外が発生した行で停止するので、スタックトレースを見なくても実際に停止している場所はわかります。スタックトレースの優れたところはそれがどこから呼ばれたかまで教えてくくれることです。
詳細の表示をクリックすると、クイックウォッチが起動します。さきほども説明したように例外にはいろいろな種類があり、その数はおそらく100を超えています。これらの例外はそれぞれがクラスであり、奇妙に思えるかもしれませんがプロパティやメソッドを持っています。例外処理アシスタントを見ても何が起こっているか、どう解決したらよいかわからない場合、例外クラスのプロパティなどにヒントがあるかもしれません。「詳細の表示」をクリックして、クイックウォッチで確認してみましょう。
この例の場合たいした情報はありません。
この中で一番役に立つのは InnerExceptionプロパティ (読み方:InnerException=インナーエクセプション)です。例外というのは連鎖的に発生することがあります。まず例外Aが発生したので、例外Bが発生し、それをきっかけに最終的に例外Cが発生するというような連鎖です。例外処理アシスタントは最後に起こった例外の情報を表示します。InnerExceptionプロパティは、その元となった例外情報を持っています。
画像の例ではInnerExceptionの値は Nothing になっていて、元となった例外が存在しないことを示しています。
発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。 LiveShareを使うと詳しい知り合いと一緒にVisual Studioを操作しながら助けてもらうことができます。 英語の動画ですが、なんとなく何が起こっているかわかります。 https://www.youtube.com/watch?v=9QXwSg9-2qQ&feature=youtu.be&t=140 |
例外発生時の処理を何も記述していなければ、例外の発生とともにプログラムは終了します。
例外の発生を検知して、何か処理を記述することを「例外を補足(キャッチ)する」「例外をハンドルする」と言う場合があります。
「例外」というものを発明した人物は、例外をどこかから飛んでくる野球のボールのようにイメージしていたのではないかと思います。(これは私が思っているだけです。) 通常のフローを実行していたのに、急にどこかからか野球のボールが飛んでくるわけです。実際、例外を発生させることを「例外をスロー(Throw)する」と言います。Throwとは「投げる」という意味です。 そして、ボールが落っこちたらゲームオーバーなのですが、うまくキャッチ(補足)して何か処理を実行することもできるという比喩だと思います。 ※英語版Wikipediaによると、1972年にMacLispという言語で CATCH・THROWというキーワードが使われたようです。 Exception handling (英語) |
例外を補足するには Try ~ Catch ~ End Try の構文を使用します。(読み方:Try=トライ, Catch=キャッチ, End Try=エンド トライ)。
Try の構文は独特の雰囲気があり、慣れるまでは使いにくいと思います。
Try
通常のフロー
Catch 例外1を表す変数 As 補足する例外1
例外処理
Catch 例外2を表す変数 As 補足する例外2
例外処理
Catch ・・・ '←いくつでも Catch を追加できます。
・・・
Finally
必ず実行する処理(例外が発生してもしなくても実行されます。)
End Try
下記はシンプルな Try ~ Catch ~ End Try の例です。
Debug.WriteLine("開始") Try Dim result As String = IO.File.ReadAllText("C:\xxxxxxx") Debug.WriteLine("うまく処理を完了しました。") Catch ex As Exception Debug.WriteLine("例外が発生しました。") End Try Debug.WriteLine("終了") |
この例だと、C:\xxxxxxx というファイルを読み込んで、「うまく処理を完了しました。」というメッセージを表示するのが通常のフローです。
C:\xxxxxxx というファイルは実際には存在しない(と思う)ので、このプログラムを実行すると「例外が発生しました。」とだけ表示されます。
プログラムは下記の順番で実行されていきます。
ポイントは、通常のフローは Try ~ Catch の間 (この区間を Catch節 とも呼びます) に記述することです。この通常のフローで例外が発生すると、通常のフローの実行は停止し、例外処理が実行されます。例外処理は Catch 節 に記述します。
この例では通常フローの1行目(上記図の②)で例外が発生するため、通常フローの2行目は実行されず、例外処理が実行されます。
もし、冷害が発生しない場合は、例外処理(上記図の③)は実行されません。
このプログラムでは例外処理に1行(上記図の③の行)しか記述してありませんが、複数行記述することもできます。ここに書くべきプログラムは、例外を収束させ、通常のフローを再開できるようにする処理、または、未保存のデータを保存するなどして安全にプログラムを終了する処理です。
上記図の①の行と④の行は Try ~ End Try の外にあるので、Try ~ End Try の影響を受けません。つまり、例外処理のない通常のフローです。
Finally(読み方:Finally=ファイナリー)を使うと例外が発生してもしなくても最後に実行する処理を記述することができます。Finallyは省略可能です。必要な場合だけ記述します。
Debug.WriteLine("開始") Try Dim result As String = IO.File.ReadAllText("C:\xxxxxxx") Debug.WriteLine("うまく処理を完了しました。") Catch ex As Exception Debug.WriteLine("例外が発生しました。") Finally Debug.WriteLine("Finallyを実行します。") End Try Debug.WriteLine("終了") |
このプログラムは下記の順番で実行されます。
この例では例外が発生してから Catch → Finally が実行されますが、例外が発生しない場合でも通常フローに続いて Finally が実行されます。
ただ、VB2005以降は Finally を使う機会がかなり少なくなっているように感じます。もともと、何か後処理が必要な場合に Finally に記述することが多かったのですが、VB2005で Using ~ End Using が登場してからは、Using ~ End Using を使っておくだけで、何もしなくても後処理(=Disposeメソッドの呼び出し)が必ず実行されることが保証されるようになりました。Using ~ End Using は例外が発生した場合でも必ず Dispose を呼び出してくれます。そうするとFinallyに記述する後処理というのは Using ~ End Usingで対応できないものということになるので、かなりレアになるわけです。
なお、Endステートメントでプログラムを強制終了させるとFinallyを実行せずにプログラムが終了します。Endステートメントは強制終了をする機能なので基本的には使用しないようにご注意ください。
公式ドキュメント(https://docs.microsoft.com/ja-jp/dotnet/visual-basic/language-reference/statements/try-catch-finally-statement)には、Endステートメントの他に、StackOverflowExceptionの場合は、Finallyが実行されないと記述されています。StackOverflowExceptionとは、簡単に言うと、プログラム内でいろいろなものを呼び出しすぎてもうこれ以上の呼び出しは実行できませんという極限状況を意味しています。 公式ドキュメントには記載がありませんが、メモリが足りなくなって処理を実行できない状況でもFinallyは実行されないのではないかと私は予想します。(例外を発生させる余力が残っている場合OutOfMemoryExceptionが発生します。) どんなにがんばって穴のないプログラムを書いても、雷が落ちたり、停電が発生したりするとプログラムは強制的に中断されてしまいます。ハードウェアが故障したり、ユーザーがWindowsのタスクマネージャーを使って強制終了させてしまうこともあるかもしれません。だから、本当の意味で「必ず実行される処理」を書くことは不可能です。もし、停電しても必ず実行される処理が必要である場合は、単一のプログラムの力だけでは実行不可能で、もっと広い目線でグランドアーキテクチャーをデザインしてリスクを低減させる措置を取ることになります。そのための労力やコストはかなりのものとなるので、停電が発生した場合など本当にイレギュラーな情報は終了処理が実行できなくてもあきらめられるような構造になっているのがベターです。 参考:無停電電源装置 https://kakaku.com/pc/ups/guide_0140/?lid=shop_pricemenu_guide_0140#Section1 |
Try節や Catch節 で Exit Try を使用すると、そのTry節や、Catch節から抜け出すことができます。抜け出した後FinallyがあればFinallyが実行され、FinallyがなければEnd Tryの次から処理が続行されます。
私は20年近くに及ぶ .NET の経験の中で Exit Try が必要になったことは一度もないように思います。Try節で例外が発生してCatch節に実行がうつるだけでもある種のジャンプのようなことが発生しているのに、さらに Exit Try でもジャンプがありえるとすると少し複雑すぎます。あまり使用すべきではないように感じます。
Visual Studio では trycf と入力してから TABキーを押すことで、Try ~ Catch ~ Finally ~ End Try の雛形を生成してくれます。
または、Try ~ Catch ~ Finally ~ End Try を挿入したい箇所で 右クリック して [スニペット] - [スニペットの挿入] で 「コードパターン - If、For Each、Try Catch、Property、その他」- 「エラー処理(例外)」から、3パターンの Try を選択して挿入することができます。
Catchでは、上記の例のように Catch ex As Exception とするとすべての例外をキャッチできます。Catch ex As XxxxxxException と記述すると、XxxxxxException または、XxxxxxException の派生クラスの例外だけをキャッチできます。
「派生クラス」(はせいクラス)という言葉はまだ初級講座では説明していませんが、簡単に言うとあるクラスの機能を引き継いで別のクラスを作ることができて、それを派生クラスと呼びます。例外の場合は、ある例外をさらに詳細で区別する必要がある場合などに派生クラスとして新しい例外が作成される場合があります。例外処理においてはあまり派生クラスは気にしなくても大丈夫です。 |
この As の後ろに指定できるものは 例外を表すクラスです。これらのクラスをひとまとめに「例外クラス」と呼びます。例外クラスは名前の最後が Exception になっているという慣例があります。これは単なる慣例なので、無視してExceptionで終わらない名前の例外クラスを作ることは可能ではありますが、見たことがありません。
Exception以外の例外クラスを指定した場合、たとえば、
Catch ex As IO.FileNotFoundException
と記述すると、このCatch節は FileNotFoundException (読み方:FileNotFoundException=ファイルノットファウンドエクセプション)の例外が発生した場合だけ実行されます。
この FileNotFoundException の例外は、存在しないファイルを読み込もうとするなど、存在する前提のファイルが存在しない場合に発生する例外です。
もう1つ例を挙げましょう。
Catch ex As DevideByZeroException
と記述すると、このCatch節は DevideByZeroException (読み方:DevideByZeroException = ディバイドバイゼロエクセプション)の例外が発生した場合だけ実行されます。
この DevideByZeroException の例外は、整数などのわり算をするときに 0 で割ろうとすると発生します。学校でも ÷ 0 はできないと習いましたよね。
発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。 0 でわり算ができない理由は、答えが無限になるからです。無限という概念は難しく、「無限 + 1 と 無限 + 2 はどちらが大きいか?」という難問が派生します。これを常に気にする必要があると、単純な式 x+1 < x+2 ですら、x=無限の場合を含めると100%成り立たなくなり、大きな影響があります(この式の左辺と右辺からそれぞれ -x するとその影響の大きさに身震いします)。このような難問に挑むよりも算数・数学の基本的な考え方を理解・習得するほうが現実的かつ実用的であるため小中学校の算数・数学では無限を扱わず、「0で割ることはできない」というスコープで数を扱っています。(小中学校で無限を扱わない理由は私の推測です。) 一方、プログラムに登場する浮動小数点型(VBではSingleとDouble)には、無限を表現する Inifinity (読み方:Inifinity=インフィニティ)という値が用意されているため、0 で割り算すると値が Inifinity になるだけで例外は発生しません。 値がInifinityであるかどうかは、Single.IsInfinityメソッドまたはDouble.IsInfinityメソッドで確認できます。
無限をあらわす手段があるからと言って、「無限 + 1 と 無限 + 2 はどちらが大きいか?」という難問が回避されるわけではなく、扱いは難しいです。私はSingleやDoubleは極力使用せずに、小数が必要である場合はDecimalを使用するように推奨しています。Decimalは私たちが想像したとおりに動作してくれるので扱いやすいです。0 で割り算すると DevideByZeroException も発生します。 ところ、ついでに、Inifinity を使って 「無限 + 1 と 無限 + 2 はどちらが大きいか?」を調べてみましょう。何が起こるか想像できますか?
|
Catch で特定の例外を対象にしている場合、それ以外の例外が発生すると、Try ~ Catch を記述していない場合と同じことがおきます。つまりプログラムの実行は停止します。
次の例は、先ほどの例とほとんど同じですが、 Catch の部分で As Exception を As DivideByZeroException に変更しています。
Debug.WriteLine("開始") Try Dim result As String = IO.File.ReadAllText("C:\xxxxxxx") Debug.WriteLine("うまく処理を完了しました。") Catch ex As DivideByZeroException Debug.WriteLine("例外が発生しました。") End Try Debug.WriteLine("終了") |
これを実行すると、IO.File.ReadAllText の行でプログラムの実行が停止し、最初に説明したようにVisual Studioで実行していれば例外ヘルパーが表示され、exeを直接実行していれば、終了か続行の選択を求めるダイアログが表示されます。
例外の種類に応じて複数の Catch を記述することもできます。
下記の例は4つの異なる例外クラスに対してそれぞれCatch節があり、発生した例外によって実行されるCatch節が異なります。
このどれでもない例外が発生した場合は、プログラムの実行は停止します。
Try 通常フロー Catch ex As DivideByZeroException Debug.WriteLine("DivideByZeroExceptionの例外が発生しました。") Catch ex As ArgumentException Debug.WriteLine("ArgumentExceptionの例外が発生しました。") Catch ex As InvalidCastException Debug.WriteLine("InvalidCastExceptionの例外が発生しました。") Catch ex As UriFormatException Debug.WriteLine("UriFormatExceptionの例外が発生しました。") End Try |
しかし、これほどまでに小分けに例外処理が存在するというのは何かおかしいです。Catchは「例外」なんです。いろいろな例外を想定しているならば、それはもはや例外ではなく、通常のフローに記述できないか検討するべきです。たとえば、わり算を実行する前に割る数が 0 かどうか調べるIf文を記述すれば DevideByZeroException の例外が発生することは防げます。
通常のフローとしてIf文を書いても、例外処理として Catch 文を書いても、書く場所が違うだけで同じではないかと思われるかもしれませんが、この2つにはかなりの違いがあります。VBや.NETにはプログラムを効率よく構造化するための仕組みがたくさん用意されていますが、これらの仕組みは通常フローとしては使いやすくできていますが、例外処理と連携させる場合、複雑なことを考慮しないとうまく使えないことがよくあります。たとえば、If や Select Case を使っていろいろな条件で分岐することを考えてみてください。これに加えてさらに Try ~ Catch でも何か分岐があるとするとそれだけで考えることが増えてしまいますよね。さらにまだ説明していない機能、たとえば非同期処理(つまり処理Aと処理Bを並行して実行していく仕組み)では、非同期で例外が発生した場合の扱いがことさら難しく、実際にこのプログラムをやってみると例外など発生して欲しくないものだと願うことになります。
例外はあくまで「例外」ととらえ、例外処理をがんばってたくさん記述していくのではなく、通常フローのプログラムをがんばるのが正しい方向であるということを忘れないようにお願いします。
とは言え、例外が起こらないように通常フローで何もかもチェックするのはなかなか大変な場合があります。たとえば、ファイルを作成する処理では、対象のパスがそもそも存在するか?書き込み権限はあるか?ストレージの要領(ディスク容量)は残っているか?など逐一チェックするのも非効率です。 メモ帳など一般的アプリケーションでファイルの保存に失敗したら、「保存に失敗した」というメッセージを表示 と 可能であればその理由(後で説明しますが例外のメッセージを取得する手段があります)を表示する程度でよく、なにかこった例外処理が必要というわけではありません。そうであれば、この場合は、ひとくくりに例外処理を記述したほうが良いでしょう。 |
Catch節内では例外クラスからエラーメッセージなど詳細を情報を取得することができます。どのような情報が取得可能かは例外クラスによって異なります。例外ヘルパーからクイックウォッチしたときに確認できる情報と同じです。
Catch節内で例外クラスにアクセスするには、 Catch ex As xxxxException と記述している場合、変数 ex を使用します。
この変数 ex は、通常の変数と同じように自由な名前を付けることができますが、ex という名前を付けることがほとんどです。
たとえば、次のプログラムは例外発生時にエラーメッセージとスタックトレースをテキストファイルに書き込みます。
'セキュリティのあるプロトコル(TLS 1.2)を使用します。 Net.ServicePointManager.SecurityProtocol = Net.SecurityProtocolType.Tls12 'HTTP通信機能のある HttpClientクラスをインスタンス化します。 Using client As New System.Net.WebClient Try '▼通常のフロー Dim content As String = client.DownloadString("https://www.yahoo.co.jp/xxxx") Debug.Write("応答を受信しました。") Debug.Write("-----------------------") Debug.Write(content) Catch ex As Net.WebException '▼例外処理 '実行中のアプリケーションのパスを取得 Dim appPath As String = AppDomain.CurrentDomain.BaseDirectory.TrimEnd("\"c) 'エラー情報を書き込むファイルのフルパス Dim fileName As String = appPath & "\error.txt" 'エラーメッセージを改行つきで書き込み IO.File.AppendAllText(fileName, ex.Message & Environment.NewLine) 'スタックトレースを改行つきで書き込み IO.File.AppendAllText(fileName, ex.StackTrace.ToString & Environment.NewLine) '書き込んだファイル(error.txt)のあるフォルダーを開きます。 Process.Start(appPath) End Try End Using 'client |
このプログラムは https://www.yahoo.co.jp/xxxx にアクセスして、内容を取得します。成功すると html の内容がデバッグウィンドウに出力されるのですが、このURLは存在しないのでWebException(読み方:WebException=ウェブエクセプション)の例外が発生します。
例外処理の中で 変数 ex を使って例外クラスにアクセスし、ex.Messageプロパティ と ex.StatckTraceプロパティを使って、エラーメッセージとスタックトレースを取得し、ファイルに記録します。
例外処理で例外の情報をファイルなどにログに記録しておくのは一般的な処理です。
When(読み方:ホエン) を使用すると特定の例外が発生した場合で かつ 何か条件を満たすだけ 例外をキャッチすることができます。
次のプログラムはWhenで追加条件を指定していないので WebException が発生すると必ず Catch で捕まえます。
'セキュリティのあるプロトコル(TLS 1.2)を使用します。 Net.ServicePointManager.SecurityProtocol = Net.SecurityProtocolType.Tls12 'HTTP通信機能のある HttpClientクラスをインスタンス化します。 Using client As New System.Net.WebClient Try '▼通常のフロー Dim content As String = client.DownloadString("https://www.yahoo.co.jp/xxxx") Debug.Write("応答を受信しました。") Debug.Write("-----------------------") Debug.Write(content) Catch ex As Net.WebException '▼例外処理 '実行中のアプリケーションのパスを取得 Debug.Write("WebException発生") End Try End Using 'client |
このプログラムは https://www.yahoo.co.jp/xxxx にアクセスして、内容を取得するのですが、このURLは存在しないのでWebException(読み方:WebException=ウェブエクセプション)の例外が発生します。
インターネットから情報を取得できない原因はたくさんあるのに、例外クラスはそれほど細かく分かれていません。ドメイン(www.yahoo.co.jp)が存在しないからエラーなのか、ドメインはあるけど xxxx というページが存在しないからエラーなのか、何かログインが必要なページだからエラーなのか、When を使うとこういった追加の条件を指定できる場合があります。
この例では xxxx というページが存在しないことが例外発生の理由であり、HTTPの仕様ではこの場合、404 という応答を返すことになっています。そこで、When を使って次のように 404 応答が返ってきてWebExceptionが発生した場合だけ、例外をCatchするというプログラムができます。
'セキュリティのあるプロトコル(TLS 1.2)を使用します。 Net.ServicePointManager.SecurityProtocol = Net.SecurityProtocolType.Tls12 'HTTP通信機能のある HttpClientクラスをインスタンス化します。 Using client As New System.Net.WebClient Try '▼通常のフロー Dim content As String = client.DownloadString("https://www.yahoo.co.jp/xxxx") Debug.Write("応答を受信しました。") Debug.Write("-----------------------") Debug.Write(content) Catch ex As Net.WebException When ex.Message.Contains("404") '▼例外処理 '実行中のアプリケーションのパスを取得 Debug.Write("そのページは存在しません。") End Try End Using 'client |
発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。 このサンプルの When の条件は次のように書くこともできます。 Catch ex As Net.WebException When DirectCast(ex.Response, system.net.httpwebresponse).StatusCode = Net.HttpStatusCode.NotFound |
複数のCatchとともにこれを使うと同じ例外でも異なる例外処理を定義することが可能になります。
Using client
As New System.Net.WebClient Try '▼通常のフロー Dim content As String = client.DownloadString("https://www.yahoo.co.jp/xxxx") Debug.Write("応答を受信しました。") Debug.Write("-----------------------") Debug.Write(content) Catch ex As Net.WebException When ex.Message.Contains("404") '404(=ページがない)時の例外処理 Debug.Write("そのページは存在しません。") Catch ex As Net.WebException When ex.Message.Contains("407") '407(=プロキシー認証ができない)時の例外処理 Debug.Write("プロキシーの認証情報が必要です。") Catch ex As Net.WebException When ex.Message.Contains("500") '500(サーバーエラー)時の例外処理 Debug.Write("アクセスしたページで何か問題が発生しています。現在利用できません。") End Try End Using 'client |
このようにWhenだけが違う同じ例外に複数のCatchを用意する必要があるとすると理由は何でしょうか?
Catchを1個にして中で分岐しても良いように思うかもしれませんが、これはほとんどの場合アンチパターン(ダメなやり方)です。
Using client
As New System.Net.WebClient Try '▼通常のフロー Dim content As String = client.DownloadString("https://www.yahoo.co.jp/xxxx") Debug.Write("応答を受信しました。") Debug.Write("-----------------------") Debug.Write(content) Catch ex As Net.WebException If ex.Message.Contains("404") Then '404(=ページがない)時の例外処理 Debug.Write("そのページは存在しません。") ElseIf ex.Message.Contains("407") Then '407(=プロキシー認証ができない)時の例外処理 Debug.Write("プロキシーの認証情報が必要です。") ElseIf ex.Message.Contains("500") Then '500(サーバーエラー)時の例外処理 Debug.Write("アクセスしたページで何か問題が発生しています。現在利用できません。") End If End Try End Using 'client |
WebExceptionにはこのプログラムで扱っている 404・407・500 以外にも多数のエラーコードが用意されています。When で記述する場合、404・407・500以外の理由でWebExceptionが発生すると、Catchしていない状況になるので、プログラムの実行が停止します。想定していない状況が発生して停止するのは、被害を最小限にとどめるための正しい判断です。想定しないエラーが発生しているのに実行を継続すると2次被害・3次被害が発生してしまうかもしれないからです。
一方、WebExceptionを条件なしでCatchしてしまうと、404・407・500以外の場合もCatchが実行されます。しかし、その場合、どのIfの分岐にも当てはまらないので実際に実行される処理は何もありません。そして、End Try到達後は何事もなかったかのように処理が継続していきます。つまり、想定外の状況が発生しているのに何もせず処理を継続する→2次被害・3次被害の可能性がある ということです。
※何もする必要がないとわかっていてこのように書くのであれば OK です。
念のために補足します。WebExceptionの場合 When を使うべきだ と言っているわけではありません。When を使って分岐させるのと Catch節内で If を使って分岐するのとでは意味が違うということが言いたいことです。When にするか If や Select Case で分岐するか迷ったらこの違いを理解したうえで適切な方を選択してください。 とは言え、前述したように「例外」の処理を複雑に書くのは良くないことなので、WhenとかIfでがんばらなければいけないという状況になったら、その時点で何かもっといいやり方があるのではないか考えてみることも必要だと思います。 |
なお、When の条件は例外クラスとは関係なく書くこともできます。たとえば、変数 x の値が 3 で、WebExceptionが発生した場合に例外を捕まえたければ Catch ex As Net.WebException When x = 3 と書けます。
どういうどきにどういう例外が発生するかはMicrosoft Docsで各メソッドごとに書いてあります。
たとえば、先ほど例で使用したFileクラスの ReadAllTextメソッドが発生させる場合がある例外はReadAllTextメソッドの説明に書いてあります。
このメソッドは引数pathにファイル名を指定すると、戻り値としてそのファイルの内容を取得する機能です。
引数pathが空の場合は ArgumentException、引数pathがnullの場合(nullとはVBでいうところのNothingです) ArgumentNullExceptionなど想定するケースごとにどのような例外になるか書いてあります。
メモリ不足を表す OutOfMemoryException など、メソッドの機能と関係なく発生する例外もありますので、何かメソッドを呼び出し時に、Microsoft Docsでそのメソッドの説明に記載されていない例外が発生することもあります。それも含めて発生する可能性があるすべての例外の一覧を取得する方法はありません。
よく目にする例外をいくつか紹介しておきます。
rgumentException(読み方:ArgumentException=オーギュメントエクセプション)は、引数に想定外の内容を指定したときに発生します。たとえば、ファイル名を指定すべきところに何か(ファイル名とは解釈できない)文章を指定した場合です。Convert.ToStringメソッドのオーバーロードの1つ、Convert.ToString(Integer,Integer)は2つ目の引数には、2, 8, 10, 16 の4種類の値しか指定できません。他の値を指定すると ArgumentException が発生します。
メソッドに引数を付けて呼び出すことはとても多いので、ArgumentException を見る機会も多いはずです。ArgumentExceptionが発生した場合はどの引数でそれが発生したのかを確認し、正しい値はどのようなものなのか、必要に応じてそのメソッドのMicrosoft Docsでの記述を確認することをお勧めします。
ArgumentNullException(読み方:ArgumentNullException=オーギュメントヌルエクセプション)は、Nothing を指定できない引数に Nothing を指定した場合に発生します。
InvalidCastException(読み方:InvalidCastException=インバリッドキャストエクセプション)は、型変換できないのに、その型変換を使用としたときに発生します。
次の例は文字列 "ABC" を 数値に変換しようとしているのでInvalidCastExceptionが発生します。
Dim x As Integer = CInt("ABC") |
NullReferenceException(読み方:NullReferenceException=ヌルリファレンスエクセプション)は、値がNothingの変数を使ってメソッドやプロパティなどを呼び出そうとして時に発生します。初心者のうちは特によく発生すると思います。
次の例は、インスタンス化されていない変数 names の Add メソッドを呼び出そうとしているのNullReferenceExceptionが発生します。
Dim names As
List(Of
String) names.Add("Apple") '←ここでNullReferenceException |
念のために書いておくとNewを使ってインスタンスを生成すればこの例は正常に動きます。Dim names As New List(Of String) という具合です。
Stringのような単純な型でも初期化されていない状態は Nothing なので、メソッドやプロパティを呼び出すとNullReferenceExceptionが発生します。
Dim value As
String If value.Length = 0 Then '←ここで NullReferenceException Debug.WriteLine("valueは空です。") End If |
この場合、value に初期値 "" を割り当てるか、valueのメソッドやプロパティを呼び出さない手段で内容を確認すれば正常に動作します。たとえば、If Len(value) = 0 Then や If String.IsNullOrEmpty(value) Then や If value = "" Then などです。これらの式は微妙に意味が違いますので、確認したい内容に合わせて修正してください。
If Len(value) = 0 Then と If String.IsNullOfEmpty(value) = 0 は、valueの値が0文字か、value が Nothing である場合 True になります。 If value = "" Then は valueの値が0文字の場合、True になります。 Dim value As String = "" と初期化した上で、If value.Length = 0 Then とした場合も同様で、valueの値が0文字の場合、Trueになります。 |
あまり見かけないかもしれませんが、少し癖のある例外もあるので簡単に紹介しておきます。
メソッドAからメソッドBを呼び出して、メソッドBからまた別のメソッドAを呼び出して、というようなお互いを呼び出すプログラムを書くと、永遠に呼び出し続けてプログラムが終わらなくなります。今私のプログラムで試してみたところ14000回ちょっとで例外が発生しました。このとき発生する例外が StackOverflowException (読み方:StackOverflowException=スタックオーバーフローエクセプション)です。
VBでは、メソッドを呼び出すときは常に戻り先としての呼び出し元を記録しておきます。この記録を呼び出し履歴と呼ばれ、「スタックトレース」略して「スタック」と呼ばれることもあります。14000回も呼び出すとこのスタックは14000個の達します。こんなに大量のスタックは必要ないはずなので、このあたりが限界となりこれ以上スタックを増やせなくなります。その状態が StackOverflowException です。このとき、スタックがいっぱいになってしまっているので、通常のプログラムの動作はほとんど行うことができなくなります。そのためTry ~ Catch でもCatchできず停止してしまいます。
次の例は、自分自身を永遠に呼び出し続けてStackOverflowExceptionを発生させるメソッドの例です。Try ~ CatchしているのもかかわらずCatchできません。StackOverflowExceptionが発生するまで1秒もかかりません。例外が発生したらVisual Studioで実行の停止をすれば良いので、お試しする限りでは特に脅威はありません。
Private Sub DigStack(count
As Integer) Try DigStack(count + 1) '←自分自身を呼び出す。 Catch ex As StackOverflowException Debug.WriteLine("スタックがいっぱいになってしまいました!") End Try End Sub |
メソッドA→メソッドB→メソッドC→メソッドA→・・・のように循環して呼び出している場合、どこをどう修正すればよいのかわかりにくいかもしれません。そのときはStackOverflowExceptionが発生したときに[デバッグ]メニューの[ウィンドウ] - [呼び出し履歴] を確認すると、このスタックの内容を簡易的に確認できるので何が起こっているかわかりやすいです。
このようにMethodA→B→Cが繰り返し呼び出されているのがわかります。プログラムの何行目から呼び出されているかも表示されるので便利です。
AggregateException(読み方:AggregateException=アグリゲートエクセプション)は、非同期処理で例外が発生した場合にスローされます。
通常プログラムは、書いてある順番に1個ずつ処理されていくのですが、時間がかかる処理が必要な場合、その時間がかかる処理が終了するのを待っている間、別の処理を動かしたいというが良くありますし、VBではそのようなことも簡単にできるようになっています。(その方法は別の機会に説明します。)
これはとても便利なのですが複雑な問題をいろいろ引き起こします。例外処理がその代表です。
その「時間がかかる処理」がちゃんと終了せず、例外が発生したらどうするかということです。このとき発生するのがAggregateExceptionです。時間がかかる処理は複数の例外を発生させているかもしれないので、AggregateExceptionはそれらの例外を1つにまとめています。AggregateException自体は、別の処理を実行していたら何か例外が発生した という状況を表しているだけで、具体的に何が発生したかは表していません。
AggregateException発生時に具体的な情報を取得するには AggregateExceptionのInnerExceptionsプロパティ(読み方:InnerExceptions=インナーエクセプションズ)を調べます。
ここにAggregateExceptionの原因となった1個以上の例外クラスが格納されています。
根本的な原因となった例外はArgumentExceptionかもしれませんし、InvalidCastExceptionかもしれませんし、その他の例外かもしれません。しかし、発生している例外はAggregateExceptionなので、この例外を捕まえるには、Catch ex As AggregateException とする必要があります。
ただ、特例があって、AggregateExceptionの原因となった例外が1つだけで、かつ、ある条件をみたすとき、AggregateExceptionではなく、その原因となった例外を直接 Catch することが可能です。この「ある条件」を説明にするには、非同期処理についても説明が必要になってくるためここでは割愛します。要するにこのAggregateExceptionは言語仕様上も特別扱いされているというわけです。
OutOfMemoryException(読み方:OutOfMemoryException=アウトオブメモリーエクセプション)は、メモリーが足りなくなって実行を継続できない場合に発生します。寄せ集めれば大量のメモリーが余っている場合でも、.NETは連続したメモリー領域を要求する場合があるので、この要求に答えられないとOutOfMemoryExceptionが発生します。つまり、Windowsのタスクマネージャーで見るとメモリーはまだ余力があるという状態でもこの例外は発生する場合があります。
システムのメモリーが枯渇している状況の場合、作成したプログラムやVisual Studioが正常に動作するために必要な処理も動作できなくなることがあるので、OutOfMemoryExceptionが発生する状況では通常では考えられないようなことが起こる場合があります。この例外とともに、不可解なことがいろいろ発生している場合は、まずこの例外が発生しないようにすることを目標にしましょう。
Try ~ Catch を使用していると、例外発生時も実行が停止せずCatch節の例外処理が実行されるということをここまで説明してきました。
Catchしていない例外が発生した場合は、Visual Studioは実行を停止し、問題を調査するための例外ヘルパーを表示するということも説明しました。
しかし、これでは不便な場合もあります。
プログラム開発中はいろいろな調査や情報収集が必要な場合があり、Catchしている例外でも、発生時に実行を停止したり、例外ヘルパーを使いたいという場合があります。
特にプログラムが巨大になり、Try の中が複雑になってくると、Catch で捕まえた例外が一体どこで発生したのかわかりにくい、Catchにジャンプする直前に実行を停止して状況を理解したいなどと思うようになってくることがあります。
あまり良くないのですが、Try ~ Catch を適当に使っていてプログラムがごちゃごちゃになってくると、こう思うことが多くなるように感じます。
Visual Studio の例外設定を使うと、細かい例外の種類ごとに発生時の停止方法を指定できます。
この機能はVisual Studioで実行していることが前提なので、プログラムが完成して、exeを直接実行するなど、Visual Studio以外の環境で実行する場合には意味がありません。
例外設定は[デバッグ]メニューから[ウィンドウ] - [例外設定]で表示できます。
例外設定ウィンドウをはじめ表示するとこのように見慣れない項目が出てきます。Visual StudioではVisual Basic以外のプログラミング言語も扱えるため、Visual Basicでは無縁の項目もここでは設定できるのです。
VBで扱う「例外」はこの中では Common Language Runtime Exceptions です。
これを展開すると、下記のように例外の一覧が出現します。
ここでチェックを付けた例外はスローされたとき(つまり発生したとき)にVisual Studioが実行を停止し、例外ヘルパーを表示します。
用が済んだらチェックをはずして元に戻すようにしておいてください。そうしないと、Catchしているのに例外が発生してもCatchにジャンプしてくれない。どうしてだろう??? といういらぬ混乱を招きがちです。
この例外の一覧に目的の例外が表示されていない場合、「Common Language Runtime Exceptions」をクリックした状態で、ヘッダー部の + アイコンをクリックすることで、目的の例外を追加することができます。