手続き型音楽の日常

関数型音楽に乗り換えたい

VB.NETの構造化例外処理と非構造化例外処理の話

どうせプログラミング言語でブログを書いても、基本的に既出だし、私が書いてもわかりにくいだけで皆さんのためにならないんだろうなと思います。

でも、今回の記事には自信があります。自身があるというのは、調べた中であまり見かけない話題だからです。

なぜ触れれらていないのか私にはわかりません。しかし、この機会にブログのアクセスアップ あっごめんなさい嘘です…

というわけで今回は、 VB.NET の構造化例外処理と非構造化例外処理について、色々と調べた結果をまとめてみます。

非構造化例外処理

10.10.2 非構造化例外処理ステートメント

まず非構造化例外処理とはなんぞやというところから話をしたいと思います。

非構造化例外処理とは、ざっくり言うと 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中のエラーメッセージは、例外の種類で異なります。

構造化例外処理

10.10.1 構造化例外処理ステートメント

On Error に対して、もっとオブジェクトな指向の例外処理があります。おなじみ Try Catch Finally です。

「構造化例外処理」の名の通り、文法の構造的に例外処理が保証される仕組みです。

これは完全に C# の流れに押されて出現したもので、 VB6 などの時代には存在しませんでした。 (C# に押されたというか .NET 化した影響?)

今では JavaECMAScript (またはその派生) などあらゆる言語に同様の構文が定義されており、非常に扱いやすい構文だと思います。

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 を書いた時点でコンパイルエラーになります。

例外処理の概要 (Visual Basic)

本当に?

あまり知られていない構造化例外処理

上記では、構造化例外処理として Try Catch Finally を紹介しました。

最近まで、私はこれ以外の構造化例外処理を知りませんでした。

次の記事では、もう一つ、構造化例外処理を利用したコードを紹介しています。

code.msdn.microsoft.com

少々古めの記事ですが、読んでみましょう。私は自分の目を疑いましたよ。

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 ErrorUsing は同時に使えるのか? という疑問が生まれます。

こうなったら、実際どうなるのか試すしかありませんね。

テストコード

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 NextUsing のほうは、エラーを出すロジックはいくつもあるというのに、それらのどこでも例外の情報が捕捉できていません。

これは一大事です。

例外を補足できるようにする

後者で例外を補足できるようにするためには、 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 NextUsing を併用したコード内で例外が発生したとき、一番外側の Using の外側でないと取得できないという結果になりました。

なお、 On Error Goto を使用しても、 Resume Next 同様、一番外側の Using の外側まで抜けたあと GoTo 先へジャンプするようになっています。

まとめ

結果的に何が言いたいかといえば、

Using 構造化例外処理 であり、 On Error と併用できないはずが なぜかできでしまう

ということです。そして、

併用してしまうと 必ず一番外側の Using まで抜けてしまう

のです。さらに、

この情報が どのリファレンスにも明記されていない のです

というか、ネットで探しても全く出てこない。

英語が読めないので英語のサイトは全く読んでいないのですが、少なからず日本語のサイトではこの現象を取り上げているサイトが少ない。というかない。

一応、私が実際に出会った問題として書いておきますが、この現象について参考になるサイトを知っている方がいたら、ぜひ教えていただきたいと思います。

長文になってしまいましたが、最後までお付き合い頂きありがとうございました。

ちなみに私は Form.ShowDialog() するとき using 使ってるコード見ると指が勝手に Delete キーを あっごめんなさい嘘です…