Visual Basic 初級講座 |
Visual Basic 中学校 > 初級講座 >
第33回 例外
例外を自分で作る必要性とその利用法について説明します。VBでは例外(=エラー)時の処理についてはすっきりと整理された方法が提供されていますから、自己流のやり方にこだわらないで積極的にVBの仕組みを活用するようにしましょう。
この回の要約 ・Throw を使うと例外を発生させることができる。
・自分で新しい例外クラスを作成することができる。 |
例外とエラー処理についてはこの初級講座の第12回でも既に説明しています。詳しい内容は直接初級講座第12回を呼んでいただければわかるのですが、要するに例外(=エラー)に備えて、Try文を使用するかOn Errorを使用した方が良いというのがそのときの説明でした。
今回はこの点をさらに詳しく説明するのではなく、自分で「例外」(=エラー)を作り出す方法が主題です。 プログラムは例外が発生しないように作るものなのにどうして自分で例外を作り出す必要があるのか疑問に思われる方もいらっしゃるでしょう。それでも確かに自分で例外を作る必要はあるのです。
以下では長くなりますが順に例外が必要となる理由を説明し、その利用方法を紹介してきます。
ますは次のプログラムを見てください。
Private
Sub Button1_Click(ByVal
sender As System.Object,
ByVal e As System.EventArgs)
Handles Button1.Click Call WriteLog("Button1_Clickを実行します。") Call WriteLog("これはテストです。") End Sub |
Private
Sub WriteLog(ByVal
Value As String)
Dim
Writer As New
IO.StreamWriter("C:\Log\MyAppLog.txt",
True) strTime = Now.ToString("yyyy/MM/dd
HH:mm:ss ") End Sub |
■リスト1:ログ作成
このプログラムではボタンをクリックするとログに書き込みを実行します。ログはC:\Log\に作成されるので実行前にこのフォルダを作成しておいてください。「実行前にフォルダを作成する必要がある」ということは今回のポイントのひとつとなります。
さて、実行してログが正常に書き込まれることを確認したら、今度はさきほどのLogフォルダを削除してもう一度プログラムを実行してみてください。今度はフォルダがないので例外が発生します。System.IO.DirectoryNotFoundException(読み方:DirectoryNotFoundException = ディレクトリ ノット ファウンド エクセプション)です。
フォルダがないくらいのことは想像できますから優秀なプログラムならあらかじめこのような事態に備えて準備しておくべきですね。そこでTry文を使って例外を捕まえるようにこのプログラムを改造してみます、次のようになります。
Private
Sub Button1_Click(ByVal
sender As System.Object,
ByVal e As System.EventArgs)
Handles Button1.Click Call WriteLog("Button1_Clickを実行します。") Call WriteLog("これはテストです。") End Sub |
Private
Sub WriteLog(ByVal
Value As String) Try
Dim Writer As New
IO.StreamWriter("C:\Log\MyAppLog.txt",
True) strTime =
Now.ToString("yyyy/MM/dd
HH:mm:ss ")
Catch
ex As Exception End Sub |
■リスト2:単純な例外処理。あまり好ましくない。
これでフォルダがないからと言ってプログラムが例外で強制終了してしまうようなことはなくなります。しかし、ここまでのことはこれまでの初級講座の中でも既に説明していることばかりでとりたてて新しいことはありません。
今回はさらに踏み込んでこのプログラムの問題点を考えて見ます。まず、「例外が発生しました。…」というメッセージが2回表示されるのが気になります。これはボタンのClickイベントでWriteLogを2回呼び出しているからですが、ユーザーの目には鬱陶しく写るでしょう。次にこれと同じことのいいかえではあるのですが、WriteLogが失敗して例外が発生しているのにボタンのClickイベント側ではそのことに気が付かずに処理を続行している点は大問題です。今回のプログラムは特に何をするわけではないので特に不都合には見えないかもしれませんが実際のプログラムでは1つ1つの命令を順番に実行して複雑な処理を行うのですから、途中で例外が発生したことを無視して処理を続けるというのは重大なバグなのです。
それから、WriteLogメソッドの中でエラーメッセージを表示する処理があるというのは一関数一機能の原則に反することになり好ましくありません。ただし、一関数一機能の原則については別の機会に説明しますので今回はこの点では目を瞑ります。
以上3つの問題点を簡単にまとめてみます。
1.エラーメッセージが2回表示される。
2.WriteLogで例外が発生しているのにボタンのClickイベントでは処理を続行しようとしている。
3.WriteLogメソッド内でエラーメッセージを表示している。
実はこの3点ともTryの場所を変えるだけで簡単に解決できるのです。解決編は次のようになります。
Private
Sub Button1_Click(ByVal
sender As System.Object,
ByVal e As System.EventArgs)
Handles Button1.Click Try Call WriteLog("Button1_Clickを実行します。") Call WriteLog("これはテストです。")
Catch
ex As Exception End Sub |
Private
Sub WriteLog(ByVal
Value As String)
Dim
Writer As New
IO.StreamWriter("C:\Log\MyAppLog.txt",
True) strTime = Now.ToString("yyyy/MM/dd
HH:mm:ss ") End Sub |
■リスト3:標準的な例外処理。
このように呼び出し元でTryを書いておけば、呼び出し先で発生した例外も捕まえることができるのです。
しかし、これでは十分でない場合もあります。
たとえば、このプログラムではログに書き込む文字数が10文字までと決まっているとしましょう。また、書き込めるのは半角文字のみで全角文字は書き込めないとしましょう。
この機能をプログラムにそのまま追加すると次のような形になります。
Private
Sub Button1_Click(ByVal
sender As System.Object,
ByVal e As System.EventArgs)
Handles Button1.Click Try Call WriteLog("Button1_Clickを実行します。") Call WriteLog("これはテストです。") Catch
ex As Exception End Sub |
Private
Sub WriteLog(ByVal
Value As String)
'▼文字数チェック
'▼全角文字が混ざっていないかチェック '▼書き込み実行 strTime = Now.ToString("yyyy/MM/dd
HH:mm:ss ") End Sub |
■リスト4:カスタムエラーの実装。あまり好ましくない。
これで確かに望みどおりの動作になるのですが、これをやってしまうと解決したはずのさっきの問題がまた復活してしまうのです。
つまり、メッセージは2回表示されますし、WriteLogに失敗しているのに処理を続行してしまいますし、WriteLogの中でエラーメッセージを表示することになってしまうのです。
これというのも10文字までしか書き込めないとか、全角文字は書き込めないといったことが我々が勝手に決めたことであってVBにとっては例外でも何でもないのでTry文に無視されてしまうからです。
このような問題に対応するのに構造化例外処理、つまり例外とTry文を活用する方法と、活用しない方法と2種類の解決方法があります。今回は構造化例外処理がメインテーマなのですが、まずは例外に頼らない従来からある古典的な解決方法から見てみましょう。
補足説明 -
全角文字の判別 今回の例では全角文字の判別方法として文字数とバイト数を比べる方法を使っています。 半角文字は1文字1バイトなので半角文字だけで構成された文字列は必ず文字数 = バイト数となります。これに対し、全角文字は1文字で2バイトあるので全角文字がまざった文字列は必ず文字数 < バイト数となります。 文字数を取得するにはLen関数を使用します。バイト数を取得するにはEncodingクラスのGetByteCountメソッドを使用します。つまりこの2つの戻り値を比較することで文字列に全角文字がまざっているかそうでないか区別できるわけです。 |
関数の内部で発生するカスタムエラー(※1)に対応するために昔からよく使われている方法は「戻り値」です。
※1:カスタムエラー:プログラムを設計したものが定めるエラー。VBにとってのエラーではない。今回の例ではログに10文字以上書き込もうとしたり、ログに全角文字を書き込もうとするとカスタムエラーとなる。
関数の戻り値がTrueなら正常終了、Falseならエラーがあったということにするわけです。たとえば次のようになります。
Private
Sub Button1_Click(ByVal
sender As System.Object,
ByVal e As System.EventArgs)
Handles Button1.Click Try
If WriteLog("Button1_Clickを実行します。")
=
True
Then Catch
ex As Exception End Sub |
Private
Function WriteLog(ByVal
Value As String)
As
Boolean
'▼文字数チェック
'▼全角文字が混ざっていないかチェック '▼書き込み実行 strTime = Now.ToString("yyyy/MM/dd
HH:mm:ss ") Return True End Function |
■リスト5:戻り値によるカスタムエラーの通知
WriteLogメソッドの宣言がPrivate SubではなくPrivate Functionに変更されている点に注意してください。
これで先ほどよりはスマートに動作しますが、プログラムは気に入らないものとなります。ボタンのClickイベントを見ていただくとわかるのですが、今まではなかったIf文が追加されています。これだけの短いプログラムでもIf文の追加が必要となるのですから、大きく複雑なプログラムで同じことをやったらIf文がいくつふえるのか考えたくないほどです。
それでも、これで解決できるとあればIf文くらい恐れるものではありませんから、これを読んでいる皆さんはこの方式で対応すると決めた場合には堂々と大量のIf文を追加してください。
それから、今回の件では良いのですが、この例ではボタンのClickイベント内ではWriteLogが成功したか失敗したか判断することはできますが、失敗した場合に何故失敗したのか知る方法がありません。この例は単純なのでエラーの原因を知る必要はないのですがいつもそのようなプログラムばかり作っているわけではないはずです。
例外であればいろいろな例外があってもCatch節で調べればすぐに何があったかわかるのですが、この例では例外ではないし戻り値もTrueかFalseだけなのでまず無理です。
これを解決するためにはこの方法をさらに発展させて戻り値を数値にします。0は成功、1は10文字以上書き込もうとしたために失敗、2は全角文字を書き込もうとしたので失敗とあらかじめ決めておけばボタンのClickイベント側では戻り値をみて何が起こったか知ることができます。ただし、実際にはこのような数値が大量にあるとあとで何番が何かわからなくなってしまったり、混乱を招く恐れがありますので、戻り値を単なる数値型ではなく自作の列挙型にします。
以下はその例です。
Private
Enum WriteLogError Success '正常終了 ValueTooLong '10文字以上書き込もうとした場合 WideStringNotAllowed '全角文字を書き込もうとした場合 End Enum |
Private
Sub Button1_Click(ByVal
sender As System.Object,
ByVal e As System.EventArgs)
Handles Button1.Click Try
If WriteLog("Button1_Clickを実行します。")
= WriteLogError.Success
Then Catch
ex As Exception End Sub |
Private Function
WriteLog(ByVal Value As
String) As
WriteLogError
'▼文字数チェック
'▼全角文字が混ざっていないかチェック '▼書き込み実行 strTime = Now.ToString("yyyy/MM/dd
HH:mm:ss ") Return WriteLogError.Success End Function |
■リスト6:列挙体を利用したカスタムエラーの扱い
今度は同じ問題を構造化例外処理によって解決してみます。問題の所在を考えてみると要するにTryが「例外」だけを対象にしているのでカスタムエラーが無視されていしまう点が根本原因です。
ここで、「自分で例外を作る」必要性がでてくるわけです。自分で例外を発生させるにはThrow(読み方:Throw = スロー)を使用します。次のようになります。
Private
Sub Button1_Click(ByVal
sender As System.Object,
ByVal e As System.EventArgs)
Handles Button1.Click Try Call WriteLog("Button1_Clickを実行します。") Call WriteLog("これはテストです。") Catch
ex As Exception End Sub |
Private Sub
WriteLog(ByVal Value As
String)
'▼文字数チェック
'▼全角文字が混ざっていないかチェック '▼書き込み実行 strTime = Now.ToString("yyyy/MM/dd
HH:mm:ss ") End Sub |
■リスト7:例外のスロー
いかがですか。これですと呼び出し側であるボタンのClickイベントは常識的なTry文を書いておくだけでよく、すべてはWriteLog側で制御することになってプログラムもすっきりします。
このサンプルにも登場しているようにThrowはその後に例外クラスのインスタンスを指定します。このインスタンスがTry文にCatchされます。例外にもいろいろあるので最もふさわしいとおもった例外をThrowすればよいのですが、適切な例外がない場合でシンプルにことを進めたい場合はこの例のようにApplicationException(読み方:ApplicationException = アプリケーションエクセプション)を使用します。ただし、今回の例では引数に関する間違いなのでArgumentException(読み方:ArgumentException = オーギュメントエクセプション)の方がふさわしいかもしれません。汎用的に使える主な例外を挙げておきます。
例外クラス | 読み方 | 説明 |
ArgumentException | オーギュメントエクセプション | 引数に間違いがあります。 |
InvalidOperationException | インバリドオペレイションエクセプション | クラスやメソッドの使用方法に間違いがあります。 |
NullReferenceException | ヌルリファレンスエクセプション | 変数の値がNothingです。 |
IndexOutOfRangeException | インデックスアウトオブレンジエクセプション | (主に配列やコレクションで)範囲外です。 |
TimeoutException | タイムアウトエクセプション | 時間切れです。 |
NotSupportedException | ノットサポーテッドエクセプション | 使用できないメソッド等です。 |
ApplicationException | アプリケーションエクセプション | その他のカスタムエラー。 |
さて、この例でボタンのClickイベント側でなぜWriteLogメソッドに失敗したのか調べようとしたらどういう方法があるでしょうか?
通常は例外の種類別にCatch節を用意しておけば自動的に区別できるのですが、今回は10文字以上書き込もうとした場合も全角文字を書き込もうとして場合もApplicationExceptionが発生するようになっているので例外の種類では区別できません。
今回の例でどうしても区別したいのでしたらAppicationExceptionのMessageプロパティ(読み方:Message = メッセージ)を使用することになります。しかし、この方法は全くお勧めできません。それでも一応サンプルを紹介しておきます。
Private
Sub Button1_Click(ByVal
sender As System.Object,
ByVal e As System.EventArgs)
Handles Button1.Click Try Call WriteLog("Button1_Clickを実行します。") Call WriteLog("これはテストです。") Catch ex As Exception
Select Case ex.Message
Case
"ログには全角文字は書き込めません。"
Case Else End Select End Try End Sub |
■リスト8:悪い方法による例外の識別。
このサンプルを見ればどうしてお勧めできないかわかるとおもいます。メッセージが1文字でも変ったら例外処理がうまく動かなくなってしまうのが最大の問題点です。それにプログラムが醜くなります。
そこで最後に既存の例外を利用するのではなく例外自体を自分で作ってしまう方法を紹介します。
まず、ログに10文字以上書きこもうとしたときのために例外ValueTooLongExceptionを作成します。
例外もクラスですから例外を作成するということはクラスを作成するということになります。そして、例外クラスを作成するには別のどれかの例外クラスを継承することになります。クラスの作成や継承についてはもっと後の機会に詳しく取り上げますので今回は細部にこだわらないようにお願いします。
なお例外クラスはかなりシンプルなクラスとなります。ValueTooLongExceptionのプログラムは次のようになります。
Public
Class ValueTooLongException Inherits ApplicationException
Public Sub New(ByVal
Message As String) End Class |
■リスト9:例外クラスの作成
ご覧のようにかなり短いプログラムです。内容はありませんが例外クラスは特に機能がなくてもよいのでこれで十分です。これをとりあえずはForm1のEnd Classの下、つまりコードの一番下に書いておけばよいです。自作のクラスをどこに書くべきかは別の機会に詳しく説明します。
これで、さきほどのプログラムの中でThrow New ApplicationException…としていたところでThrow New ValueTooLongException…と記述することができます。
同様にログに全角文字を書き込もうとした場合のために例外WideStringNotAllowdExceptionを作成しましょう。といってもプログラムはValueTooLongExceptionと同じです。
Public
Class WideStringNotAllowedException Inherits ApplicationException
Public Sub New(ByVal
Message As String) End Class |
■リスト10:例外クラスの作成
後はいままでの例外の知識でプログラムを完成させることができます。
念のために完成版を書いておきます。この完成版はVB2005のものですが、VB.NET2002およびVB.NET2003でも途中に「Windows フォームデザイナで生成されたコード」が挟まるだけで後は同じです。
Public Class Form1 |
Private
Sub Button1_Click(ByVal
sender As System.Object,
ByVal e As System.EventArgs)
Handles Button1.Click Try Call WriteLog("Button1_Clickを実行します。") Call WriteLog("これはテストです。")
Catch ex As ValueTooLongException
Catch ex As
WideStringNotAllowedException
Catch ex As Exception End Try End Sub |
Private
Sub WriteLog(ByVal
Value As String)
'▼文字数チェック
'▼全角文字が混ざっていないかチェック
'▼書き込み実行 strTime =
Now.ToString("yyyy/MM/dd
HH:mm:ss ") End Sub End Class |
Public
Class ValueTooLongException Inherits ApplicationException
Public Sub New(ByVal
Message As String) End Class |
Public
Class WideStringNotAllowedException Inherits ApplicationException
Public Sub New(ByVal
Message As String) End Class |
■リスト11:例外クラスを利用したカスタムエラー処理
今回は小さなテーマを長々と書いてしまいましたので簡単にまとめておきます。
要するに「自作のメソッドの中で発生した不都合な状況をどうすればよいか?」ということが問題点でした。
この「不都合な状況」がVBの例外であれば呼び出し元でTry文を使用すればよいだけですが、自分で定義したエラー、つまりカスタムエラーの場合にはTry文に無視されてしまうので、Try文にひっかけてもらうためには例外をThrowするというのが今回の最も重要な点です。
そして、例外を区別するために例外クラスを自分で作ることもできるのでした。また、構造化例外処理を使用しないでメソッドの戻り値などを工夫する手もあるのでした。
さて、あれもできるこれもできると書いてきましたが我々はどうするべきでしょうか?法律を丸暗記しただけでは司法試験に合格できないのと同じで、何ができるかわかっただけではなにをすべきかわかったことにはなりません。
ここでは私の姿勢を紹介します。私はメソッド側で例外をThrowさせるのが好みです。このとき小さいちょっとしたプログラムならばApplicationExceptionをThrowします。大きなプログラムの場合は自作の例外クラスを用意しますが、無計画に例外クラスを作っていくとそのうちに収拾がつかなくなってしまいますからある程度は計画的に例外クラスを作るようにします。
おそらく世の中の多くのプログラマが私と同じようなスタンスだとは思いますが、古風なプログラマの中には例外をThrowすることを嫌う人もいるようです。例外処理についてはプログラムが大規模になればなるほど一貫性のある方法が必要となってきますのでみなさんも普段のちょっとしたプログラムを作る時から例外について意識するようにしてみてください。