今回は、前回に引き続きエクセルVBA開発におけるデバッグ(debug)について話したいと思うのですが、今回は、少し応用編というか、ちょいテクというか、そんな感じのお話しをしてみたいと思います。
以下からサンプルをダウンロードしてください。
サンプルのダウンロード
第19回でも、ウォッチ(or ローカル)ウインドウを使ったデバッグ方法について説明しましたが、もう少しだけ、補足として、このウインドウを使ったデバッグについて、説明したいと思います。以下が、サンプルの【値の変更】ボタンが押された時の、プログラムです。
このサンプルは、架空のシステムのプログラムと思ってください。そのシステムに操作モードが手動、セミオート、自動の3種類があったとします。ボタンが押された時には、そのモードが、自動に設定されているものとします。
'操作モード(架空)
Private Enum EnumOperationMode
MODE_MANUAL = 0 '手動
MODE_AUTO_SEMI 'セミオート
MODE_AUTO '自動
End Enum
'ウォッチ(or ローカル)ウインドウを使った変数値の変更
Private Sub CBtnWatch_Click()
Dim op_mode As EnumOperationMode
'自動に設定されているとします。
op_mode = MODE_AUTO
'Select文の行にブレークポイントを設定。
Select Case op_mode
Case MODE_MANUAL
Debug.Print "操作モード-手動"
Case MODE_AUTO_SEMI
Debug.Print "操作モード-セミオート"
Case MODE_AUTO
Debug.Print "操作モード-自動"
Case Else
Debug.Print "操作モード不明"
End Select
End Sub
このモード(変数op_mode)をデバッグ中にウォッチウインドウを使って変更してみたいと思います。ということで、Select文の先頭にブレークポイントを設定してください。
ブレークポイントが設定できたら、実際に、【値の変更】ボタンを押してプログラムを一時停止状態にしてみてください。
以下の位置で、プログラムが停止しますので、ウォッチウインドウまたは、ローカルウインドウを表示してみてください。確認したいのが、op_modeのローカル変数ですので、今回は、ローカルウインドウを表示してみます。
ウインドウを見ると、op_modeに自動であるMODE_AUTOが設定されていることが分かります。
このように列挙値を定義し、変数の型をその列挙型にすると、デバッガでは、値が数値ではなく、列挙値として表示されるようになります。これにより、単に数値で確認するよりも、定義値で確認した方が、人間にとって、理解しやすくなります。
それでは、次に、デバッガを停止せず、プログラムも変更せずに、この値を変更してみましょう。ローカルウインドウの値の箇所をクリックすると編集モードになりますので、値を、操作モードがセミオート(MODE_SEMI_AUTO)に変更してみましょう。MODE_SEMI_AUTOと入力してもよいですが、直接、値の1を設定しても同等の結果となります。
値の変更ができたら、【F8】キーを押しながら、ステップ実行し、Case文のMODE_SEMI_AUTOのパスにプログラムの処理が走ることを、実際に確認してみましょう。
このように、デバッグ中に変数の値を、ローカルまたはウォッチウインドウにて、変更することが可能です。これにより、デバッグ作業が効率的に行なえる場合がありますので、是非、覚えておきましょう。
ちなみに、Constで定義された定数は、ウォッチウインドウにより値の確認は出来ますが、変更はできません。
続いてアサートについて、説明したいと思います。このアサートをうまく使えば、非常に効率的にプログラムが開発が進みますので、是非挑戦して、自分のものにして欲しいと思います。アサート(assert)は英語の、断言する、強く主張するという意味ですが、プログラムでどのように使用すればよいか分かりますでしょうか? 文法としては、以下のように記述しますが、処理内容は、評価式がFalse判定された場合に、プログラムが中断されるというものです。
Debug.Assert 評価式
それでは、実際にサンプルプログラムを使って、もう少し具体的に説明したいと思います。以下がそのサンプルなのですが、これは、サイコロの目をシュミレートした実装となっています。サイコロなので、1~6の値をランダムにDice関数が返却するようにしてあります。なのですが、実は、このコード不具合が含まれています。不具合については、後で説明するので、今のタイミングではこれ以上、触れません。
'サイコロをシミュレートした関数。乱数で1~6までを返却。
Private Function Dice() As Long
Dice = Int(6 * Rnd())
End Function
'Assertの使い方
Private Sub CBtnDesign_Click()
Dim d_num As Long
'サイコロをシミュレートした関数なので、
'1-6までの値しか存在しないのですが…
d_num = Dice()
'1-6までのはず!という断定(Assert)。
Debug.Assert d_num >= 1 And d_num <= 6
'サイコロの目をシートに出力
Me.Range("F6") = d_num
End Sub
Dice関数を呼び出した後に、Debug.Assertを記述していますが、これは、『Dice関数で返却される値は、1~6までの値のはず』ということを、強く主張(断言)しています。ですので、もし、1~6以外の値が返却されると、評価式がFalse判定され、プログラムが中断してしまいます。
それでは、試しに、【サイコロ】ボタンを押してプログラムを実行してみましょう。うまくいけば、1-6までの値が、シートのセルに出力されるはずなのですが、おそらく、6回に1回ぐらいAssertが発生するかと思います。
原因は、Dice関数に不具合があるのですが、皆さん、分かりますでしょうか? Rnd関数は乱数を発生させる関数なのですが、仕様を確認すると返却値が0以上、1未満( 0 <= 返却値 < 1 )となっています。ということは、今のままでは、0-5の値が返却されてしまう実装になってしまっています。正しくは、以下のように+1してやらなければなりません。自分では正しく実装できていたつもりなのですが、Dice関数が、0を返却することがあることに気付いていなかったことが、不具合の原因だったということです。
'サイコロをシミュレートした関数。乱数で1~6までを返却。
Private Function Dice() As Long
Dice = Int(6 * Rnd()) + 1
End Function
このように、仕様上ありえないという条件が見出せる場所にAssertメソッドを使用することを、強くお勧めします。
では、このAssertを使うメリットはどこにあるのかということなのですが…
今回のような場合であれば、単純な不具合なのですが、そうではなく複雑な条件が重なり合って、致命的なシステムの不具合が発生する場合があります。そうなると原因を追究することに、非常に時間がかかってしまうわけなのですが、Assertを要所、要所に実装しておけば、複数の条件が重なり合う前に、異常を検知しプログラムの実装ミスや設計ミスに対して素早く対応出来き、品質の高いシステムが開発できるようになります。
自分では、しっかり設計して、間違いのないつもりでも、所詮、人間の考えることなので、設計漏れや、勘違いが、多分に起こりえます。Assertをうまく利用すれば、早い段階で、これらのミスに気付けますので、非常に効率よく開発を進めることが出来ます。
他にも、例えば、通信システムにおいて、不具合が発生したとします。その原因が送信側が、仕様外、すなわち、想定外のデータを送ったことによるものだった場合、仮に、もし受信側で、仕様外のデータの受信に対してAssertを挙げていれば、受信側のソースコードを見直すことなく、送信側の不具合ということが判断できます。このように、どちらの側の不具合なのかという原因の切り分けを、容易にすることにも繋がります。
もう少し、Assertの使い方で、私がよくやるデバッグの仕方を簡単に説明したいと思います。以下のような2重のFor文を使って説明します。i=800、j=1900のときの、For文の処理をデバッグしたいとします。
'For文ループのデバッグ
Private Sub CBtnLoop_Click()
Dim i As Long
Dim j As Long
For i = 1 To 1000
For j = 1 To 2000
'i=800、j=1900のときの動作確認がしたい場合。
Next j
Next i
End Sub
このコードにブレークポイントを設定し、ステップ実行やステップアウト実行を繰り返し、i=800、j=1900の状態にしようとすると、とんでもなく時間が掛かり大変な作業となってしまいます。
ですので、こういった場合、以下のようにAssertを記述します。こうすると、i=800かつ、j=1900のときに、アサートが発生し、プログラムが中断され、容易にデバッグすることが可能となります。
'i=800、j=1900のときの動作確認がしたい場合。
Debug.Assert i <> 800 Or j <> 1900
アサートを発生させるための、条件が複数で評価式が複雑になり、頭で考えるのが難しくなった場合は、以下のようにIf文と併用することで、理解し易くなると思いますので、参考にしてみてください。無駄なコードを記述することになりますが、デバッグが終了すれば消せば良いだけです。
If i = 800 And j = 1900 Then
Debug.Assert False
End If
Assertの説明の最後になりますが、Assertは処理が実行されますので、その分、少なからず、プログラムの実行に時間が掛かります。不具合が改善されたり、システムが安定し、Assertが不要になったタイミングで、必要に応じて、Assert処理を削除、もしくは、コメントアウトしましょう。
Debug.Printにより、付加的な情報を残すことが、ロギングの手法の1つとしてありますが、このように、単純に文字を表示領域に出力する以外にも、ファイルなどのリソースに、デバッグの手助けとなる情報を保存するような方法も、ロギングの一つに挙げられます。
エクセルVBAにおいては、保存領域のリソースとして、シートがありますので、これを使わない手はないと思います。サンプルの【ロギング】ボタンにログの書き込みのサンプルを実装していますので、参考にしてみてください。
Debug.Printには、表示出来る領域に限りがありますが、シートにログを保存するようにすれば、無限ではありませんが、Debug.Printよりも、多くログを残すことが出来るようになります。サンプルでは、日付や日時情報なども、一緒に残すように実装していますので、長時間稼動のシステムのデバッグにも有効な手段となり得ます。
以下から、今回の豆知識で紹介した内容のエクセルファイルがダウンロードできます。
サンプルのダウンロード
ご意見・ご要望等ありましたら、画面最下部のメールアドレスまでご連絡ください。