C# の Conditional 属性でデバッグ専用メソッドを作る

開発中はログ出力やバリデーションチェックを入れたいが、リリースビルドではそれらを取り除きたい、という場面は多い。Conditional 属性を使うと、特定のプリプロセッサシンボルが定義されているときだけメソッド呼び出しが有効になり、定義されていないときは呼び出し自体がコンパイラによって除去される。

基本的な使い方

Conditional 属性は System.Diagnostics 名前空間にある。メソッドに付与し、引数にプリプロセッサシンボル名を文字列で指定する。

using System;
using System.Diagnostics;

public class Logger
{
    [Conditional("DEBUG")]
    public static void DebugLog(string message)
    {
        Console.WriteLine($"[DEBUG] {message}");
    }
}

DEBUG シンボルが定義されている(通常は Debug ビルド構成)ときは DebugLog の呼び出しが通常どおり実行される。Release ビルドのように DEBUG が未定義の場合、呼び出し箇所がまるごと消える。メソッド本体はアセンブリに残るが、呼び出し側のコードから参照されなくなるという仕組みだ。

if ディレクティブとの違い

同じことは #if DEBUG でもできるが、Conditional 属性にはいくつかの利点がある。

#if DEBUG ディレクティブ

呼び出し側のコードを #if で囲む必要がある。呼び出し箇所が多いとコードが煩雑になり、囲み忘れのリスクもある。

Conditional 属性

メソッド定義に 1 回付けるだけで、すべての呼び出し箇所が自動的に制御される。呼び出し側のコードはクリーンなまま保てる。

具体的に見てみよう。

// #if を使う場合(呼び出し側が煩雑)
public void ProcessData()
{
#if DEBUG
    DebugLog("処理開始");
#endif
    // 実際の処理
#if DEBUG
    DebugLog("処理完了");
#endif
}

// Conditional 属性を使う場合(呼び出し側はシンプル)
public void ProcessData()
{
    DebugLog("処理開始");
    // 実際の処理
    DebugLog("処理完了");
}

後者のほうが圧倒的に読みやすい。呼び出し箇所が増えるほど、この差は大きくなる。

制約と注意点

Conditional 属性にはいくつかの制約がある。まず、戻り値を持つメソッドには付けられない。呼び出しが除去されたとき、戻り値をどう扱うかが曖昧になるためだ。

// これはコンパイルエラーになる
[Conditional("DEBUG")]
public static int GetDebugValue()
{
    return 42;
}

戻り値の型は必ず void でなければならない。また、out パラメータを持つメソッドにも付けられない。

戻り値は void のみ

呼び出しが除去されると戻り値が未定義になるため、void 以外は禁止されている。

out パラメータ禁止

out 引数も呼び出し除去時に値が未初期化になるため使えない。

ref パラメータは可

ref は呼び出し前に初期化済みなので、呼び出しが除去されても問題ない。

複数のシンボルを指定する

Conditional 属性を複数回付けると、いずれかのシンボルが定義されていれば呼び出しが有効になる。OR 条件として機能する。

[Conditional("DEBUG")]
[Conditional("TRACE")]
public static void Log(string message)
{
    Console.WriteLine(message);
}

DEBUGTRACE のどちらかが定義されていれば Log が呼ばれる。AND 条件にしたい場合は、Conditional 属性だけでは実現できないため、#if との組み合わせやカスタムシンボルの定義で対応する必要がある。

カスタムシンボルを使う

DEBUGTRACE だけでなく、独自のシンボルを定義して使うこともできる。

// プロジェクトの設定や #define で PERFORMANCE_LOG を定義
#define PERFORMANCE_LOG

using System;
using System.Diagnostics;

public class Profiler
{
    [Conditional("PERFORMANCE_LOG")]
    public static void LogElapsed(string label, long ms)
    {
        Console.WriteLine($"[PERF] {label}: {ms}ms");
    }
}

パフォーマンス計測ログのように、デバッグとは別の目的で条件付き実行したい場面に便利だ。プロジェクトファイルの DefineConstants にシンボルを追加すれば、ファイル先頭の #define なしでプロジェクト全体に適用できる。

Debug.Assert との関係

.NET 標準の Debug.AssertDebug.WriteLine も内部的に Conditional("DEBUG") が付いている。つまり、Release ビルドでは自動的に除去される。自前のデバッグ用メソッドを作るときも同じパターンを踏襲することで、ビルド構成による振る舞いの一貫性を保てる。Conditional 属性は、コードの可読性を損なわずにビルド構成ごとの挙動を切り替える、実用的な仕組みだといえる。