C#
.NET

MCP 70-483 Programming in C# のお勉強 「3-4 アプリケーションをデバッグする」

More than 1 year has passed since last update.

MCP試験 70-483 Programming in C# の学習材料。

目次はこちら

3-4 アプリケーションをデバッグする

C#プログラムをデバッグするための前提知識。

DebugビルドとReleaseビルド

Visual StudioのC#プロジェクトではDebugとReleaseの2種類のビルドが用意されている。

  • Debugビルド:デバッグに最適化された構成
  • Releaseビルド:パフォーマンスに最適化された構成

具体的な構成の違いは下記の通り。

Debug Release
定義される定数 DEBUG, TRACE TRACE
コードの最適化 無し 有り
デバッグ情報 full pdb-only

DEBUG定数とTRACE定数

DebugビルドではDEBUG、TRACEの2つの定数が定義される。ReleaseビルドではTRACE定数のみが定義される。DEBUG定数とTRACE定数は主に下記の用途で使用される。

  • DebugクラスとTraceクラス
  • ConditionalAttribute
  • プリプロセッサ(#ifディレクティブ)

Visual Studioではプロジェクトを右クリック ⇒ プロパティ ⇒ ビルド ⇒ 全般-「DEBUG定数の定義」「TRACE定数の定義」で設定されている。

3-4-defineconsts.png

DebugクラスとTraceクラス

System.Diagnostics.Debug クラスと System.Diagnostics.Traceクラスには、Assert, Fail, Write, Indent, Unindent など、アプリケーションログやテスト(アサーション)に有用なメソッドやプロパティが用意されている。

Debugクラスのメソッドは、DEBUG定数が定義されている(Debugビルドの既定)場合のみ実行される。Traceクラスのメソッドは、TRACE定数が定義されている(DebugビルドとReleaseビルドの既定)場合のみ実行される。

サンプルコード

このコードをDebugビルドとReleaseビルドで実行すると、

Debug.WriteLine("debug");
Trace.WriteLine("trace");

// WriteIf: 第一引数が true の場合、メッセージを出力
int size = 1234;
Debug.WriteLineIf(size > 1000, "debug: sizeが1000を超えた!");
Trace.WriteLineIf(size > 1000, "trace: sizeが1000を超えた!");

// Assert: 第一引数が false の場合、プログラムを中断してメッセージを出力
int index = -1;
Debug.Assert(index >= 0, "debug: index must be a positive integer!");
Trace.Assert(index >= 0, "trace: index must be a positive integer!");

Debugビルドで実行した場合の出力:Debug, Trace両方のメッセージが出力

debug
trace
debug: sizeが1000を超えた!
trace: sizeが1000を超えた!
---- デバッグ アサート失敗 ----
---- 短いメッセージのアサート ----
debug: index must be a positive integer!
---- 長いメッセージのアサート ----

※メッセージダイアログ※

Releaseビルドで実行した場合の出力:Debugメッセージは出力されない

trace
trace: sizeが1000を超えた!
---- デバッグ アサート失敗 ----
---- 短いメッセージのアサート ----
trace: index must be a positive integer!
---- 長いメッセージのアサート ----

※メッセージダイアログ※

※VisualStudioで実行する場合、Debug, Traceで出力したメッセージは出力ウィンドウの出力先:デバッグに表示される。出力ウィンドウがない場合は、メニューの デバッグ⇒ウィンドウ⇒出力 で表示できる。

リンク

ConditionalAttribute

定数が定義されていないときにメソッドの呼び出しを無視するようコンパイラに指示するための属性。デバッグビルド時にだけ実行したいメソッドを作りたくなった時などに使う。

サンプルコード

class Program
{
    static void Main(string[] args)
    {
        WriteDebugMessage("てすと");
    }

    // このメソッドは DEBUG 定数が定義されている時だけ実行される
    [Conditional("DEBUG")]
    static void WriteDebugMessage(string message)
    {
        Console.WriteLine("Releaseビルドでは実行されないよ!");
    }
}

Debug/Traceクラスのメソッド

DebugクラスとTraceクラスの各メソッドには ConditionalAttribute が付けられている。

// Debug.WriteLine メソッドの定義
[ConditionalAttribute("DEBUG")]
public static void WriteLine(
    object value
)
// Trace.WriteLine メソッドの定義
[ConditionalAttribute("TRACE")]
public static void WriteLine(
    object value
)

リンク

#ifディレクティブ

ifディレクティブはプリプロセッサディレクティブの一つで、ビルドで定義されている定数によってコンパイルするコードを切り変えることができる。ConditionalAttributeと似ているが、ifディレクティブはメソッド単位ではなくソースコードの任意の行の範囲に適用できる。

サンプルコード

namespace ConsoleApp8
{
    class Program
    {
        static void Main(string[] args)
        {
#if DEBUG
            Console.WriteLine("DEBUG定数が定義されている");
#endif

#if RELEASE
            Console.WriteLine("RELEASE定数なんてないよ!");
#elif TRACE
            Console.WriteLine("TRACE定数が定義されている");
#else
            Console.WriteLine("RELEASE定数もTRACE定数も定義されていない");
#endif
        }
    }
}

Debugビルド(DEBUG, TRACE定数が定義)の時は次のコードとしてコンパイルされる

namespace ConsoleApp8
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("DEBUG定数が定義されている");
            Console.WriteLine("TRACE定数が定義されている");
        }
    }
}

Releaseビルド(TRACE定数が定義)の時は次のコードとしてコンパイルされる

