VB6で(なんちゃって)構造化例外処理

最近はずっとVB6*1で仕事をしているので、できない事が多くてイライラしてくる。

だからと言って、いい加減な物を作るわけにはいかないので、VB6でもできる限りの事をやる為に色々とフレームワーク的なものを作り始めた。これを使えば一定の品質を保てるって程度のやつ。

まずはVB6のしょぼいエラー処理の機能をもう少しましにする機能を作った。

やりたい事は

  • 一貫したエラー処理
  • 呼び出し履歴が参照できる(スタックトレース)
  • エラーに追加情報を載せる

これらを実現する為に、まず例外クラスから作ることにする。

その前にこのクラスが必要なので、載せとく。単なる文字列バッファクラスね。

clsStringBuilder.cls

Option Explicit
'#---------------------------------------------------------------------------
'# クラス名:clsStringBuilder
'# 概要  :文字列バッファクラス
'#---------------------------------------------------------------------------

Private m_strBuffer As String

Private Sub Class_Initialize()
    m_strBuffer = ""
End Sub

'#---------------------------------------------------------------------------
'# 関数名:Append
'# 機能  :バッファに文字列を追加します。
'#
'# 引数  :strValue:   文字列
'#       :varParam0:  パラメータ1
'#       :varParam1:  パラメータ2
'#       :varParam2:  パラメータ3
'#       :varParam3:  パラメータ4
'#       :varParam4:  パラメータ5
'#
'# 戻り値:自分自身
'# 備考 :文字列に「{番号}」という形式の文字列を入れると、その部分が
'#         引数で指定した番号のパラメータと置き換わります。
'#         例: objBuffer.Append("my name is {0}.", "hoge")
'#              ' 「my name is hoge.」という文字列が追加される。
'#---------------------------------------------------------------------------
Public Function Append( _
    Optional ByRef strValue As String = "", _
    Optional ByRef varParam0 As Variant, _
    Optional ByRef varParam1 As Variant, _
    Optional ByRef varParam2 As Variant, _
    Optional ByRef varParam3 As Variant, _
    Optional ByRef varParam4 As Variant) As clsStringBuilder

    Dim i As Integer
    Dim varParams(4) As Variant
    If Not IsMissing(varParam0) Then varParams(0) = varParam0
    If Not IsMissing(varParam1) Then varParams(1) = varParam1
    If Not IsMissing(varParam2) Then varParams(2) = varParam2
    If Not IsMissing(varParam3) Then varParams(3) = varParam3
    If Not IsMissing(varParam4) Then varParams(4) = varParam4
    
    For i = LBound(varParams) To UBound(varParams)
        strValue = Replace(strValue, "{" & CStr(i) & "}", varParams(i))
    Next
    
    m_strBuffer = m_strBuffer & strValue
    
    Set Append = Me
End Function

'#---------------------------------------------------------------------------
'# 関数名:AppendLine
'# 機能  :バッファに文字列を追加して、改行を挿入します。
'#
'# 引数  :strValue:   文字列
'#       :varParam0:  パラメータ1
'#       :varParam1:  パラメータ2
'#       :varParam2:  パラメータ3
'#       :varParam3:  パラメータ4
'#       :varParam4:  パラメータ5
'#
'# 戻り値:自分自身
'#---------------------------------------------------------------------------
Public Function AppendLine( _
    Optional ByRef strValue As String = "", _
    Optional ByRef varParam0 As Variant, _
    Optional ByRef varParam1 As Variant, _
    Optional ByRef varParam2 As Variant, _
    Optional ByRef varParam3 As Variant, _
    Optional ByRef varParam4 As Variant) As clsStringBuilder
    
    Set AppendLine = Append( _
        strValue & vbCrLf, _
        varParam0, varParam1, varParam2, varParam3, varParam4 _
    )
End Function

'#---------------------------------------------------------------------------
'# 関数名:Clear
'# 機能  :バッファにある文字列をクリアします。
'#
'#---------------------------------------------------------------------------
Public Sub Clear()
    m_strBuffer = ""
End Sub

