デバッグ時に役立つ便利な属性

注意:
この文書は以前「.NETでいきまっしょい!」で公開していたものですが、公開以降メンテナンスされていません。 今や古い情報となった内容が記載されている場合があるのでご注意ください。

1.System.Diagnostics 名前空間


 System.Diagnostics 名前空間にはデバッグ時に便利な属性がいくつか存在します。 ここではそのうちのいくつかを紹介します。 また、それに先だって#Ifディレクティブとその用法を見ていきます。

2.#Ifディレクティブ, #Constディレクティブ


 まずはじめに、属性ではありませんが#Ifディレクティブについて見てみます。 #Ifディレクティブは条件付きコンパイルを行うための構文で、例えばデバッグビルドの時だけ特定のコードを有効(または無効)にするといったような場合に使用します。 また、#Constディレクティブを用いて独自に条件付きコンパイル定数を定義することもできます。
#Ifディレクティブ, #Constディレクティブ
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
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 の値を変えた場合の実行結果を次に示します。
デバッグビルド LogOutputTime = True
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
デバッグビルド LogOutputTime = False
Start application
Begin calculation.
Calculation was finished.
End application
Press any key to continue
リリースビルド
Begin calculation.
Calculation was finished.
Press any key to continue

3.Conditional属性


 なぜ最初に#Ifディレクティブを紹介したかというと、#Ifディレクティブには欠点があるからです。 というのは、前の例のようにデバッグ時だけにあるメソッドを実行したいときにはメソッドの内のコードだけを#Ifディレクティブをくくらなければなりません。 メソッド宣言全体を#Ifでくくってしまうと、リリースビルドの時点ではこのメソッドはコンパイルされなくなり、呼び出しが無効になるからです。 これは呼び出し側にも#Ifディレクティブを適用すれば解決できますが、非常にムダが多くなります。 また、メソッド内のコードのみに適用した場合でも、リリースビルド時でも実際メソッドが呼び出されるので、無用なオーバーヘッドが生じます。
 この問題を解決するにはConditional属性を使用することができます。 Conditional属性はメソッドに対して使用され、属性のコンストラクタに指定された条件付きコンパイル定数が定義されている時に限りそのメソッドが実際に呼び出されます。 それ以外の場合には実行されることはありません。 次のコードでその使用例を挙げます。
Conditional属性
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
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
デバッグビルド
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を使うなどする必要があります。
戻り値が必要な場合
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
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
デバッグビルド
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であれば必ずログが出力されます。
複数指定した場合
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
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

4.Obsolete属性


 Obsolete属性は古くなったメソッドなどに対して適用し、それが使用された場合に警告を発することができる属性です。
Obsolete属性をエラーとする場合
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
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

 このコードをコーディングしているときに次のような警告が表示されました。
表示された警告
表示された警告

 さらに、コンパイル時にはこれと同じ警告が発せられました。 このように、Obsolete属性は互換性のために残してあるような仕様の古くなったコードに対して、使用しないように促す警告を発することができます。 この例ではメソッドに対してObsolete属性を適用していますが、アセンブリ、モジュール、パラメータ、戻り値以外の要素に対して適用することができます。 また、警告ではなくエラーとして扱う場合には次のようにします。
Obsolete属性をエラーとする場合
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
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

 つまり、属性のコンストラクタの二つ目の引数にTrueを指定します。 こうすると完全にエラーとなり、この要素を使用したコードをコンパイルしようとするとコンパイルエラーが発生します。
表示されたエラー・警告
表示されたエラー・警告

 さらに、構造体に対してObsolete属性を適用した例を次に挙げます。
構造体に対する適用
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
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
表示される警告
表示される警告

5.DebuggerStepThrough属性


 DebuggerStepThrough属性は構造体、クラス、メソッド、コンストラクタに対して適用し、適用された要素はデバッグ対象からはずれます。
DebuggerStepThrough属性
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
Option Strict On

Imports System.Diagnostics

Module AttributesForDebugging

    <DebuggerStepThrough()> _
    Sub DebuggedMethod()

        ' デバッグ済みのコードがあるとする

        Console.WriteLine("処理が完了しました。")

    End Sub


    Sub Main()

        DebuggedMethod()

    End Sub

End Module

 例えばこのコードの19行目にブレークポイントを設定し、メソッドにステップ・インしようとしても、ステップ・インせずにメソッドの処理が終了し、メソッド呼び出しのあった次の行に進んでしまいます。 このように、バグの原因を探すために一行ずつ実行するときにデバッグ済みのコードも一行ずつ実行しなければならないのは非常に不便なので、デバッグが完了したメソッドやクラスにDebuggerStepThrough属性を適用することでデバッグ対象から外すことができ、効率的なデバッグが行えるようになります。
 ただし、一行ずつ実行することはできなくても、ブレークポイントを設定することができ、実行時には設定されたところで停止します。 また言うまでもありませんが、リリースビルドではDebuggerStepThrough属性を指定していようといなかろうと無関係です。

6.DebuggerHidden属性


 DebuggerHidden属性は、基本的にはDebuggerStepThrough属性と同様のもので、メソッド及びプロパティに対して使用します。 ただDebuggerStepThrough属性とは異なり使用されたメソッドの中ではブレークポイントを設定することができません。 厳密に言うと、ブレークポイント自体の設定はできるもののそのメソッドの外部で停止します。
DebuggerHidden属性
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
Option Strict On

Imports System.Diagnostics

Module AttributesForDebugging

    <DebuggerHidden()> _
    Sub DebuggedMethod()

        ' デバッグ済みのコードがあるとする

        Console.WriteLine("処理が完了しました。")

    End Sub

    Sub Main()

        DebuggedMethod()

    End Sub

End Module

 DebuggerHiddenAttributeという名前が表すとおり、デバッグ時にはその要素が「見えなくなる」と解釈するといいと思います。