namespace ConsoleApp8
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("TRACE定数が定義されている");
        }
    }
}

DEBUG, TRACE以外の定数定義

ビルドにはDEBUG, TRACE以外の定数も使用できる。Visual Studioでは、プロジェクトを右クリック ⇒ プロパティ ⇒ ビルド ⇒ 全般-条件付きコンパイルシンボル に好きな定数を定義できる。

3-4-defineconsts2.png

リンク

プリプロセッサディレクティブには、#if以外にも #define, #warning, #error, #region, #pragma などがある。詳細は次のリンク先で。C# プリプロセッサ ディレクティブ

#ifディレクティブとConditionalAttributeの違いについては次のページで解説されている。条件付きの呼び出し (ConditionalAttribute) - Programming/.NET Framework - 総武ソフトウェア推進所

コードの最適化

コードの最適化を有効にすると、コンパイラによって実行されないコードが除外されたりより効率の良い等価なコードに置き換えられたりする。既定ではDebugビルドでは最適化は無効化され、Releaseビルドでは最適化が有効化されている。

3-4-optimize.png

リンク

/optimize (C# コンパイラ オプション)

PDBファイル(シンボルファイル)

PDB(Program DataBase)ファイルは、ソースコードのステートメントとEXEファイルの実行命令を対応付けるファイルで、シンボルファイルとも言う。

このPDBファイルがあると、アプリケーションがクラッシュしたときにどのソースコードファイルの何行目でエラーが発生したかなど、デバッグに有用な情報を得ることができる。

3-4-pdbfile.png

PDBファイルがないときのスタックトレース:ソースコードファイルと行番号がわからなくてデバッグに困る

System.ArgumentNullException: 値を Null にすることはできません。
   場所 ConsoleApp8.Program.C()
   場所 ConsoleApp8.Program.B()
   場所 ConsoleApp8.Program.A()
   場所 ConsoleApp8.Program.Main(String[] args)

PDBファイルがあるときのスタックトレース:ソースコードファイルと行番号がわかってデバッグが捗る

System.ArgumentNullException: 値を Null にすることはできません。
   場所 ConsoleApp8.Program.C() 場所 C:\Users\ichiro.tanaka\Documents\Visual Studio 2017\Projects\ConsoleApp8\ConsoleApp8\Program.cs:行 38
   場所 ConsoleApp8.Program.B() 場所 C:\Users\ichiro.tanaka\Documents\Visual Studio 2017\Projects\ConsoleApp8\ConsoleApp8\Program.cs:行 33
   場所 ConsoleApp8.Program.A() 場所 C:\Users\ichiro.tanaka\Documents\Visual Studio 2017\Projects\ConsoleApp8\ConsoleApp8\Program.cs:行 28
   場所 ConsoleApp8.Program.Main(String[] args) 場所 C:\Users\ichiro.tanaka\Documents\Visual Studio 2017\Projects\ConsoleApp8\ConsoleApp8\Program.cs:行 16

ちなみにPDBファイルにはソースコードのフルパスが埋め込まれている。なのでうっかりしてると↑のように開発者のユーザ名が漏れたりするので気を付けよう。といってもEXEファイルにもファイルパスは残るので、そもそもパスにユーザ名等が含まれない場所でビルドするのが良い。

デバッグ情報の設定

デバッグ情報のレベルを変えることもできる。既定ではDebugビルドではfull、Releaseビルドではpdb-onlyという設定になっている。

Visual Studioでは、プロジェクトを右クリック ⇒ プロパティ ⇒ ビルド ⇒ 出力 ⇒ 詳細設定 ⇒ 「ビルドの詳細設定」 ⇒ 出力-デバッグ情報 で設定を変更できる。

3-4-debug.png

リンク

Exercise

ビルドとPDBファイルとスタックトレース

ビルドの種類とPDBファイルの有無で、スタックトレースにどのような違いが出るか?

実施手順

  1. 下記のコードをDebugビルドでコンパイル
  2. 生成された.exeファイルを実行し、表示されるスタックトレースを見る
  3. 同じディレクトリにある.pdbファイルを消したあと再度.exeファイル実行し、出力されるスタックトレースを見る
  4. Releaseビルドで1~3を実施し、下記パターンのスタックトレースを比較する
    • Debugビルド / PDBファイル有り
    • Debugビルド / PDBファイル無し
    • Releaseビルド / PDBファイル有り
    • Releaseビルド / PDBファイル無し
class Program
{
    static void Main(string[] args)
    {
        try
        {
            A();
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }

        Console.ReadLine();
    }

    static void A()
    {
        B();
    }

    static void B()
    {
        C();
    }

    static void C()
    {
        throw new NotImplementedException();
    }
}

コードの最適化とは

最適化の有効・無効ではどんな違うがでる?

  1. 下記のコードをDebugビルド(または最適化無し)でコンパイルし、生成された IL を見る
  2. 下記のコードをReleaseビルド(または最適化有り)でコンパイルし、生成された IL を見る
void Main()
{
    string a = "  abcdefg  ";
    string b = a.Substring(2);
    string c = b.Remove(0, 2);
    string d = c.Trim();

    Console.WriteLine(d);
}

LINQPadで最適化の有効・無効を切り替えるには、メニュー ⇒ Edit ⇒ Preferences ⇒ Query ⇒ Query Optimization

3-4-linqpad.png

生成される IL は IL タブで見れる

3-4-linqpad-il.png

ILをVisual Studioで見たい場合は Ildasm.exe (IL 逆アセンブラー) が使える。