'#---------------------------------------------------------------------------
'# 関数名:ToString
'# 機能  :現在のバッファの文字列を取得します。
'#
'# 戻り値:文字列
'#---------------------------------------------------------------------------
Public Function ToString() As String
    ToString = m_strBuffer
End Function

'#---------------------------------------------------------------------------
'# プロパティ名:Length
'# 機能        :現在のバッファの文字列長さを取得します。
'#
'# 戻り値:文字列の長さ
'#---------------------------------------------------------------------------
Public Property Get Length() As Long
    Length = Len(m_strBuffer)
End Property

実装

で、例外クラスが以下

clsException.cls

Option Explicit
'#---------------------------------------------------------------------------
'# クラス名:clsException
'# 概要  :ユーザ定義のエラーの情報を格納するクラス
'#---------------------------------------------------------------------------

Private m_lngNumber As Long
Private m_strSource As String
Private m_strDescription As String

Private m_objInnerException As clsException

'#---------------------------------------------------------------------------
'# プロパティ名:Number
'# 機能  :エラー番号を取得、設定します。
'#
'# 戻り値:エラー番号
'#---------------------------------------------------------------------------
Public Property Let Number(ByRef lngNumber As Long)
    m_lngNumber = lngNumber
End Property
Public Property Get Number() As Long
    Number = m_lngNumber
End Property

'#---------------------------------------------------------------------------
'# プロパティ名:Source
'# 機能  :エラーのソースを取得、設定します。
'#
'# 戻り値:エラーのソース
'#---------------------------------------------------------------------------
Public Property Let Source(ByRef strSource As String)
    m_strSource = strSource
End Property
Public Property Get Source() As String
    Source = m_strSource
End Property

'#---------------------------------------------------------------------------
'# プロパティ名:Description
'# 機能  :エラーの詳細を取得、設定します。
'#
'# 戻り値:エラーの詳細
'#---------------------------------------------------------------------------
Public Property Let Description(ByRef strDescription As String)
    m_strDescription = strDescription
End Property
Public Property Get Description() As String
    Description = m_strDescription
End Property

'#---------------------------------------------------------------------------
'# プロパティ名:InnerException
'# 機能  :内部エラーオブジェクトを取得、設定します。
'#
'# 戻り値:エラーオブジェクト
'#---------------------------------------------------------------------------
Public Property Set InnerException(ByRef objInnerException As clsException)
    Set m_objInnerException = objInnerException
End Property
Public Property Get InnerException() As clsException
    Set InnerException = m_objInnerException
End Property

Private Sub Class_Initialize()
    m_lngNumber = 0
    m_strSource = ""
    m_strDescription = ""
End Sub

'#---------------------------------------------------------------------------
'# 関数名:ToString
'# 機能  :現在のオブジェクトを文字列に変換します。
'#
'# 戻り値:文字列
'#---------------------------------------------------------------------------
Public Function ToString() As String
    Dim objExp As clsException
    
    Set objExp = Me
    
    With New clsStringBuilder
        .AppendLine "エラー番号:[{0}]", Me.Number
        .AppendLine
        .AppendLine Me.Description
        
        Do Until objExp Is Nothing
            .AppendLine "  at {0}", objExp.Source
            
            Set objExp = objExp.InnerException
        Loop
        
        ToString = .ToString
    End With
End Function

あとはこのクラスを使って色々とエラーを発生させるためのモジュールを定義する。

modException.bas

Option Explicit

' 不正な操作
Public Const InvalidOperation = 1000
' 不正な引数
Public Const InvalidArgument = 1001
' インデックスが範囲外
Public Const IndexOutOfRange = 1002
' 未実装
Public Const NotImplementation = 1003

Private m_objLastException As clsException
    
