VB.NETの構造化例外処理と非構造化例外処理の話
どうせプログラミング言語でブログを書いても、基本的に既出だし、私が書いてもわかりにくいだけで皆さんのためにならないんだろうなと思います。
でも、今回の記事には自信があります。自身があるというのは、調べた中であまり見かけない話題だからです。
なぜ触れれらていないのか私にはわかりません。しかし、この機会にブログのアクセスアップ あっごめんなさい嘘です…
というわけで今回は、 VB.NET の構造化例外処理と非構造化例外処理について、色々と調べた結果をまとめてみます。
非構造化例外処理
まず非構造化例外処理とはなんぞやというところから話をしたいと思います。
非構造化例外処理とは、ざっくり言うと On Error ~
と Err
オブジェクトを用いた例外処理です。
Visual Basic がまだ .NET化する前、有名なところでいえば VB6 等の時代に使われた文法を踏襲し、 VB.NET での例外を処理できるように改造された仕組みです。
未だに Office などのマクロを組むために使用されている VBA は、 VB6 の構文がほぼそのまま残っているため、当時の仕様のまま使うことができます。
簡単な例を見れば、この構文の方向性がなんとなくわかると思います。
Module Module1 Sub Main() On Error GoTo ErrProc1 Debug.Print("A通りますよー") On Error GoTo ErrProc2 Debug.Print("B通りますよー") On Error GoTo ErrProc3 Debug.Print("C通りますよー") Err.Raise(5) On Error Resume Next Debug.Print("D通りますよー") On Error GoTo 0 Debug.Print("おわり") Exit Sub ErrProc1: Debug.Print("ErrProc1でっせ") Exit Sub ErrProc2: Debug.Print("ErrProc2でっせ" & vbNewLine & Err.Number & ":" & Err.Description) ErrProc3: Debug.Print("ErrProc3でっせ") Resume Next End Sub End Module
要するに、 エラー処理をラベルで指示する というものです。この構文は、現在ではほぼ 禁忌 とされています。
なぜ禁忌かというと、まず一点は、スパゲッティコードになるから。あからさまに GoTo
を使う構文なので、それはまあ仕方ありません。
そしてもう一点。これは、世にも恐ろしい Resume
という機能があるからです。
Resume
は、エラーが起きた場所から処理をやり直すという意味の命令です。 エラーが起きても、元の正常処理を続きから再開できるというとても強力で忌々しい命令です。
確かに、使ってもさほど影響がない部分というのはもちろんありますが、闇雲に多用してしまうとあとからどうしようもないコードが生まれてしまうので、現在では意図的に避けられています。
詳しい説明はここでは省きますので、上記のコードがどのように動作するかを簡単にまとめます。
- ケース1
- エラー場所 -
MsgBox "A通りますよー"
の直後 - 出力結果
A通りますよー
ErrProc1でっせ
- エラー場所 -
- ケース2
- エラー場所 -
MsgBox "B通りますよー"
の直後 - 出力結果
A通りますよー
B通りますよー
ErrProc2でっせ
5:プロシージャの呼び出し、または引数が不正です。
ErrProc3でっせ
C通りますよー
D通りますよー
おわり
- エラー場所 -
- ケース3 -
MsgBox "C通りますよー"
の直後- 出力結果
A通りますよー
B通りますよー
C通りますよー
ErrProc3でっせ
D通りますよー
おわり
- 出力結果
- ケース4 -
MsgBox "D通りますよー"
の直後 A通りますよー
B通りますよー C通りますよー
D通りますよー
おわり
※なお、ケース2中のエラーメッセージは、例外の種類で異なります。
構造化例外処理
On Error
に対して、もっとオブジェクトな指向の例外処理があります。おなじみ Try
Catch
Finally
です。
「構造化例外処理」の名の通り、文法の構造的に例外処理が保証される仕組みです。
これは完全に C# の流れに押されて出現したもので、 VB6 などの時代には存在しませんでした。 (C# に押されたというか .NET 化した影響?)
今では Java や ECMAScript (またはその派生) などあらゆる言語に同様の構文が定義されており、非常に扱いやすい構文だと思います。
Module Module1 Sub Main() Try Debug.Print("A通りますよー") Try Debug.Print("B通りますよー") Try Debug.Print("C通りますよー") Try Debug.Print("D通りますよー") Catch Throw New Exception() End Try Debug.Print("おわり") Catch Debug.Print("ErrProc3でっせ") Throw New Exception() End Try Catch ex As Exception Debug.Print("ErrProc2でっせ" & vbNewLine & ex.Message) End Try Catch Debug.Print("ErrProc1でっせ") Finally Debug.Print("おわり") End Try End Sub End Module
構造化するということは、つまりブロックをネストするという意味ですので、先程のコードよりかなりインデントが深くなっています。
また、非構造化例外処理の Resume
機能に該当する機能は一切ないですし、任意に例外処理の実行順を入れ替えることもできません。
一見できることが制限されたように思いますが、この制限があるからこそ、スパゲッティコードが生まれにくいという恩恵をうけることができます。
- ケース1
- エラー場所 -
MsgBox "A通りますよー"
の直後 - 出力結果
A通りますよー
ErrProc1でっせ
おわり
- エラー場所 -
- ケース2
- エラー場所 -
MsgBox "B通りますよー"
の直後 - 出力結果
A通りますよー
B通りますよー
ErrProc2でっせ
プロシージャの呼び出し、または引数が不正です。
おわり
- エラー場所 -
- ケース3
- エラー場所 -
MsgBox "C通りますよー"
の直後 - 出力結果
A通りますよー
B通りますよー
C通りますよー
ErrProc3でっせ
ErrProc2でっせ
プロシージャの呼び出し、または引数が不正です。
おわり
- エラー場所 -
- ケース4
- エラー場所 -
MsgBox "D通りますよー"
の直後 - 出力結果
A通りますよー
B通りますよー
C通りますよー
D通りますよー
ErrProc3でっせ
ErrProc2でっせ
プロシージャの呼び出し、または引数が不正です。
おわり
- エラー場所 -
※なお、ケース2,3,4中のエラーメッセージは、例外の種類で異なります。
構造化例外処理と非構造化例外処理の共存
これら2つの文法は、お互いに排他的に使わなければなりません。噛み砕いて言えば、 同時には使えません 。
厳密には、同じプロシージャ内、いわば一つのメソッド内に両方記述してはならないという言語仕様になっています。
たとえ Try
Finally
文のみを使用して Catch
を使っていなくても、 On Error
を書いた時点でコンパイルエラーになります。
本当に?
あまり知られていない構造化例外処理
上記では、構造化例外処理として Try
Catch
Finally
を紹介しました。
最近まで、私はこれ以外の構造化例外処理を知りませんでした。
次の記事では、もう一つ、構造化例外処理を利用したコードを紹介しています。
少々古めの記事ですが、読んでみましょう。私は自分の目を疑いましたよ。
VB.NET には Using
という便利な構文があります。 IDisposable
インターフェイスを実装したクラスのインスタンスに対し、構文上でその破棄を保証するものです。
これも、 VB6 や VBA には無い機能です。
この構文は、コンパイラを通すとなんと、 Try
Finally
句へ変貌するというのです 。嘘みたい。
実際、 C# にも同様の機能を持つ using
というステートメントがありますが、このリファレンスには次のような説明がされています。
using ステートメント (C# リファレンス) | Microsoft Docs
using ステートメントを使うと、オブジェクトでのメソッドの呼び出し中に例外が発生した場合でも Dispose が必ず呼び出されます。 オブジェクトを try ブロックに配置し、finally ブロックで Dispose を呼び出しても、同じ結果が得られます。実際には、コンパイラは using ステートメントをこのように変換します。 前のコード例は、コンパイル時に次のコードに展開されます (オブジェクトのスコープの範囲を定義する中かっこが加えられています)。
{ Font font1 = new Font("Arial", 10.0f); try { byte charset = font1.GdiCharSet; } finally { if (font1 != null) ((IDisposable)font1).Dispose(); } }
C# がこのようにコンパイルするのですから、 VB も必然的に同じような動作になるのでしょうね。Try
Finally
を使うのですから、 Using
も構造化例外処理の仲間となっていまいます。
何がおかしいのか
先程の私の紹介でもそうでしたが、改めて言語仕様やリファレンスをよく読むと、構造化例外に Using
が出てきていません。
むしろ、
Finally セクション内のコードは、Catch ブロック内のコードが実行されたかどうかに関係なく、常に最後 (エラー処理ブロックがスコープを失う直前) に実行されます。 Finally セクションには、クリーンアップ コード (ファイルを閉じたりオブジェクトを解放したりするコードなど) を配置します。 例外をキャッチする必要はないけれども、リソースをクリーンアップする必要がある場合、Finally セクションではなく、Using ステートメントを使用します。 詳細については、「Using ステートメント (Visual Basic)」を参照してください。
とあり、 Using
は構造化例外ではないかのような書き方がされています 。
じゃあ、 On Error
と Using
は同時に使えるのか? という疑問が生まれます。
こうなったら、実際どうなるのか試すしかありませんね。
テストコード
Module Module1 Sub Use_Try_Finally() Dim objA As DisposableObject = New DisposableObject("A") Try Dim objB As DisposableObject = New DisposableObject("B") Try Dim objC As DisposableObject = New DisposableObject("C") Try Debug.Print("すべてインスタンス化されました") Catch ex As Exception Debug.Print(ex.Message) Finally objC.Dispose() End Try Catch ex As Exception Debug.Print(ex.Message) Finally objB.Dispose() End Try Catch ex As Exception Debug.Print(ex.Message) Finally objA.Dispose() End Try Debug.Print("すべてDisposeされました") End Sub Sub Use_Using_OnError() On Error Resume Next Using objA As DisposableObject = New DisposableObject("A") Using objB As DisposableObject = New DisposableObject("B") Using objC As DisposableObject = New DisposableObject("C") Debug.Print("すべてインスタンス化されました") If Err.Number <> 0 Then Debug.Print(Err.Description) End Using If Err.Number <> 0 Then Debug.Print(Err.Description) End Using If Err.Number <> 0 Then Debug.Print(Err.Description) End Using Debug.Print("すべてDisposeされました") End Sub Sub Main() Debug.Print("--- Use_Try_Finally -------------") Use_Try_Finally() Debug.Print("--- Use_Using_OnError -----------") Use_Using_OnError() End Sub End Module
え?これだけじゃ実行できない?
そうです。 DisposableObject
を実装します。
なんで別クラスにしたかといえば、このクラスのほうが短くかけるので、例外とかの挙動を書きやすいかなと思ったんですね。
うん。察してくれ。
通常の挙動を見てみる
では、まず通常の挙動から。 DisposableObject
の実装は次のとおりです。
''' <summary> IDisposeableを継承するオブジェクト </summary> Public Class DisposableObject Implements IDisposable ''' <summary> インスタンスの名前 </summary> Private myName As String ''' <summary> Disposeがすでに呼ばれたか </summary> Private disposedValue As Boolean ''' <summary> DisposableObject をインスタンス化します </summary> ''' <param name="name">インスタンスのわかり易い名前</param> Public Sub New(name As String) myName = name Debug.Print(myName & "がインスタンス化されました") End Sub 'Privateにして隠蔽 Private Sub New() End Sub '内部用Disposeメソッド Protected Overridable Sub Dispose(disposing As Boolean) If Not disposedValue Then If disposing Then Debug.Print(myName & "がDisposeされました") End If End If disposedValue = True End Sub ''' <summary> リソースを破棄します </summary> Public Sub Dispose() Implements IDisposable.Dispose Dispose(True) End Sub End Class
インスタンス時にインスタンスの名前を表示して、Disposeするときも表示するだけです。
これを実行すると、次のような結果が得られます。
— Use_Try_Finally ————-
Aがインスタンス化されました
Bがインスタンス化されました
Cがインスタンス化されました
すべてインスタンス化されました
CがDisposeされました
BがDisposeされました
AがDisposeされました
すべてDisposeされました
— Use_Using_OnError ———–
Aがインスタンス化されました
Bがインスタンス化されました
Cがインスタンス化されました
すべてインスタンス化されました
CがDisposeされました
BがDisposeされました
AがDisposeされました
すべてDisposeされました
普通ですね。
例外時の挙動を見てみる
次に例外を発生させてみます。 DisposableObject
の実装は次のとおりです。
''' <summary> IDisposeableを継承するオブジェクト </summary> Public Class DisposableObject Implements IDisposable ''' <summary> インスタンスの名前 </summary> Private myName As String ''' <summary> Disposeがすでに呼ばれたか </summary> Private disposedValue As Boolean ''' <summary> DisposableObject をインスタンス化します </summary> ''' <param name="name">インスタンスのわかり易い名前</param> Public Sub New(name As String) myName = name 'Cのときは例外 If myName = "C" Then Throw New Exception("例外!!!!") Debug.Print(myName & "がインスタンス化されました") End Sub 'Privateにして隠蔽 Private Sub New() End Sub '内部用Disposeメソッド Protected Overridable Sub Dispose(disposing As Boolean) If Not disposedValue Then If disposing Then Debug.Print(myName & "がDisposeされました") End If End If disposedValue = True End Sub ''' <summary> リソースを破棄します </summary> Public Sub Dispose() Implements IDisposable.Dispose Dispose(True) End Sub End Class
インスタンスを作るときに名前が「C」だった場合、エラーになるというだけの分岐が一つ増えただけです。
これでどうなるでしょうか。
— Use_Try_Finally ————-
Aがインスタンス化されました
Bがインスタンス化されました
例外がスローされました: ‘System.Exception’ (vb_using_test.exe の中)
例外がスローされました: ‘System.Exception’ (vb_using_test.exe の中)
例外!!!!
BがDisposeされました
AがDisposeされました
すべてDisposeされました
— Use_Using_OnError ———–
Aがインスタンス化されました
Bがインスタンス化されました
BがDisposeされました
AがDisposeされました
すべてDisposeされました
例外の情報が・・・消えた!
Try-Catch
のほうは、問題なく例外を捕捉できています。
OnError Resume Next
と Using
のほうは、エラーを出すロジックはいくつもあるというのに、それらのどこでも例外の情報が捕捉できていません。
これは一大事です。
例外を補足できるようにする
後者で例外を補足できるようにするためには、 Use_Using_OnError
を次のように修正する必要があります。
Sub Use_Using_OnError() On Error Resume Next Using objA As DisposableObject = New DisposableObject("A") Using objB As DisposableObject = New DisposableObject("B") Using objC As DisposableObject = New DisposableObject("C") Debug.Print("すべてインスタンス化されました") If Err.Number <> 0 Then Debug.Print(Err.Description) End Using If Err.Number <> 0 Then Debug.Print(Err.Description) End Using If Err.Number <> 0 Then Debug.Print(Err.Description) End Using If Err.Number <> 0 Then Debug.Print(Err.Description) Debug.Print("すべてDisposeされました") End Sub
先ほどとのコードの違いは、 最後の End Using
のあとに例外処理を入れているだけ です。
これで、どのような結果になるでしょうか。
— Use_Using_OnError ———–
Aがインスタンス化されました
Bがインスタンス化されました
BがDisposeされました
AがDisposeされました
例外!!!!
すべてDisposeされました
やっと例外が取得できました。
先ほどのコードを比較すると、 Resume Next
と Using
を併用したコード内で例外が発生したとき、一番外側の Using
の外側でないと取得できないという結果になりました。
なお、 On Error Goto
を使用しても、 Resume Next
同様、一番外側の Using
の外側まで抜けたあと GoTo
先へジャンプするようになっています。
まとめ
結果的に何が言いたいかといえば、
Using
は 構造化例外処理 であり、 On Error
と併用できないはずが なぜかできでしまう
ということです。そして、
併用してしまうと 必ず一番外側の Using
まで抜けてしまう
のです。さらに、
この情報が どのリファレンスにも明記されていない のです 。
というか、ネットで探しても全く出てこない。
英語が読めないので英語のサイトは全く読んでいないのですが、少なからず日本語のサイトではこの現象を取り上げているサイトが少ない。というかない。
一応、私が実際に出会った問題として書いておきますが、この現象について参考になるサイトを知っている方がいたら、ぜひ教えていただきたいと思います。
長文になってしまいましたが、最後までお付き合い頂きありがとうございました。
ちなみに私は Form.ShowDialog()
するとき using
使ってるコード見ると指が勝手に Delete キーを あっごめんなさい嘘です…