Excel VBAをクラスを使って改善しよう: スタックトレースログ出力自動化
開発経緯
VBAでは実行時エラーが発生した場合、VBEから呼び出し履歴(CTRL+L)でスタックトレースの確認が可能ですが、これは中断時しか確認することが出来ません。
ですので、各プロシージャ呼び出しの開始と終了でログ出力を行うことがあるかと思います。
Public Sub レガシーなスタックトレースログ出力()
Debug.Print "レガシーなスタックトレースログ出力() " & ": 開始 """
'何かの処理を行う
Debug.Print "レガシーなスタックトレースログ出力() " & ": 終了 """
End Sub
かなり簡易な実装で、関数を定数にするとかログ用の共通プロシージャを作ったほうが良いとか色々とツッコミはあるかと思いますが、骨子はプロシージャに1つずつログ埋め込みが必要といういうことです。
別にこれでも悪くはないのですが、以下の点で課題があります。
- プロシージャ名を変更したらログ出力も修正が必要
- ログ出力し忘れても気が付かない、または面倒であえてやらなくなる
- プロシージャなどの小さいレベルでも実装するのか・しないのか判断に迷う
これらの課題に対して、どうにかできないかを考えてみました。
解決の糸口はCallByName
CallByNameはクラスのメソッドやプロパティを文字列で指定して実行することが出来ます。
多少のパフォーマンス劣化にはなりますが、遅ければ使う箇所を少なくすればよいだけです。
発想としては、CallByNameの前後にログ出力する仕組みを作った標準モジュールを作成すればよい、という単純なものです。
ただし、単純であることと簡単であることは異なるため、実装はそれなりに苦労しました。
利用する側はCallByNameとそれほど違いなく利用できます。
早速実装を見ていきましょう。
利用する側のコード
まずは利用する側のコードを見ていきます。このほうがステップを追っていけるので今回は分かりやすいかと思います。
Public Sub Test()
Debug.Print CallGetPropertyByName(ThisWorkbook, "Name")
Debug.Print TypeName(CallGetPropertyByName(ThisWorkbook, "WorkSheets"))
Debug.Print CallGetPropertyByName(CallGetPropertyByName(ThisWorkbook, "WorkSheets", "Sheet1"), "Name")
End Sub
実行結果は以下の通り。
2023/09/15 16:07:30.452 START: Workbook#Name(VbGet)
2023/09/15 16:07:30.454 FINISH: Workbook#Name(VbGet)
ログ出力.xlsm
2023/09/15 16:07:30.456 START: Workbook#WorkSheets(VbGet)
2023/09/15 16:07:30.456 FINISH: Workbook#WorkSheets(VbGet)
Sheets
2023/09/15 16:07:30.458 START: Workbook#WorkSheets(VbGet)
2023/09/15 16:07:30.459 FINISH: Workbook#WorkSheets(VbGet)
2023/09/15 16:07:30.460 START: Worksheet#Name(VbGet)
2023/09/15 16:07:30.460 FINISH: Worksheet#Name(VbGet)
Sheet1
CallGetPropertyByName は値を取得するプロパティに使います。
使い方はCallByNameとほぼ同様で、CallByNameの第3引数であるVbCallType別にプロシージャにしているだけです。今回はVbGetを指定した時と同様の動作になります。
CallByNameと異なるのは、実行前と後にログが出力されることです。
YYYY/MM/DD hh:mm:ss.fff START: クラス型名#メソッド・プロパティ名(実行タイプ)
YYYY/MM/DD hh:mm:ss.fff FINISH: クラス型名#メソッド・プロパティ名(実行タイプ)
また、可変長引数は10個までとしています。
他に以下のプロシージャを用意しました。
CallSubByName
CallFunctionByName
CallSetPropertyByName
CallLetPropertyByName
メソッドは戻り値がないSubプロシージャと戻り値のあるFunctionプロシージャを分けて実装しています。
この辺りは後でまとめてテストした内容を確認していきます。
CallUtilモジュール
それではまずは実行本体の実装を見ていきます。
かなり長いので覚悟してください。
Option Explicit
Option Private Module
Public Function CallFunctionByName(pObj As Object, pMethodName As String, Optional pArgs1 As Variant, Optional pArgs2 As Variant, Optional pArgs3 As Variant, Optional pArgs4 As Variant, Optional pArgs5 As Variant, Optional pArgs6 As Variant, Optional pArgs7 As Variant, Optional pArgs8 As Variant, Optional pArgs9 As Variant, Optional pArgs10 As Variant) As Variant
Dim vParams As CallUtilParameter
Set vParams = CreateParamter(pArgs1, pArgs2, pArgs3, pArgs4, pArgs5, pArgs6, pArgs7, pArgs8, pArgs9, pArgs10)
Dim vOutValue As CallUtilValue
Set vOutValue = CallByNameEx(pObj, pMethodName, VbMethod, vParams)
If vOutValue.IsObject Then
Set CallFunctionByName = vOutValue.Value
Else
CallFunctionByName = vOutValue.Value
End If
End Function
Public Sub CallSubByName(pObj As Object, pMethodName As String, Optional pArgs1 As Variant, Optional pArgs2 As Variant, Optional pArgs3 As Variant, Optional pArgs4 As Variant, Optional pArgs5 As Variant, Optional pArgs6 As Variant, Optional pArgs7 As Variant, Optional pArgs8 As Variant, Optional pArgs9 As Variant, Optional pArgs10 As Variant)
Dim vParams As CallUtilParameter
Set vParams = CreateParamter(pArgs1, pArgs2, pArgs3, pArgs4, pArgs5, pArgs6, pArgs7, pArgs8, pArgs9, pArgs10)
Call CallByNameEx(pObj, pMethodName, VbMethod, vParams)
End Sub
Public Function CallGetPropertyByName(pObj As Object, pMethodName As String, Optional pArgs1 As Variant, Optional pArgs2 As Variant, Optional pArgs3 As Variant, Optional pArgs4 As Variant, Optional pArgs5 As Variant, Optional pArgs6 As Variant, Optional pArgs7 As Variant, Optional pArgs8 As Variant, Optional pArgs9 As Variant, Optional pArgs10 As Variant) As Variant
Dim vParams As CallUtilParameter
Set vParams = CreateParamter(pArgs1, pArgs2, pArgs3, pArgs4, pArgs5, pArgs6, pArgs7, pArgs8, pArgs9, pArgs10)
Dim vOutValue As CallUtilValue
Set vOutValue = CallByNameEx(pObj, pMethodName, VbGet, vParams)
If vOutValue.IsObject Then
Set CallGetPropertyByName = vOutValue.Value
Else
CallGetPropertyByName = vOutValue.Value
End If
End Function
Public Sub CallLetPropertyByName(pObj As Object, pMethodName As String, Optional pArgs1 As Variant, Optional pArgs2 As Variant, Optional pArgs3 As Variant, Optional pArgs4 As Variant, Optional pArgs5 As Variant, Optional pArgs6 As Variant, Optional pArgs7 As Variant, Optional pArgs8 As Variant, Optional pArgs9 As Variant, Optional pArgs10 As Variant)
Dim vParams As CallUtilParameter
Set vParams = CreateParamter(pArgs1, pArgs2, pArgs3, pArgs4, pArgs5, pArgs6, pArgs7, pArgs8, pArgs9, pArgs10)
Call CallByNameEx(pObj, pMethodName, VbLet, vParams)
End Sub
Public Sub CallSetPropertyByName(pObj As Object, pMethodName As String, Optional pArgs1 As Variant, Optional pArgs2 As Variant, Optional pArgs3 As Variant, Optional pArgs4 As Variant, Optional pArgs5 As Variant, Optional pArgs6 As Variant, Optional pArgs7 As Variant, Optional pArgs8 As Variant, Optional pArgs9 As Variant, Optional pArgs10 As Variant)
Dim vParams As CallUtilParameter
Set vParams = CreateParamter(pArgs1, pArgs2, pArgs3, pArgs4, pArgs5, pArgs6, pArgs7, pArgs8, pArgs9, pArgs10)
Call CallByNameEx(pObj, pMethodName, VbSet, vParams)
End Sub
Private Function CreateParamter(Optional pArgs1 As Variant, Optional pArgs2 As Variant, Optional pArgs3 As Variant, Optional pArgs4 As Variant, Optional pArgs5 As Variant, Optional pArgs6 As Variant, Optional pArgs7 As Variant, Optional pArgs8 As Variant, Optional pArgs9 As Variant, Optional pArgs10 As Variant) As CallUtilParameter
With New CallUtilParameter
Set CreateParamter = .Init(pArgs1, pArgs2, pArgs3, pArgs4, pArgs5, pArgs6, pArgs7, pArgs8, pArgs9, pArgs10)
End With
End Function
Private Function CallByNameEx(pObj As Object, pMethodName As String, pType As VbCallType, pParams As CallUtilParameter) As CallUtilValue
Call Logger.Start(TypeName(pObj), pMethodName, pType)
Select Case pParams.Count
Case 0
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType))
Case 1
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1))
Case 2
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2))
Case 3
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2, pParams.Args3))
Case 4
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2, pParams.Args3, pParams.Args4))
Case 5
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2, pParams.Args3, pParams.Args4, pParams.Args5))
Case 6
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2, pParams.Args3, pParams.Args4, pParams.Args5, pParams.Args6))
Case 7
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2, pParams.Args3, pParams.Args4, pParams.Args5, pParams.Args6, pParams.Args7))
Case 8
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2, pParams.Args3, pParams.Args4, pParams.Args5, pParams.Args6, pParams.Args7, pParams.Args8))
Case 9
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2, pParams.Args3, pParams.Args4, pParams.Args5, pParams.Args6, pParams.Args7, pParams.Args8, pParams.Args9))
Case 10
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2, pParams.Args3, pParams.Args4, pParams.Args5, pParams.Args6, pParams.Args7, pParams.Args8, pParams.Args9, pParams.Args10))
Case Else
Debug.Print "引数の設定が不正です"
Debug.Assert False
Set CallByNameEx = Nothing
End Select
Call Logger.Finish(TypeName(pObj), pMethodName, pType)
End Function
Public Function CreateValue(pValue As Variant) As CallUtilValue
With New CallUtilValue
Set CreateValue = .Init(pValue)
End With
End Function
CallGetPropertyByNameを例に説明します。
シグニチャ
Public Function CallGetPropertyByName(pObj As Object, pMethodName As String, Optional pArgs1 As Variant, Optional pArgs2 As Variant, Optional pArgs3 As Variant, Optional pArgs4 As Variant, Optional pArgs5 As Variant, Optional pArgs6 As Variant, Optional pArgs7 As Variant, Optional pArgs8 As Variant, Optional pArgs9 As Variant, Optional pArgs10 As Variant) As Variant
第1引数、第2引数はCallByNameと同様、第1引数がオブジェクト、第2引数は実行したいメソッド名の文字列です。
第3引数以降は、可変長引数を表現するための省略可能な引数を10個用意しています。
ParamArrayを使ってもよかったのですが、後述しますが固定数でなければCallByNameをラッピングすることが難しかったため、あえて10個までしか設定できないことを明示するためにParamArrayを使うのは却下しました。
パラメータ管理クラス
次に引数で受け取った省略可能な引数を管理するためのクラスを作成します。
Dim vParams As CallUtilParameter
Set vParams = CreateParamter(pArgs1, pArgs2, pArgs3, pArgs4, pArgs5, pArgs6, pArgs7, pArgs8, pArgs9, pArgs10)
CreateParamterは単純なファクトリです。
Private Function CreateParamter(Optional pArgs1 As Variant, Optional pArgs2 As Variant, Optional pArgs3 As Variant, Optional pArgs4 As Variant, Optional pArgs5 As Variant, Optional pArgs6 As Variant, Optional pArgs7 As Variant, Optional pArgs8 As Variant, Optional pArgs9 As Variant, Optional pArgs10 As Variant) As CallUtilParameter
With New CallUtilParameter
Set CreateParamter = .Init(pArgs1, pArgs2, pArgs3, pArgs4, pArgs5, pArgs6, pArgs7, pArgs8, pArgs9, pArgs10)
End With
End Function
CallUtilParameterクラスの本体です。
Option Explicit
Private mArgs1 As CallUtilValue
Private mArgs2 As CallUtilValue
Private mArgs3 As CallUtilValue
Private mArgs4 As CallUtilValue
Private mArgs5 As CallUtilValue
Private mArgs6 As CallUtilValue
Private mArgs7 As CallUtilValue
Private mArgs8 As CallUtilValue
Private mArgs9 As CallUtilValue
Private mArgs10 As CallUtilValue
Public Function Init(Optional pArgs1 As Variant, Optional pArgs2 As Variant, Optional pArgs3 As Variant, Optional pArgs4 As Variant, Optional pArgs5 As Variant, Optional pArgs6 As Variant, Optional pArgs7 As Variant, Optional pArgs8 As Variant, Optional pArgs9 As Variant, Optional pArgs10 As Variant) As CallUtilParameter
Set mArgs1 = CreateValue(pArgs1)
Set mArgs2 = CreateValue(pArgs2)
Set mArgs3 = CreateValue(pArgs3)
Set mArgs4 = CreateValue(pArgs4)
Set mArgs5 = CreateValue(pArgs5)
Set mArgs6 = CreateValue(pArgs6)
Set mArgs7 = CreateValue(pArgs7)
Set mArgs8 = CreateValue(pArgs8)
Set mArgs9 = CreateValue(pArgs9)
Set mArgs10 = CreateValue(pArgs10)
Set Init = Me
End Function
Public Function Count() As Long
If mArgs1.IsMissing Then
Count = 0
ElseIf mArgs2.IsMissing Then
Count = 1
ElseIf mArgs3.IsMissing Then
Count = 2
ElseIf mArgs4.IsMissing Then
Count = 3
ElseIf mArgs5.IsMissing Then
Count = 4
ElseIf mArgs6.IsMissing Then
Count = 5
ElseIf mArgs7.IsMissing Then
Count = 6
ElseIf mArgs8.IsMissing Then
Count = 7
ElseIf mArgs9.IsMissing Then
Count = 8
ElseIf mArgs10.IsMissing Then
Count = 9
Else
Debug.Print "引数が10以上設定されています。"
Debug.Assert False
Count = -1
End If
End Function
Public Property Get Args1() As Variant
If mArgs1.IsObject Then
Set Args1 = mArgs1.Value
Else
Args1 = mArgs1.Value
End If
End Property
Public Property Get Args2() As Variant
If mArgs2.IsObject Then
Set Args2 = mArgs2.Value
Else
Args2 = mArgs2.Value
End If
End Property
Public Property Get Args3() As Variant
If mArgs3.IsObject Then
Set Args3 = mArgs3.Value
Else
Args3 = mArgs3.Value
End If
End Property
Public Property Get Args4() As Variant
If mArgs4.IsObject Then
Set Args4 = mArgs4.Value
Else
Args4 = mArgs4.Value
End If
End Property
Public Property Get Args5() As Variant
If mArgs5.IsObject Then
Set Args5 = mArgs5.Value
Else
Args5 = mArgs5.Value
End If
End Property
Public Property Get Args6() As Variant
If mArgs6.IsObject Then
Set Args6 = mArgs6.Value
Else
Args6 = mArgs6.Value
End If
End Property
Public Property Get Args7() As Variant
If mArgs7.IsObject Then
Set Args7 = mArgs7.Value
Else
Args7 = mArgs7.Value
End If
End Property
Public Property Get Args8() As Variant
If mArgs8.IsObject Then
Set Args8 = mArgs8.Value
Else
Args8 = mArgs8.Value
End If
End Property
Public Property Get Args9() As Variant
If mArgs9.IsObject Then
Set Args9 = mArgs9.Value
Else
Args9 = mArgs9.Value
End If
End Property
Public Property Get Args10() As Variant
If mArgs10.IsObject Then
Set Args10 = mArgs10.Value
Else
Args10 = mArgs10.Value
End If
End Property
CallUtilParameterクラスのメンバーフィールドにある10個のCallUtilValueクラスのオブジェクトはこのアプリケーションの値クラスです。こちらは後で解説します。
CallUtilParameterクラスは責務は非常に単純です。
10個の引数を保持して、必要な時にプロパティで取得できるようにしているだけです。
ただし、値がオブジェクトかそうでないかを判断するロジックをここにカプセル化しています。
CallUtilValueクラスで値の状態を管理する
次に、CallUtilValueクラスをみていきます。
このクラスは値の状態を管理することが責務です。
Option Explicit
Private mMissingFlag As Boolean
Private mObjFlag As Boolean
Private mValue As Variant
Public Function Init(pValue As Variant) As CallUtilValue
If Information.IsMissing(pValue) Then
mMissingFlag = True
mValue = pValue
Set Init = Me
Exit Function
End If
mObjFlag = Information.IsObject(pValue)
If mObjFlag Then
Set mValue = pValue
Else
mValue = pValue
End If
Set Init = Me
End Function
Public Function IsObject() As Boolean
IsObject = mObjFlag
End Function
Public Property Get Value() As Variant
If mObjFlag Then
Set Value = mValue
Else
Value = mValue
End If
End Property
Public Function IsMissing() As Boolean
IsMissing = mMissingFlag
End Function
値はVariant型であるため、変数に保持する(プリミティブ型なら代入、オブジェクト型なら参照)度に、チェックを行う必要があることから、初期化で状態を保持しています。
突貫工事で作成したため、再初期化を防止するようなコードは入れていません。
また、省略可能な引数は省略された場合、参照不能の引数となります。
これはInformation.IsMissing(pValue)でチェックすることができます。
これが必要な理由は、参照不能の値をCallByNameに渡すとエラーになるためです。
では、元のCallUtilモジュールのほうに戻ります。
CallByNameExでCallByNameをラッピングする
CallByNameExでCallByNameをラッピングするファンクションです。
また、CallGetPropertyByNameではメソッドのタイプをVbGetとしてCallByNameの引数としています。
Dim vOutValue As CallUtilValue
Set vOutValue = CallByNameEx(pObj, pMethodName, VbGet, vParams)
CallByNameExで可変長配列に渡す引数を選択する
コードはCallUtilモジュールのほうにありますので、ポイントだけ抜粋します。
まず、ここでログを出力しています。
Call Logger.Start(TypeName(pObj), pMethodName, pType)
'途中の処理
Call Logger.Finish(TypeName(pObj), pMethodName, pType)
Loggerモジュールは以下のような内容です。第1引数はオブジェクト型でもよかったのですが、文字列で渡したい場合も考えて、あえて実行元でTypeNameを使って文字列にしてもらっています。
また、Loggerはクラスではありません。これも使い勝手を考慮して、Newする手間を省くためにモジュールとしています。
ただし、モジュールですとLoggerとモジュール名を接頭辞にしなくても動作してしまうため、ここは好みが分かれるところだと思います。
Option Explicit
Option Private Module
Public Sub Start(pTarget As String, pProc As String, pType As VbCallType)
Debug.Print Timestamp.Value & " START: " & pTarget & "#" & pProc & "(" & ConvertEnumToString(pType) & ")"
End Sub
Public Sub Finish(pTarget As String, pProc As String, pType As VbCallType)
Debug.Print Timestamp.Value & " FINISH: " & pTarget & "#" & pProc & "(" & ConvertEnumToString(pType) & ")"
End Sub
Private Function ConvertEnumToString(pType As VbCallType) As String
Select Case pType
Case VbCallType.VbMethod
ConvertEnumToString = "VbMethod"
Case VbCallType.VbGet
ConvertEnumToString = "VbGet"
Case VbCallType.VbLet
ConvertEnumToString = "VbLet"
Case VbCallType.VbSet
ConvertEnumToString = "VbSet"
Case Else
Debug.Print "Enum値不正"
Debug.Assert False
End Select
End Function
ここで使用しているTimeStampモジュールは、おなじみのGetLocalTimeのラッパーです。
Option Explicit
Option Private Module
Public Type SYSTEMTIME
wYear As Integer
wMonth As Integer
wDayOfWeek As Integer
wDay As Integer
wHour As Integer
wMinute As Integer
wSecond As Integer
wMilliseconds As Integer
End Type
'// 64bit版
#If VBA7 And Win64 Then
Private Declare PtrSafe Sub GetLocalTime Lib "kernel32" (lpSystemTime As SYSTEMTIME)
'// 32bit版
#Else
Private Declare Sub GetLocalTime Lib "kernel32" (lpSystemTime As SYSTEMTIME)
#End If
Public Property Get Value() As String
Dim vSystemTime As SYSTEMTIME
'// 現在日時取得
Call GetLocalTime(vSystemTime)
'// yyyy/mm/dd hh:mm:ss.fff形式に整形
Dim vTimeStamp As String
vTimeStamp = Format(vSystemTime.wYear, "0000") & "/" & Format(vSystemTime.wMonth, "00") & "/" & Format(vSystemTime.wDay, "00") & " " & _
Format(vSystemTime.wHour, "00") & ":" & Format(vSystemTime.wMinute, "00") & ":" & Format(vSystemTime.wSecond, "00") & "." & Format(vSystemTime.wMilliseconds, "000")
Value = vTimeStamp
End Property
本題はここではありませんのでサクサクといきます。
パラメータの個数によってCallByNameへ渡す引数を選択します。
方法が思いつかず、10個Select-Caseで分岐しています。
Select Case pParams.Count
Case 0
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType))
Case 1
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1))
Case 2
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2))
Case 3
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2, pParams.Args3))
Case 4
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2, pParams.Args3, pParams.Args4))
Case 5
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2, pParams.Args3, pParams.Args4, pParams.Args5))
Case 6
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2, pParams.Args3, pParams.Args4, pParams.Args5, pParams.Args6))
Case 7
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2, pParams.Args3, pParams.Args4, pParams.Args5, pParams.Args6, pParams.Args7))
Case 8
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2, pParams.Args3, pParams.Args4, pParams.Args5, pParams.Args6, pParams.Args7, pParams.Args8))
Case 9
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2, pParams.Args3, pParams.Args4, pParams.Args5, pParams.Args6, pParams.Args7, pParams.Args8, pParams.Args9))
Case 10
Set CallByNameEx = CreateValue(CallByName(pObj, pMethodName, pType, pParams.Args1, pParams.Args2, pParams.Args3, pParams.Args4, pParams.Args5, pParams.Args6, pParams.Args7, pParams.Args8, pParams.Args9, pParams.Args10))
Case Else
Debug.Print "引数の設定が不正です"
Debug.Assert False
Set CallByNameEx = Nothing
End Select
パラメータの個数はCallUtilParameterから取得できます。
Public Function Count() As Long
If mArgs1.IsMissing Then
Count = 0
ElseIf mArgs2.IsMissing Then
Count = 1
ElseIf mArgs3.IsMissing Then
Count = 2
ElseIf mArgs4.IsMissing Then
Count = 3
ElseIf mArgs5.IsMissing Then
Count = 4
ElseIf mArgs6.IsMissing Then
Count = 5
ElseIf mArgs7.IsMissing Then
Count = 6
ElseIf mArgs8.IsMissing Then
Count = 7
ElseIf mArgs9.IsMissing Then
Count = 8
ElseIf mArgs10.IsMissing Then
Count = 9
Else
Debug.Print "引数が10以上設定されています。"
Debug.Assert False
Count = -1
End If
End Function
CallUtilValueクラスのIsMissingメソッドでどこまで引数が設定されているかが分かります。
前述のとおり、参照不能の値をCallByNameに渡すとエラーになるためです。
あとはCallByNameを実行し、戻り値からCallUtilValueを生成しています。
ここでは一度変数に保持しないで直接引数のところでCallByNameを実行すると、オブジェクト型かどうかを判別しないで済みます。
テスト
各プロシージャのテスト
初めのサンプルでは定義済みクラスの実行でログを追加するところをお見せしました。
次は、独自定義のクラスでやってみます。
テスト対象のクラスです。
Option Explicit
Private mValue As String
Public Sub SubNoParam()
Debug.Print "Subでパラメータなし"
End Sub
Public Sub Sub1Param(p1 As String)
Debug.Print "パラメータ1つ", p1
End Sub
Public Function Func2Param(p1 As String, p2 As String) As String
Func2Param = p1 & "@" & p2
End Function
Public Function Func3Param(p1 As String, p2 As String, p3 As String) As TestTarget
mValue = p1 & "@" & p2 & "@" & p3
Set Func3Param = Me
End Function
Public Property Get Value() As String
Value = mValue
End Property
Public Property Get Self() As TestTarget
Set Self = Me
End Property
Public Property Let Value(pValue As String)
mValue = pValue
End Property
Public Property Set Self(pObj As TestTarget)
mValue = pObj.Value
End Property
このクラスを使用する際に、独自のCallByName拡張を使ってみます。
Public Sub Test()
Dim vTest1 As TestTarget
Set vTest1 = New TestTarget
Call CallSubByName(vTest1, "SubNoParam")
Call CallSubByName(vTest1, "Sub1Param", "p1")
Debug.Print CallFunctionByName(vTest1, "Func2Param", "p1", "p2")
Dim vObj As TestTarget
Set vObj = CallFunctionByName(vTest1, "Func3Param", "p1", "p2", "p3")
Debug.Print CallGetPropertyByName(vObj, "Value")
Dim vTest2 As TestTarget
Set vTest2 = New TestTarget
Call CallLetPropertyByName(vTest2, "Value", "prop")
Debug.Print CallGetPropertyByName(vTest2, "Value")
Set vObj = CallGetPropertyByName(vTest2, "Self")
Debug.Print CallGetPropertyByName(vTest2, "Value")
Call CallSetPropertyByName(vTest1, "Self", vTest2)
Debug.Print CallGetPropertyByName(vTest1, "Value")
End Sub
実行結果は以下のようになります。
2023/09/18 15:37:09.565 START: TestTarget#SubNoParam(VbMethod)
Subでパラメータなし
2023/09/18 15:37:09.570 FINISH: TestTarget#SubNoParam(VbMethod)
2023/09/18 15:37:09.572 START: TestTarget#Sub1Param(VbMethod)
パラメータ1つ p1
2023/09/18 15:37:09.576 FINISH: TestTarget#Sub1Param(VbMethod)
2023/09/18 15:37:09.579 START: TestTarget#Func2Param(VbMethod)
2023/09/18 15:37:09.581 FINISH: TestTarget#Func2Param(VbMethod)
p1@p2
2023/09/18 15:37:09.586 START: TestTarget#Func3Param(VbMethod)
2023/09/18 15:37:09.589 FINISH: TestTarget#Func3Param(VbMethod)
2023/09/18 15:37:09.592 START: TestTarget#Value(VbGet)
2023/09/18 15:37:09.592 FINISH: TestTarget#Value(VbGet)
p1@p2@p3
2023/09/18 15:37:09.594 START: TestTarget#Value(VbLet)
2023/09/18 15:37:09.594 FINISH: TestTarget#Value(VbLet)
2023/09/18 15:37:09.595 START: TestTarget#Value(VbGet)
2023/09/18 15:37:09.596 FINISH: TestTarget#Value(VbGet)
prop
2023/09/18 15:37:09.597 START: TestTarget#Self(VbGet)
2023/09/18 15:37:09.598 FINISH: TestTarget#Self(VbGet)
2023/09/18 15:37:09.599 START: TestTarget#Value(VbGet)
2023/09/18 15:37:09.601 FINISH: TestTarget#Value(VbGet)
prop
2023/09/18 15:37:09.603 START: TestTarget#Self(VbSet)
2023/09/18 15:37:09.604 FINISH: TestTarget#Self(VbSet)
2023/09/18 15:37:09.605 START: TestTarget#Value(VbGet)
2023/09/18 15:37:09.606 FINISH: TestTarget#Value(VbGet)
prop
正しく実行できていますね。
階層構造の場合
次にクラスから別のクラスのメソッドを実行してみます。
公開されているクラスで内部のクラスを実行するところで、CallByName拡張を使うことで、START~FINISHが実行された順に階層として出力されます。
Option Explicit
Public Sub Execute()
Debug.Print "これは公開用のクラスです。これから内部でしか使わないクラスをCallSubByNameで実行します"
Dim vIn As Internal
Set vIn = New Internal
Call CallSubByName(vIn, "Execute")
Debug.Print "内部のクラスの実行が終わって戻ってきました。"
End Sub
Option Explicit
Public Sub Execute()
Debug.Print "内部でしか使わないクラスです"
End Sub
Public Sub 階層構造でも正しく出力されるか()
Dim v1 As Publish
Set v1 = New Publish
Call CallSubByName(v1, "Execute")
End Sub
結果を見てみます。
2023/09/18 15:42:13.414 START: Publish#Execute(VbMethod)
これは公開用のクラスです。これから内部でしか使わないクラスをCallSubByNameで実行します
2023/09/18 15:42:13.421 START: Internal#Execute(VbMethod)
内部でしか使わないクラスです
2023/09/18 15:42:13.424 FINISH: Internal#Execute(VbMethod)
内部のクラスの実行が終わって戻ってきました。
2023/09/18 15:42:13.427 FINISH: Publish#Execute(VbMethod)
きちんと出力されました。
標準モジュールでも使えます
標準モジュールでも使うことができます。
例えばこのようなプロシージャがあったとします。
Public Function 標準モジュールでも可能() As String
標準モジュールでも可能 = "標準モジュールからの戻り値です"
End Function
標準モジュールですのでCallByNameを使うことはできませんが、これをラッピングするアダプタークラスを用意します。
Option Explicit
Public Function 標準モジュールラッピング() As String
標準モジュールラッピング = 標準モジュールでも可能
End Function
このアダプタークラスを使ってあげれば標準モジュールでも実行できます。
Public Sub TestFor標準モジュール()
Dim vAdapter As Adapter
Set vAdapter = New Adapter
Debug.Print CallFunctionByName(vAdapter, "標準モジュールラッピング")
End Sub
結果は以下の通り。
2023/09/18 19:32:57.490 START: Adapter#標準モジュールラッピング(VbMethod)
2023/09/18 19:32:57.493 FINISH: Adapter#標準モジュールラッピング(VbMethod)
標準モジュールからの戻り値です
使うのは面倒ではないのか?
これまで色々な使い方を見てきました。
ただ、文法が結構異なるように見えますので、使うの面倒そうって思っていませんか?
大丈夫です。次ぐ使えるようになります。
Step1:普通に記述する
例えば、一番初めに紹介した、ブック名を出力するものを考えてみます。
通常の記述は、
Debug.Print ThisWorkbook.Name
こう記述します。
Step2:インテリセンスで表示する
ThisWorkbookの前で、Callくらいまで打ち込んでインテリセンス(Ctrl+Space)でCallByName拡張を表示します。
あとは使用する対象を選択します。すると勝手にセミコロンが入ります。
Public Sub Testtest()
Debug.Print CallGetPropertyByName; ThisWorkbook.Name
End Sub
Step3:引数を修正する
これでは動作しないため、引数を修正します。
セミコロンをカッコ初めにして、
第1引数はオブジェクトなので、ThisWorkbookを使います。
第2引数はメソッド名なので、Nameをダブルクォーテーションで囲んで、"Name"にします。
さらに引数がある場合は、使っていた引数をそのまま渡せばよいだけです。
最後にカッコを閉じます。
Public Sub Testtest()
Debug.Print CallGetPropertyByName(ThisWorkbook, "Name")
End Sub
このように実行順序に引数が並んでいるため、比較的簡単に利用できます。
できないこと
これはCallByNameでもできませんが、ThisWorkbook.WorkSheets("Sheet1").Nameのようなコロンを続けて実行するものは1回の実行では出来ません。
例でもある通り、
Debug.Print CallGetPropertyByName(CallGetPropertyByName(ThisWorkbook, "WorkSheets", "Sheet1"), "Name")
といった形で2回にする必要があります。
ただ、これは定義済みオブジェクトを使用するときは多用しますが、自分で作成したクラスではあまり使用しません。
デルメルの法則に従っていれば、あまりそういうコードは記載しないでしょう。
結局、課題は解決できたのか
最後に課題の解決がどのようになされたのか振り返ってみましょう。
-
プロシージャ名を変更したらログ出力も修正が必要
⇒引数でメソッド名を渡すので、間違っていたら動作しないため、正しいことが保証される -
ログ出力し忘れても気が付かない、または面倒であえてやらなくなる
⇒CallByNameよりも簡単に記述でき、記述順の変更もないため修正は容易 -
プロシージャなどの小さいレベルでも実装するのか・しないのか判断に迷う
⇒あとからいつでもログ出力を入れ込めるので、出力したくなったら入れるでOK
解決できています。
作ったものを破壊せずに後からログを導入できるのはとてもいいことです。
皆様もぜひ使ってみてください。
Discussion