System.Diagnostics名前空間にはデバッグ時に便利な属性がいくつか存在します。 ここではそのうちのいくつかを紹介します。 また、それに先だって#Ifディレクティブとその用法を見ていきます。
まずはじめに、属性ではありませんが#Ifディレクティブについて見てみます。 #Ifディレクティブは条件付きコンパイルを行うための構文で、例えばデバッグビルドの時だけ特定のコードを有効(または無効)にするといったような場合に使用します。 また、#Constディレクティブを用いて独自に条件付きコンパイル定数を定義することもできます。
Option Strict On ' 時刻も出力する #Const LogOutputTime = False Imports System.Threading Module AttributesForDebugging ' イベントを記録する Sub LogEvent(ByVal ev As String) #If Debug Then #If LogOutputTime Then Console.WriteLine(ev + " Time: " + DateTime.Now.ToString()) #Else Console.WriteLine(ev) #End If #End If End Sub Sub Main() ' イベントを記録 LogEvent("Start application") ' 開始メッセージを出力 Console.WriteLine("Begin calculation.") ' いくつものややこしい処理があるとする Thread.Sleep(3000) ' 終了メッセージを出力 Console.WriteLine("Calculation was finished.") ' イベントを記録 LogEvent("End application") End Sub End Module
まず、コードの説明をしますが、Main()のコードはそれほど重要ではありません。 実際に#Ifディレクティブを使用しているLogEvent()メソッドについて見てみます。 このメソッドの動作を簡単に説明すると、デバッグビルドの場合ではDebugスイッチが宣言されているので13行目から19 行目のコードはアクティブになります。 また、LogOutputTimeスイッチにTrueが指定されている場合は15行目がアクティブになり、 Falseの場合は17行目がアクティブになります。 リリースビルドとデバッグビルド、またそのそれぞれでLogOutputTimeの値を変えた場合の実行結果を次に示します。
Start application Time: 2003/03/30 11:42:44 Begin calculation. Calculation was finished. End application Time: 2003/03/30 11:42:47 Press any key to continue
Start application Begin calculation. Calculation was finished. End application Press any key to continue
Begin calculation. Calculation was finished. Press any key to continue
なぜ最初に#Ifディレクティブを紹介したかというと、#Ifディレクティブには欠点があるからです。 というのは、前の例のようにデバッグ時だけにあるメソッドを実行したいときにはメソッドの内のコードだけを#Ifディレクティブをくくらなければなりません。 メソッド宣言全体を#Ifでくくってしまうと、リリースビルドの時点ではこのメソッドはコンパイルされなくなり、呼び出しが無効になるからです。 これは呼び出し側にも#Ifディレクティブを適用すれば解決できますが、その数が多くなると非常に面倒です。 また、メソッド内のコードのみに適用した場合でも、リリースビルド時でもメソッドは呼び出されるので、無用なオーバーヘッドが生じます。
この問題を解決するにはConditional属性を使用することができます。 Conditional属性はメソッドに対して使用され、属性のコンストラクタに指定された条件付きコンパイル定数が定義されている時に限りそのメソッドが実際に呼び出されます。 それ以外の場合には実行されることはありません。 次のコードでその使用例を挙げます。
Option Strict On ' 時刻も出力する #Const LogOutputTime = False Imports System.Diagnostics Imports System.Threading Module AttributesForDebugging ' イベントを記録する <Conditional("Debug")> _ Sub LogEvent(ByVal ev As String) #If LogOutputTime Then Console.WriteLine(ev + " Time: " + DateTime.Now.ToString()) #Else Console.WriteLine(ev) #End If End Sub Sub Main() ' イベントを記録 LogEvent("Start application") ' 開始メッセージを出力 Console.WriteLine("Begin calculation.") ' いくつものややこしい処理があるとする Thread.Sleep(3000) ' 終了メッセージを出力 Console.WriteLine("Calculation was finished.") ' イベントを記録 LogEvent("End application") End Sub End Module_ Sub LogEvent(ByVal ev As String) #If LogOutputTime Then Console.WriteLine(ev + " Time: " + DateTime.Now.ToString()) #Else Console.WriteLine(ev) #End If End Sub Sub Main() ' イベントを記録 LogEvent("Start application") ' 開始メッセージを出力 Console.WriteLine("Begin calculation.") ' いくつものややこしい処理があるとする Thread.Sleep(3000) ' 終了メッセージを出力 Console.WriteLine("Calculation was finished.") ' イベントを記録 LogEvent("End application") End Sub End Module]]>
Start application Begin calculation. Calculation was finished. End application Press any key to continue
Begin calculation. Calculation was finished. Press any key to continue
この結果を見てわかるとおり、その機能は一見#Ifディレクティブと同じですが、リリースビルドの時、つまりDebugが定義されていない場合はメソッドの呼び出しが無視されます。 また、Conditional属性を適用したメソッドでも当然#Ifディレクティブは機能します。 ここで、属性のコンストラクタに渡す文字列は定数名で、大文字小文字が区別されます。 この定数は自分で定義した定数(#Constで定義した定数)でもかまいませんが、 Conditional属性は定数が定義されていて、なおかつ0以外の値を持つときのみ有効になります。
さらに、Conditional属性を適用するメソッドは値を返さないSubメソッドに限られます。 もし何らかの値を返したい場合は、ByRefを使うなどする必要があります。
Option Strict On Imports System.Diagnostics Imports System.Threading Module AttributesForDebugging ' イベントを記録する <Conditional("Debug")> _ Sub LogEvent(ByVal ev As String, ByRef dtm As DateTime) dtm = DateTime.Now Console.WriteLine(ev + " Time: " + dtm.ToString()) End Sub Sub Main() ' この時刻は何らかの理由で利用されるものとする Dim dtm As DateTime ' イベントを記録 LogEvent("Start application", dtm) ' いくつものややこしい処理があるとする Thread.Sleep(3000) ' イベントを記録 LogEvent("End application", dtm) End Sub End Module_ Sub LogEvent(ByVal ev As String, ByRef dtm As DateTime) dtm = DateTime.Now Console.WriteLine(ev + " Time: " + dtm.ToString()) End Sub Sub Main() ' この時刻は何らかの理由で利用されるものとする Dim dtm As DateTime ' イベントを記録 LogEvent("Start application", dtm) ' いくつものややこしい処理があるとする Thread.Sleep(3000) ' イベントを記録 LogEvent("End application", dtm) End Sub End Module]]>
Start application Time: 2003/03/22:15:14 End application Time: 2003/03/22:15:17 Press any key to continue
Press any key to continue
さらに、Conditional属性を複数指定することもできます。 ただ、その場合は定義されていない定数があるか、その定数の値が0であるようなものが一つでもあると、そのメソッドに対するConditional属性は全て無効になります。 その例を挙げますが、実行結果は省略します。 この例では、たとえリリースビルドであってもOutputLogがTrueであれば必ずログが出力されます。
Option Strict On #Const OutputLog = True Imports System.Diagnostics Imports System.Threading Module AttributesForDebugging ' イベントを記録する <Conditional("Debug"), Conditional("OutputLog")> _ Sub LogEvent(ByVal ev As String, ByRef dtm As DateTime) dtm = DateTime.Now Console.WriteLine(ev + " Time: " + dtm.ToString()) End Sub Sub Main() ' この時刻は何らかの理由で利用されるものとする Dim dtm As DateTime ' イベントを記録 LogEvent("Start application", dtm) ' いくつものややこしい処理があるとする Thread.Sleep(3000) ' イベントを記録 LogEvent("End application", dtm) End Sub End Module_ Sub LogEvent(ByVal ev As String, ByRef dtm As DateTime) dtm = DateTime.Now Console.WriteLine(ev + " Time: " + dtm.ToString()) End Sub Sub Main() ' この時刻は何らかの理由で利用されるものとする Dim dtm As DateTime ' イベントを記録 LogEvent("Start application", dtm) ' いくつものややこしい処理があるとする Thread.Sleep(3000) ' イベントを記録 LogEvent("End application", dtm) End Sub End Module]]>
Obsolete属性は古くなったメソッドなどに対して適用し、それが使用された場合に警告を発することができる属性です。
Option Strict On Imports System.Diagnostics Module AttributesForDebugging <Obsolete("このバージョンは使用しないで下さい。 NewMethodを使用して下さい。", True)> _ Sub OlderMethod(ByVal msg As String) Debug.WriteLine("より古いバージョン: " + msg) End Sub <Obsolete("将来サポートされなくなる可能性があります。 NewMethodを使用して下さい。")> _ Sub OldMethod(ByVal msg As String) Console.WriteLine("古いバージョン: " + msg) End Sub Sub NewMethod(ByVal msg As String) Console.WriteLine("新しいバージョン: " + msg) End Sub Sub Main() OlderMethod("メソッドを呼び出します") OldMethod("メソッドを呼び出します") NewMethod("メソッドを呼び出します") End Sub End Module_ Sub OlderMethod(ByVal msg As String) Debug.WriteLine("より古いバージョン: " + msg) End Sub_ Sub OldMethod(ByVal msg As String) Console.WriteLine("古いバージョン: " + msg) End Sub Sub NewMethod(ByVal msg As String) Console.WriteLine("新しいバージョン: " + msg) End Sub Sub Main() OlderMethod("メソッドを呼び出します") OldMethod("メソッドを呼び出します") NewMethod("メソッドを呼び出します") End Sub End Module]]>
このコードをコーディングしているときには次のような警告が表示されます。
さらに、コンパイル時にはこれと同じ警告が発せられます。 このように、Obsolete属性は互換性のために残してあるような仕様の古くなったコードに対して、使用しないように促す警告を発することができます。 この例ではメソッドに対してObsolete属性を適用していますが、アセンブリ、モジュール、パラメータ、戻り値以外の要素に対して適用することができます。 また、警告ではなくエラーとして扱う場合には次のようにします。
Option Strict On Imports System.Diagnostics Module AttributesForDebugging <Obsolete("このバージョンは使用しないで下さい。 NewMethodを使用して下さい。", True)> _ Sub OlderMethod(ByVal msg As String) Debug.WriteLine("より古いバージョン: " + msg) End Sub <Obsolete("将来サポートされなくなる可能性があります。 NewMethodを使用して下さい。")> _ Sub OldMethod(ByVal msg As String) Console.WriteLine("古いバージョン: " + msg) End Sub Sub NewMethod(ByVal msg As String) Console.WriteLine("新しいバージョン: " + msg) End Sub Sub Main() OlderMethod("メソッドを呼び出します") OldMethod("メソッドを呼び出します") NewMethod("メソッドを呼び出します") End Sub End Module_ Sub OlderMethod(ByVal msg As String) Debug.WriteLine("より古いバージョン: " + msg) End Sub_ Sub OldMethod(ByVal msg As String) Console.WriteLine("古いバージョン: " + msg) End Sub Sub NewMethod(ByVal msg As String) Console.WriteLine("新しいバージョン: " + msg) End Sub Sub Main() OlderMethod("メソッドを呼び出します") OldMethod("メソッドを呼び出します") NewMethod("メソッドを呼び出します") End Sub End Module]]>
つまり、属性のコンストラクタの二つ目の引数にTrueを指定します。 こうすると完全にエラーとなり、この要素を使用したコードをコンパイルしようとするとコンパイルエラーが発生します。
さらに、構造体に対してObsolete属性を適用した例を次に挙げます。
Option Strict On Imports System.Diagnostics Module AttributesForDebugging <Obsolete("System.Drawing.Colorを使用して下さい。")> _ Structure ARGB Dim R As Byte Dim G As Byte Dim B As Byte Dim A As Byte End Structure Sub Main() Dim col1 As ARGB Dim col2 As System.Drawing.Color End Sub End Module_ Structure ARGB Dim R As Byte Dim G As Byte Dim B As Byte Dim A As Byte End Structure Sub Main() Dim col1 As ARGB Dim col2 As System.Drawing.Color End Sub End Module]]>
DebuggerStepThrough属性は構造体、クラス、メソッド、コンストラクタに対して適用し、適用された要素はデバッグ対象からはずれます。
Option Strict On Imports System.Diagnostics Module AttributesForDebugging <DebuggerStepThrough()> _ Sub DebuggedMethod() ' デバッグ済みのコードがあるとする Console.WriteLine("処理が完了しました。") End Sub Sub Main() DebuggedMethod() End Sub End Module_ Sub DebuggedMethod() ' デバッグ済みのコードがあるとする Console.WriteLine("処理が完了しました。") End Sub Sub Main() DebuggedMethod() End Sub End Module]]>
例えばこのコードの19行目にブレークポイントを設定し、メソッドにステップ・インしようとしても、ステップ・インせずにメソッドの処理が終了し、メソッド呼び出しのあった次の行に進んでしまいます。 このように、バグの原因を探すために一行ずつ実行するときにデバッグ済みのコードも一行ずつ実行しなければならないのは非常に不便なので、デバッグが完了したメソッドやクラスにDebuggerStepThrough属性を適用することでデバッグ対象から外すことができ、効率的なデバッグが行えるようになります。
ただし、一行ずつ実行することはできなくても、ブレークポイントを設定することができ、実行時には設定されたところで停止します。 また言うまでもありませんが、リリースビルドではDebuggerStepThrough属性を指定していようといなかろうと無関係です。
DebuggerHidden属性は、基本的にはDebuggerStepThrough属性と同様のもので、メソッド及びプロパティに対して使用します。 ただDebuggerStepThrough属性とは異なり使用されたメソッドの中ではブレークポイントを設定することができません。 厳密に言うと、ブレークポイント自体の設定はできるもののそのメソッドの外部で停止します。
Option Strict On Imports System.Diagnostics Module AttributesForDebugging <DebuggerHidden()> _ Sub DebuggedMethod() ' デバッグ済みのコードがあるとする Console.WriteLine("処理が完了しました。") End Sub Sub Main() DebuggedMethod() End Sub End Module_ Sub DebuggedMethod() ' デバッグ済みのコードがあるとする Console.WriteLine("処理が完了しました。") End Sub Sub Main() DebuggedMethod() End Sub End Module]]>
DebuggerHiddenAttributeという名前が表すとおり、デバッグ時にはその要素が「見えなくなる」と解釈するといいと思います。