'#---------------------------------------------------------------------------
'# 関数名:modException_Throw
'# 機能  :指定したエラー内容で例外をスローします。
'#
'# 引数  :lngNumber:          エラー番号
'#       :strSource:          エラーソース
'#       :strDescription:     エラーの詳細
'#---------------------------------------------------------------------------
Public Sub modException_Throw( _
    ByRef lngNumber As Long, _
    ByRef strSource As String, _
    ByRef strDescription As String)
    
    Dim objException As New clsException
    
    With objException
        .Number = lngNumber
        .Source = strSource
        .Description = strDescription
        
        If Not m_objLastException Is Nothing Then
            Set objException.InnerException = m_objLastException
        End If
    End With
    
    Set m_objLastException = objException
    
    Call Err.Raise(lngNumber, strSource, strDescription)
End Sub

'#---------------------------------------------------------------------------
'# 関数名:modException_Throw
'# 機能  :現在のエラーを再スローします。
'#
'# 引数  :strSource:      ソース
'#---------------------------------------------------------------------------
Public Sub modException_Rethrow( _
    Optional ByRef strSource As Variant)
    
    If Err.Number <> 0 Then
        Call modException_Throw(Err.Number, _
            IIf(IsMissing(strSource), Err.Source, strSource), Err.Description
        )
    End If
End Sub

'#---------------------------------------------------------------------------
'# 関数名:modException_GetLastException
'# 機能  :最後にスローされたエラーを取得します。
'#
'# 戻り値:エラー内容
'#---------------------------------------------------------------------------
Public Function modException_GetLastException() As clsException
    If Not m_objLastException Is Nothing Then
        Set modException_GetLastException = m_objLastException
    ' 標準のエラーで代替してやる
    ElseIf Err.Number <> 0 Then
        Dim objException As New clsException
        
        With objException
            .Number = Err.Number
            .Source = Err.Source
            .Description = Err.Description
        End With
        
        Set modException_GetLastException = objException
    End If
End Function

'#---------------------------------------------------------------------------
'# 関数名:modException_ClearLastException
'# 機能  :最後にスローされたエラーを消去します。
'#
'#---------------------------------------------------------------------------
Public Sub modException_ClearLastException()
    Set m_objLastException = Nothing
    
    Call Err.Clear
End Sub

'#---------------------------------------------------------------------------
'# 関数名:modException_ExistsException
'# 機能  :エラーがスローされたかどうかを調べます。
'#
'# 戻り値:スローされていればTrue
'#---------------------------------------------------------------------------
Public Function modException_ExistsException() As Boolean
    modException_ExsitsException = Not modException_GetLastException() Is Nothing
End Function

使い方

使い方としてはこんな感じ

エラーを発生させてキャッチ
Sub Main()
    ' 最初に必ず呼び出す
    Call modException_ClearLastException()

On Error Goto EXCEPT:
    Call Execute
    
    Exit Sub
EXCEPT:
    With modException_GetLastException
        ' スタックトレースを出力
        Debug.Print .ToString
        
        MsgBox .Description
    End With
End Sub

Sub Execute()
    Call modException_Throw(666, "Execute", "エラー発生")
End Sub
ランタイムエラーをキャッチして、再スロー

Sub Main()
    ' 最初に必ず呼び出す
    Call modException_ClearLastException()

On Error Goto EXCEPT:
    Call Execute1
    
    Exit Sub
EXCEPT:
    With modException_GetLastException
        ' スタックトレースを出力
        Debug.Print .ToString
        
        MsgBox .Description
    End With
End Sub

Sub Execute1()
On Error Goto EXCEPT:
    Call Execute2
    
    Exit Sub
EXCEPT:
    ' スルー
    Call modException_Rethrow("Execute1")
End Sub

Sub Execute2()
On Error Goto EXCEPT:
    Call Execute3
    
    Exit Sub
EXCEPT:
    ' スルー
    Call modException_Rethrow("Execute2")
End Sub

Sub Execute3()
On Error Goto EXCEPT:
    ' なんかエラーが発生する処理
    
    Exit Sub
EXCEPT:
    ' 再スロー
    Call modException_Rethrow("Execute3")
End Sub

手動でスタックトレースを積み上げていかなければいけないので、コード量は増えちゃうけど必ず役に立つ時が来るので、やっときたい。
たいした事はやっていないけど、プロジェクトでエラー処理を共通化できるので、それなりに効果はあるはず。

*1:正確にはAccess VBAだけど