#ifディレクティブ(C#)/#If ... Thenディレクティブ(VB)を用いることにより、コンパイラ定数(シンボル)に基づく条件付きコンパイルを行うことができます。 これは、コンパイラ定数が定義されているか否かにしたがって、#if#endif/#If#End Ifディレクティブ内のコードをコンパイル対象とするかどうかコンパイラに選択させるものです。 (§.#ifディレクティブ)

これとは別に、.NETではConditional属性を使用することができます。 Conditional属性は主にメソッドに対して指定する属性で、この属性が指定されたメソッドは、特定のコンパイラ定数が定義されている場合にのみ呼び出しが行われるようになります。 (§.Conditional属性§.属性に対するConditional属性 (Conditionalな属性クラス))

#ifディレクティブとConditional属性は互いに似た機能を持つものですが、Conditional属性を用いることにより、#ifディレクティブでの条件付きコンパイルとメソッド呼び出しにまつわる問題点を解決することができます。

ここでは#ifディレクティブとConditional属性について解説します。 また、.NETのクラスライブラリでConditional属性が活用されている例として、Debugクラス・Traceクラスについても取り上げます。 前提となる事項として、コンパイラ定数とその定義方法についても解説します。 constreadonlyなどのいわゆる定数との違い・比較については§.コンパイラ定数・const/readonly・他の言語との違いを参照してください。

このほか、アプリケーション構成ファイルに基づいて実行コードを切り替えるSwitchクラスについても触れています。

概略

#ifディレクティブConditional属性は、どちらもコンパイラ定数(シンボルとも呼ばれる)に基づいて条件付きのコンパイル(conditional compilation)を行うための機構です。

コンパイラ定数と条件付きコンパイルを用いると、なんらかの条件付きでコンパイルしたいコードを、コンパイル結果(成果物)に含めるかどうかコンパイル時に選択させることができます。 例えば、ログ出力やデバッグ出力といった、特定のビルド構成(あるいは単に構成configuration, 詳細)のみで有効としたいコード、あるいは特定のランタイム・プラットフォームなどの対象(target)によって異なる実装となるコードなどがこれにあたります。

#ifディレクティブとConditional属性はどちらもコンパイラ定数に基づく条件付きコンパイルの対象となる点は同じですが、#ifディレクティブではディレクティブ内のコードブロックが条件付きでコンパイルされる一方、Conditional属性ではこの属性が付与されたメソッド自体ではなく、そのメソッドの呼び出しが条件付きでコンパイルされます

#ifディレクティブとConditional属性を使った条件付きコンパイルの例
using System;
using System.Diagnostics;
using System.Text;

class Sample {
  // コンパイラ定数"DEBUG"が定義されている場合のみ有効となるメソッド
  [Conditional("DEBUG")]
  static void Log(string message)
  {
    // プラットフォームごとに異なる実装をコンパイラ定数によって分けて記述する
    // (以下の各コンパイラ定数が定義されている場合のみ、そのコードがコンパイル対象となる)
#if WINDOWS
    // Windows固有のログ機構に出力する
    System.Diagnostics.EventLog.WriteEntry("sample", message);
#elif LINUX
    // Linux固有のログ機構に出力する
    Mono.Unix.Native.Syscall.syslog(SyslogLevel.LOG_DEBUG, message);
#else
    // デフォルトあるいはフォールバックのログ機構に出力する
    System.Diagnostics.Trace.WriteLine(message);
#endif
  }

  static void Main()
  {
    // このメソッド呼び出しはDEBUGが定義されている場合のみ実行される
    Log("START");

    shift_jis.GetBytes("こんにちは、世界!");
  }

  static readonly Encoding shift_jis =
#if NETFRAMEWORK
    // .NET FrameworkではEncodingクラスから直接Shift_JISエンコーディングを取得する
    Encoding.GetEncoding("shift_jis");
#else
    // それ以外(.NET Core/.NET)ではSystem.Text.Encoding.CodePages.dllの
    // CodePagesEncodingProviderからShift_JISエンコーディングを取得する
    CodePagesEncodingProvider.Instance.GetEncoding("shift_jis");
#endif
}
#ifディレクティブとConditional属性を使った条件付きコンパイルの例
Imports System
Imports System.Diagnostics
Imports System.Text

Class Sample
  ' コンパイラ定数"DEBUG"が定義されている場合のみ有効になるメソッド
  <Conditional("DEBUG")> _
  Shared Sub Log(ByVal message As String)
    ' プラットフォームごとに異なる実装をコンパイラ定数によって分けて記述する
    ' (以下の各コンパイラ定数が定義されている場合のみ、そのコードがコンパイル対象となる)
#If WINDOWS Then
    ' Windows固有のログ機構に出力する
    System.Diagnostics.EventLog.WriteEntry("sample", message)
#Else If LINUX Then
    ' Linux固有のログ機構に出力する
    Mono.Unix.Native.Syscall.syslog(SyslogLevel.LOG_DEBUG, message)
#Else
    ' デフォルトあるいはフォールバックのログ機構に出力する
    System.Diagnostics.Trace.WriteLine(message)
#End If
  End Sub

  Shared Sub Main()
    ' このメソッド呼び出しはDEBUGが定義されている場合のみ実行される
    Log("START")

    shift_jis.GetBytes("こんにちは、世界!")
  End Sub

#If NETFRAMEWORK Then
  ' NET FrameworkではEncodingクラスから直接Shift_JISエンコーディングを取得する
  Shared ReadOnly shift_jis As Encoding = Encoding.GetEncoding("shift_jis")
#Else
  ' それ以外(.NET Core/.NET)ではSystem.Text.Encoding.CodePages.dllの
  ' CodePagesEncodingProviderからShift_JISエンコーディングを取得する
  Shared ReadOnly shift_jis As Encoding = CodePagesEncodingProvider.Instance.GetEncoding("shift_jis")
#End If
End Class

#ifディレクティブとConditional属性は単にどちらか一方を置き換えるものとはならず、相補的なものです。 どのような場面でConditional属性が有効に機能するかについては§.#ifディレクティブによる条件付き呼び出しの問題点とConditional属性で解説します。

ログ出力やデバッグ出力、アサーションを目的とする場合、独自にメソッドを用意することもできますが、.NETのクラスライブラリではDebugクラス・Traceクラスが用意されているため、これを用いることもできます。

.NET Core/.NET 5以降の形式のプロジェクトファイルでは、対象とするフレームワークによってNET(.NET 5以降), NETFRAMEWORK(.NET Framework), NETCOREAPP(.NET Core), NETSTANDARD(.NET Standard)などのコンパイラ定数があらかじめ定義されるため、対象フレームワークごとにサポートされるAPIの差異により異なるコードを記述する必要がある場合は、これらのコンパイラ定数を使用して条件付きコンパイルできるようになっています。 これについては§.ターゲットフレームワークごとに定義されるコンパイラ定数を参照してください。

条件付きのコンパイルは、コンパイル時に特定の構成・ランタイム・プラットフォーム向けのコードを分岐して生成するものです。 実行時に特定の構成での実行コードを選択する方法については§.Switchクラス、ランタイムやプラットフォームを判定して分岐する方法についてはランタイム・システム・プラットフォームの情報を参照してください。

#ifディレクティブ

#if#endifディレクティブ(C#)/#If ... Then#End Ifディレクティブ(VB)は、条件付きコンパイルを行うためのプリプロセッサディレクティブ(前処理命令, preprocessor directive)です。

このディレクティブ内で記述されるコードブロックは、指定されたコンパイラ定数が定義されている場合は通常通りにコンパイル対象としてコンパイルされ、逆に定義されていない場合はコメント文などと同様にコンパイル対象から除外され、無視されます。

#ifディレクティブでは、条件分岐構文のelse, else ifと同様に、#else/#Else#elif/#ElseIf ... Thenディレクティブと組み合わせて条件分岐を構成することができます。

例として、Debug構成・Release構成を表す定数DEBUG, RELEASEを使い、それぞれの構成の時だけ有効になるコードを記述すると次のようになります。

#ifディレクティブで条件付きコンパイルを行う
using System;

class Sample {
  static void Main()
  {
#if DEBUG
    // このコードはDEBUGが定義されている場合のみコンパイルされる
    Console.WriteLine("DEBUG");
#endif

#if DEBUG
    // このコードはDEBUGが定義されている場合のみコンパイルされる
    Console.WriteLine("Hello, debugger!");
#elif RELEASE
    // このコードはRELEASEが定義されている場合のみコンパイルされる
    Console.WriteLine("Hello, world!");
#else
    // このコードはどちらも定義されていない場合のみコンパイルされる
    Console.WriteLine("Welcome to Underground");
#endif
  }
}
#ifディレクティブで条件付きコンパイルを行う
Imports System

Class Sample
  Shared Sub Main()
#If DEBUG Then
    ' このコードはDEBUGが定義されている場合のみコンパイルされる
    Console.WriteLine("DEBUG")
#End If

#If DEBUG Then
    ' このコードはDEBUGが定義されている場合のみコンパイルされる
    Console.WriteLine("Hello, debugger!")
#Else If RELEASE Then
    ' このコードはRELEASEが定義されている場合のみコンパイルされる
    Console.WriteLine("Hello, world!")
#Else
    ' このコードはどちらも定義されていない場合のみコンパイルされる
    Console.WriteLine("Welcome to Underground")
#End If
  End Sub
End Class
Debug構成でのビルドの場合
実行結果
DEBUG
Hello, debugger!
Release構成でのビルドの場合
実行結果
Hello, world!
左記以外の構成でのビルドの場合
実行結果
Welcome to Underground

このほか、#ifディレクティブは入れ子にすることができます。 演算子として、C#では&&, ||, !, ==, !=が使用でき、VBではAnd/AndAlso, Or/OrElse, =, <>が使用できます(Notは使用できない)。 コンパイラ定数との比較の際、リテラルとしてtrue/false(C#), True/False(VB)を使用できます。 条件式の優先順位指定のために(, )を使用できます。

Debug構成・Release構成など、プロジェクトファイルにおける構成と、構成ごとにコンパイラ定数を定義する方法等についてはプロジェクトファイル §.ビルド構成 (Configurationプロパティ)を参照してください。

#ifディレクティブによる条件付き呼び出しの問題点とConditional属性

#ifディレクティブを用いることにより、コンパイラ定数に従ってコンパイルするコードを切り替えることができます。 一方、#ifディレクティブによる切り替えには以下に挙げるような問題点もあります。

以下、例としてコンパイラ定数DEBUGが定義されている場合のみ、動作ログを記録する処理を実行したい場合を考えます。 ここでは動作ログを記録するメソッドLog(string)を用意し、それを呼び出すことにします。

まず、DEBUGが定義されている場合のみ動作ログを記録したいため、メソッド全体を#ifディレクティブ内に記述して条件付きコンパイルするようにしたとします。

すると、DEBUGが定義されていない場合このメソッドはコンパイルされず、メソッド自体が存在しないものとなり、呼び出しはエラーとなります。

コンパイラ定数が定義されている場合のみ実行するメソッドを作成したい
using System;

class Sample {
  // DEBUGが定義されている場合のみ動作ログを記録したい
#if DEBUG
  static void Log(string message)
  {
    Console.WriteLine(message);
  }
#endif

  static void Main()
  {
    // DEBUGが定義されていない場合、メソッド自体が
    // コンパイルされず、存在しないものとなるため、
    // 呼び出しがコンパイルエラーとなってしまう
    Log("START"); // error CS0103: 現在のコンテキストに 'Log' という名前は存在しません
  }
}
コンパイラ定数が定義されている場合のみ実行するメソッドを作成したい
Imports System

Class Sample
  ' DEBUGが定義されている場合のみ動作ログを記録したい
#If DEBUG Then
  Shared Sub Log(ByVal message As String)
    Console.WriteLine(message)
  End Sub
#End If

  Shared Sub Main()
    ' DEBUGが定義されていない場合、メソッド自体が
    ' コンパイルされず、存在しないものとなるため、
    ' 呼び出しがコンパイルエラーとなってしまう
    Log("START") ' error BC30451: 'Log' は宣言されていません。アクセスできない保護レベルになっています。
  End Sub
End Class

そこで、呼び出し側も#ifディレクティブで条件付きコンパイルされるようにします。 これで呼び出し側がコンパイルエラーとなることはなくなります。

しかし、呼び出し箇所が数箇所に限られる場合ならともかく、その数が多くなる場合を考慮すると、呼び出しすべてに#ifディレクティブを記述する必要があり、これは非常に面倒です。

また、使用する定数をDEBUG以外に変更する場合や、条件を他の定数との組み合わせに変える(例えばDEBUG && LOGなどに変更する)場合を考慮すると、保守性にも欠けます。

C/C++のマクロに相当するものがあれば、条件付きコンパイルされる呼び出しをマクロ化して使うことができますが、C#/VBにはそれに相当するものがありません。

#ifディレクティブでコンパイラ定数が定義されている場合のみ有効になるメソッドを作成する
using System;

class Sample {
  // DEBUGが定義されている場合のみ動作ログを記録したい
#if DEBUG
  static void Log(string message)
  {
    Console.WriteLine(message);
  }
#endif

  static void Main()
  {
    // 呼び出し側も条件付きでコンパイルされるようにすれば
    // コンパイルエラーにはならなくなる
#if DEBUG
    Log("START");
#endif

    // ただし、呼び出し箇所が複数あれば、そのすべてが
    // 条件付きでコンパイルされるようにする必要が出てくる
#if DEBUG
    Log("END");
#endif
  }
}
#ifディレクティブでコンパイラ定数が定義されている場合のみ有効になるメソッドを作成する
Imports System

Class Sample
  ' DEBUGが定義されている場合のみ動作ログを記録したい
#If DEBUG Then
  Shared Sub Log(ByVal message As String)
    Console.WriteLine(message)
  End Sub
#End If

  Shared Sub Main()
    ' 呼び出し側も条件付きでコンパイルされるようにすれば
    ' コンパイルエラーにはならなくなる
#If DEBUG Then
    Log("START")
#End If

    ' ただし、呼び出し箇所が複数あれば、そのすべてが
    ' 条件付きでコンパイルされるようにする必要が出てくる
#If DEBUG Then
    Log("END")
#End If
  End Sub
End Class

今度は、メソッドと呼び出しの両方を条件付きコンパイルするのではなく、メソッド内のコードのみを条件付きコンパイルするように変えます。 これで、目的を一応達成することができます。 記述の煩雑さもなく、保守性の点でも良好なので、場合によってはこれで良しとすることもできます。

ただし、メソッド呼び出し自体は行われるため、引数の評価も行われることになります。 つまり、メソッド内の処理はDEBUGが定義されている場合にのみ実行される一方、そのメソッドの呼び出し側の処理はDEBUGが定義されていなくても実行されることになります。 何らかのメソッド呼び出しの結果やプロパティの値を引数として渡す場合、そのメソッドの実行・プロパティの参照自体は行われます。

このため、何もしないメソッドのために、不要な処理が行われる、無用なオーバーヘッドが生じることになります。

これまでのように、引数として評価される可能性のあるメソッド・プロパティの実装を条件付きコンパイルするようにすることもできますが、呼び出し自体が行われることに変わりはなく、また呼び出される側が多くなれば、その分だけ記述が煩雑化することになります。

#ifディレクティブでコンパイラ定数が定義されている場合のみ処理が有効になるメソッドを作成する
using System;

class Sample {
  // DEBUGが定義されている場合のみ動作ログを記録する
  static void Log(string message)
  {
    // メソッド全体ではなく、メソッド内の処理を
    // 条件付きでコンパイルするようにする
#if DEBUG
    Console.WriteLine(message);
#endif
  }

  static void Main()
  {
    // これでコンパイルエラーにもならず、
    // 記述もシンプルにできる
    Log("START");

    // ただし、メソッドの呼び出し自体は行われる
    // 加えて、引数の評価(ここではGetMessageの呼び出し)も行われる
    Log(GetMessage());
  }

  // 引数の評価で呼び出されるメソッドが'重い'処理を行う場合、
  // 何もしないメソッド呼び出しのために'重い'処理が
  // 行われることになる
  static string GetMessage()
  {
    // 何らかの'重い'処理を行う場合を想定する
    Console.WriteLine("processing...");

    // 処理の結果を返す
    return "END";
  }
}
#ifディレクティブでコンパイラ定数が定義されている場合のみ処理が有効になるメソッドを作成する
Imports System

Class Sample
  ' DEBUGが定義されている場合のみ動作ログを記録する
  Shared Sub Log(ByVal message As String)
    ' メソッド全体ではなく、メソッド内の処理を
    ' 条件付きでコンパイルするようにする
#If DEBUG Then
    Console.WriteLine(message)
#End If
  End Sub

  Shared Sub Main()
    ' これでコンパイルエラーにもならず、
    ' 記述もシンプルにできる
    Log("START")

    ' ただし、メソッドの呼び出し自体は行われる
    ' 加えて、引数の評価(ここではGetMessageの呼び出し)も行われる
    Log(GetMessage())
  End Sub

  ' 引数の評価で呼び出されるメソッドが'重い'処理を行う場合、
  ' 何もしないメソッド呼び出しのために'重い'処理が
  ' 行われることになる
  Shared Function GetMessage() As String
    ' 何らかの'重い'処理を行う場合を想定する
    Console.WriteLine("processing...")

    ' 処理の結果を返す
    Return "END"
  End Function
End Class
DEBUGが定義されている場合
実行結果
START
processing...
END
DEBUGが定義されていない場合
実行結果
processing...

Conditional属性を使うと、ここまでに挙げた問題の解決、つまり条件付きのメソッド呼び出し引数評価の無効化を同時に行うことができます。

Conditional属性が指定されたメソッドでは、コンパイラ定数が定義されている場合は通常のメソッドと同様に動作します。 一方、コンパイラ定数が定義されていない場合は、そのメソッドの呼び出し自体がコンパイル時に削除されます。 メソッド呼び出し自体が削除されるため、それに伴う実行時の引数の評価自体も行われなくなります。

Conditional属性を使ってコンパイラ定数が定義されている場合のみ実行されるメソッドを作成する
using System;
using System.Diagnostics;

class Sample {
  // DEBUGが定義されている場合のみ動作ログを記録する
  [Conditional("DEBUG")]
  static void Log(string message)
  {
    Console.WriteLine(message);
  }

  static void Main()
  {
    // 呼び出し側は通常のメソッド呼び出しと同様に行える
    // ただし、コンパイラ定数DEBUGが定義されていない場合は
    // コンパイル時にメソッドの*呼び出し自体が削除*されるため、
    // メソッドは実行されなくなる
    Log("START");

    // メソッドの呼び出し自体が削除されるため、実行時における
    // 引数の評価(ここではGetMessageの呼び出し)も行われなくなる
    Log(GetMessage());
  }

  // DEBUGが定義されていない場合、引数の評価が行われなくなるため
  // このメソッドの呼び出しも行われなくなる
  static string GetMessage()
  {
    Console.WriteLine("processing...");

    return "END";
  }
}
Conditional属性を使ってコンパイラ定数が定義されている場合のみ実行されるメソッドを作成する
Imports System
Imports System.Diagnostics

Class Sample
  ' DEBUGが定義されている場合のみ動作ログを記録する
  <Conditional("DEBUG")> _
  Shared Sub Log(ByVal message As String)
    Console.WriteLine(message)
  End Sub

  Shared Sub Main()
    ' 呼び出し側は通常のメソッド呼び出しと同様に行える
    ' ただし、コンパイラ定数DEBUGが定義されていない場合は
    ' コンパイル時にメソッドの*呼び出し自体が削除*されるため、
    ' メソッドは実行されなくなる
    Log("START")

    ' メソッドの呼び出し自体が削除されるため、実行時における
    ' 引数の評価(ここではGetMessageの呼び出し)も行われなくなる
    Log(GetMessage())
  End Sub

  ' DEBUGが定義されていない場合、引数の評価が行われなくなるため
  ' このメソッドの呼び出しも行われなくなる
  Shared Function GetMessage() As String
    Console.WriteLine("processing...")

    Return "END"
  End Function
End Class

DEBUGが定義されている場合
実行結果
START
processing...
END
DEBUGが定義されていない場合
実行結果

(何も出力されない)

Conditional属性

Conditional属性は、コンパイラ定数に従って条件付きのメソッド呼び出しを行うための属性です。 この属性が付与されたメソッドを呼び出す場合、属性で指定されているコンパイラ定数が定義されている場合に限り呼び出しが行われます。

Conditional属性による条件付きのメソッド呼び出し
using System;
using System.Diagnostics;

class Sample {
  // コンパイラ定数"DEBUG"が定義されている場合のみ有効になるメソッド
  [Conditional("DEBUG")]
  static void Log(string message)
    => Console.WriteLine(message);

  static void Main()
  {
    // コンパイラ定数"DEBUG"が定義されている場合は通常のメソッドと同様に呼び出される
    // 定義されていない場合は、呼び出しが行われない (コンパイル時に呼び出しが削除される)
    Log("START");

    Console.WriteLine("Hello, world!");

    Log("END");
  }
}
Conditional属性による条件付きのメソッド呼び出し
Imports System
Imports System.Diagnostics

Class Sample
  ' コンパイラ定数"DEBUG"が定義されている場合のみ有効になるメソッド
  <Conditional("DEBUG")> _
  Shared Sub Log(ByVal message As String)
    Console.WriteLine(message)
  End Sub
  
  Shared Sub Main()
    ' コンパイラ定数"DEBUG"が定義されている場合は通常のメソッドと同様に呼び出される
    ' 定義されていない場合は、呼び出しが行われない (コンパイル時に呼び出しが削除される)
    Log("START")

    Console.WriteLine("Hello, world!")

    Log("END")
  End Sub
End Class
DEBUGが定義されている場合
実行結果
START
Hello, world!
END
DEBUGが定義されていない場合
実行結果
Hello, world!

Conditional属性を用いると#ifディレクティブと同様にコンパイラ定数に基づいた条件付きの呼び出しを行うことができます。 一方で#ifディレクティブと異なる点もいくつかあります。

Conditional属性が付与されたメソッドは、コンパイル結果からその呼び出しが削除されます。 そのため、#ifディレクティブとは異なり、コードのコンパイル自体は行われ、メソッド呼び出し部分の文法チェックや引数の型チェックは通常のメソッド呼び出しと同様に行われます。 それらのチェックとコンパイルが行われた後に、最終的なコンパイル結果からメソッド呼び出し部分が除外されることになります。

また、#ifディレクティブとは異なり、Conditional属性が付与されたメソッド自体は削除されません(コンパイル結果に含まれる)。 そのため、nameof/NameOf演算子でメソッドを参照することができ、リフレクションにより呼び出すこともできます。

Conditional属性による条件付き呼び出しは、コンパイラによって提供される機能です。 このため、Conditional属性が付与されたメソッドのデリゲートを作成する場合や、Conditional属性が付与されたメソッドをオーバーロードする場合・オーバーロードしてConditional属性を付与する場合の動作は、言語(コンパイラ)によって扱いが異なります。


Conditional属性が付与されている場合、メソッド呼び出しが削除されます。 これに伴い、メソッドに渡される引数の実行時における評価も行われなくなります。 このため、別のメソッドの結果を直接引数として渡すような場合、そのメソッドの呼び出しも行われなくなることになります。

Conditional属性による呼び出しの削除に伴って引数も評価されなくなる
using System;
using System.Diagnostics;

class Sample {
  // コンパイラ定数"DEBUG"が定義されている場合のみ有効になるメソッド
  [Conditional("DEBUG")]
  static void Log(string message)
    => Console.WriteLine(message);

  static void Main()
  {
    Console.WriteLine("Hello, world!");

    // Conditionalなメソッドの引数として他のメソッドの結果を渡す
    // (Logの呼び出しが削除されるため、引数の評価も行われなくなる
    // その結果GetMessageの呼び出しは実行されなくなる)
    Log(GetMessage());
  }

  static string GetMessage()
  {
    Console.WriteLine(nameof(GetMessage));
    return "Hello, logger!";
  }
}
Conditional属性による呼び出しの削除に伴って引数も評価されなくなる
Imports System
Imports System.Diagnostics

Class Sample
  ' コンパイラ定数"DEBUG"が定義されている場合のみ有効になるメソッド
  <Conditional("DEBUG")> _
  Shared Sub Log(ByVal message As String)
    Console.WriteLine(message)
  End Sub
  
  Shared Sub Main()
    Console.WriteLine("Hello, world!")

    ' Conditionalなメソッドの引数として他のメソッドの結果を渡す
    ' (Logの呼び出しが削除されるため、引数の評価も行われなくなる
    ' その結果GetMessageの呼び出しは実行されなくなる)
    Log(GetMessage())
  End Sub

  Shared Function GetMessage() As String
    Console.WriteLine(NameOf(GetMessage))
    Return "Hello, logger!"
  End Function
End Class
DEBUGが定義されている場合
実行結果
Hello, world!
GetMessage
Hello, logger!
DEBUGが定義されていない場合
実行結果
Hello, world!

インクリメント(++/--)した結果を引数として渡す場合や、参照渡し引数のあるメソッド呼び出しの結果を渡す場合なども同様で、何らかの処理を行った結果を引数として直接渡すような場面では、コンパイラ定数が定義されているかどうかによって動作や結果が変わることになるため、注意が必要です。


Conditional属性では、コンパイラ定数の大文字小文字が区別されます。 そのため、何らかの設定やプロジェクトプロパティからコンパイラ定数の定義を行う場合は、大文字小文字の扱いの違いに注意する必要があります。

Conditional属性ではコンパイラ定数の大文字小文字が区別される
using System;
using System.Diagnostics;

class Sample {
  // コンパイラ定数"Debug"が定義されている場合のみ有効になるメソッド
  [Conditional("Debug")]
  static void Log(string message)
    => Console.WriteLine(message);

  static void Main()
  {
    // コンパイラ定数の大文字小文字は区別される
    // そのため、"Debug"が定義されていれば呼び出されるが、"DEBUG"が定義されている場合は呼び出されない
    Log("START");
  }
}
Conditional属性ではコンパイラ定数の大文字小文字が区別される
Imports System
Imports System.Diagnostics

Class Sample
  ' コンパイラ定数"Debug"が定義されている場合のみ有効になるメソッド
  <Conditional("Debug")> _
  Shared Sub Log(ByVal message As String)
    Console.WriteLine(message)
  End Sub

  Shared Sub Main()
    ' コンパイラ定数の大文字小文字は区別される
    ' そのため、"Debug"が定義されていれば呼び出されるが、"DEBUG"が定義されている場合は呼び出されない
    Log("START")
  End Sub
End Class

Conditionalなメソッドでの戻り値・out引数

Conditional属性が付与されたメソッドの呼び出しはコンパイル時に削除されるという動作となるため、Conditional属性を付与できるメソッドは値を返さない(void/Subであること)、またC#ではout引数を持たないメソッドに限られます。 これは、メソッド呼び出しが削除されると、戻り値やout引数に設定されるべき値が確定できなくなるためです。 また、同様の理由からプロパティやコンストラクタにはConditional属性を付与することはできません。

何らかの結果を出力するメソッドにConditional属性を適用したい場合は、戻り値ではなく参照渡し引数ref/ByRefを用いることにより、結果を出力させることができます。 ただし、コンパイラ定数の定義の有無により参照渡し引数の値が異なることになるため、一見すると不可解な動作となりうる点に注意が必要です。

条件付きで呼び出されるメソッドのref/ByRef引数で結果を出力する
using System;
using System.Diagnostics;

class Sample {
  // コンパイラ定数"DEBUG"が定義されている場合のみ有効になるメソッド
  // ref引数で値の入力と結果の出力を行う
  [Conditional("DEBUG")]
  static void Increment(ref int x)
  {
    x += 1;
  }

  static void Main()
  {
    int x = 0;

    // 引数で与えられる変数の値をインクリメントし、結果を同じ変数に格納する
    Increment(ref x);

    Console.WriteLine($"x = {x}");
  }
}
条件付きで呼び出されるメソッドのref/ByRef引数で結果を出力する
Imports System
Imports System.Diagnostics

Class Sample
  ' コンパイラ定数"DEBUG"が定義されている場合のみ有効になるメソッド
  ' ByRef引数で値の入力と結果の出力を行う
  <Conditional("DEBUG")> _
  Shared Sub Increment(ByRef x As Integer)
    x += 1
  End Sub

  Shared Sub Main()
    Dim x As Integer = 0

    ' 引数で与えられる変数の値をインクリメントし、結果を同じ変数に格納する
    Increment(x)

    Console.WriteLine($"x = {x}")
  End Sub
End Class
DEBUGが定義されている場合
実行結果
x = 1
DEBUGが定義されていない場合
実行結果
x = 0

複数のコンパイラ定数に対するConditional属性

Conditional属性は、ひとつのメソッドに対して複数付与することができます。 Conditional属性で複数のコンパイラ定数を指定したい場合は、定数ひとつごとにConditional属性を付与します。 ひとつのConditional属性に複数のコンパイラ定数を指定することはできません。

Conditional属性が複数付与されている場合、定義されているコンパイラ定数がひとつでもあれば、そのメソッドに対する呼び出しは削除されません。 つまり、複数のConditional属性はAND演算ではなくOR演算で結合されると見ることができます。

複数のConditional属性が付与されたメソッドの呼び出し
using System;
using System.Diagnostics;

class Sample {
  // コンパイラ定数"DEBUG"または"TEST"が定義されている場合のみ有効になるメソッド
  [Conditional("DEBUG")]
  [Conditional("TEST")]
  static void PrintMessage(string message)
    => Console.WriteLine(message);

  static void Main()
  {
    // DEBUGまたはTESTが定義されていれば、呼び出しが行われる
    PrintMessage("Hello, world!");

    // #ifディレクティブで相当するコードを書くとすれば次のようになる
#if DEBUG || TEST
    //PrintMessage("Hello, world!");
#endif
  }
}
複数のConditional属性が付与されたメソッドの呼び出し
Imports System
Imports System.Diagnostics

Class Sample
  ' コンパイラ定数"DEBUG"または"TEST"が定義されている場合のみ有効になるメソッド
  <Conditional("DEBUG")> _
  <Conditional("TEST")> _
  Shared Sub PrintMessage(ByVal message As String)
    Console.WriteLine(message)
  End Sub

  Shared Sub Main()
    ' DEBUGまたはTESTが定義されていれば、呼び出しが行われる
    PrintMessage("Hello, world!")

    ' #Ifディレクティブで相当するコードを書くとすれば次のようになる
#If DEBUG Or TEST Then
    'PrintMessage("Hello, world!")
#End If
  End Sub
End Class
DEBUGが定義されている場合
実行結果
Hello, world!
TESTが定義されていない場合
実行結果
Hello, world!
DEBUGもTESTも定義されていない場合
実行結果

(何も出力されない)

否定条件のConditional属性

Conditional属性では、否定条件の指定、つまりコンパイラ定数が定義されていない場合のみ有効とすることはできません。 NotConditionalのような属性もありません。 このため、否定条件でConditional属性を指定したい場合は、例えばDEBUGに対するNOT_DEBUGを定義するなど、否定した条件で定義されるコンパイラ定数を別に用意するのが最も有効な手段となります。

不完全な手段のひとつとして、ファイル内でコンパイラ定数を定義する#define/#Constを使用して条件を否定したコンパイラ定数を定義する方法があります。

この方法で定義したコンパイラ定数はファイル内のみで有効となるため、同一ファイル内での呼び出しに対しては正しく機能するものの、ファイル外からの呼び出しに対しては機能しない点に注意する必要があります。 完全に機能させるには、呼び出し元となるすべてのファイルで条件を否定したコンパイラ定数を定義する必要があります。

異なるファイルのConditionalなメソッドを呼び出す
class X {
  public static void CallAvailableIfNotDebug() => C.AvailableIfNotDebug();
  public static void CallAvailableIfDebug() => C.AvailableIfDebug();
}
否定条件のコンパイラ定数を定義してConditional属性を付与する
#if !DEBUG
// DEBUGが*定義されていない*場合、NOT_DEBUGを定義する
#define NOT_DEBUG
#endif

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

static class C {
  // DEBUGが定義されていない(NOT_DEBUGが定義されている)場合のみ有効になるメソッド
  [Conditional("NOT_DEBUG")]
  public static void AvailableIfNotDebug(
    [CallerFilePath] string sourceFilePath = null // 呼び出し元のファイル名を取得する
  )
    => Console.WriteLine($"NOT_DEBUG: {sourceFilePath}");

  // (比較のため)DEBUGが定義されている場合のみ有効となるメソッド
  [Conditional("DEBUG")]
  public static void AvailableIfDebug(
    [CallerFilePath] string sourceFilePath = null // 呼び出し元のファイル名を取得する
  )
    => Console.WriteLine($"DEBUG: {sourceFilePath}");
}

class Sample {
  static void Main()
  {
    // 同一ファイル内から直接呼び出す
    C.AvailableIfNotDebug();
    // 異なるファイルのメソッドを経由して呼び出す
    X.CallAvailableIfNotDebug();

    // 同一ファイル内から直接呼び出す
    C.AvailableIfDebug();
    // 異なるファイルのメソッドを経由して呼び出す
    X.CallAvailableIfDebug();
  }
}
DEBUGが定義されている場合
実行結果
DEBUG: main.cs
DEBUG: external.cs
DEBUGが定義されていない場合
実行結果
NOT_DEBUG: main.cs
異なるファイルのConditionalなメソッドを呼び出す 
Class X
  Public Shared Sub CallAvailableIfNotDebug()
    M.AvailableIfNotDebug()
  End Sub

  Public Shared Sub CallAvailableIfDebug()
    M.AvailableIfDebug()
  End Sub
End Class
否定条件のコンパイラ定数を定義してConditional属性を付与する 
#If DEBUG <> True Then
' DEBUGが*定義されていない*場合、NOT_DEBUGを定義する
#Const NOT_DEBUG = True
#End If

Imports System
Imports System.Diagnostics
Imports System.Runtime.CompilerServices

Module M
  ' DEBUGが定義されていない(NOT_DEBUGが定義されている)場合のみ有効になるメソッド
  <Conditional("NOT_DEBUG")>
  Public Sub AvailableIfNotDebug(
    <CallerFilePath> Optional ByVal sourceFilePath As String = Nothing ' 呼び出し元のファイル名を取得する
  )
    Console.WriteLine($"NOT_DEBUG: {sourceFilePath}")
  End Sub

  ' (比較のため)DEBUGが定義されている場合のみ有効となるメソッド
  <Conditional("DEBUG")>
  Public Sub AvailableIfDebug(
    <CallerFilePath> Optional ByVal sourceFilePath As String = Nothing ' 呼び出し元のファイル名を取得する
  )
    Console.WriteLine($"DEBUG: {sourceFilePath}")
  End Sub
End Module

Class Sample
  Shared Sub Main()
    ' 同一ファイル内から直接呼び出す
    M.AvailableIfNotDebug()
    ' 異なるファイルのメソッドを経由して呼び出す
    X.CallAvailableIfNotDebug()

    ' 同一ファイル内から直接呼び出す
    M.AvailableIfDebug()
    ' 異なるファイルのメソッドを経由して呼び出す
    X.CallAvailableIfDebug()
  End Sub
End Class
DEBUGが定義されている場合
実行結果
DEBUG: main.vb
DEBUG: external.vb
DEBUGが定義されていない場合
実行結果
NOT_DEBUG: main.vb

Conditionalなメソッドのオーバーライド

C#では、Conditional属性を付与したメソッドをオーバーライドして、さらにConditional属性を付与することはできません。 また、Conditional属性を付与したメソッドをオーバーライドした場合は、基底クラスのメソッドと同様に条件付きの呼び出しが行われます。

VBでは、Conditional属性を付与したメソッドをオーバーライドして、さらにConditional属性を付与すると、基底クラスのConditional属性は上書きされ、オーバーライドしたメソッドのConditional属性が適用されます。

オーバーライドしたメソッドに対するConditional属性の付与
using System;
using System.Diagnostics;

class C0 {
  [Conditional("D0")]
  public virtual void M() => Console.WriteLine("C0");
}

class C1 : C0 {
  // Conditionalなメソッドをオーバーライドしたメソッドでは
  // Conditional属性を追加で付与することはできない
  /*
  [Conditional("D1")] // error CS0243: 条件付き属性はオーバーライド メソッドであるため、 'C1.M()' では無効です
  */
  public override void M() => Console.WriteLine("C1");
}

class Sample {
  static void Main()
  {
    var c0 = new C0();
    var c1 = new C1();

    c0.M(); // D0が定義されている場合は呼び出される
    c1.M(); // D0が定義されている場合は呼び出される(基底クラスのメソッドと同様)
  }
}
オーバーライドしたメソッドに対するConditional属性の付与
Imports System
Imports System.Diagnostics

Class C0
  <Conditional("D0")> _
  Public Overridable Sub M()
    Console.WriteLine("C0")
  End Sub
End Class

Class C1
  Inherits C0
  ' Conditionalなメソッドをオーバーライドしたメソッドでも
  ' Conditional属性を付与することができる
  ' (基底クラスでのConditional属性の指定が上書きされる)
  <Conditional("D1")> _
  Public Overrides Sub M()
    Console.WriteLine("C1")
  End Sub
End Class

Class Sample
  Shared Sub Main()
    Dim c0 As New C0()
    Dim c1 As New C1()

    c0.M() ' D0が定義されている場合に呼び出される
    c1.M() ' D1が定義されている場合に呼び出される (D0の定義には影響されない)
  End Sub
End Class

Conditionalなメソッドからのデリゲートの作成

C#では、Conditional属性が付与されたメソッドからデリゲートを作成しようとするとコンパイルエラーとなる一方、VBではデリゲートを作成して呼び出すこともできます。 この場合、コンパイラ定数の定義は無視されることになります。

Conditional属性が付与されたメソッドのデリゲートを作成する
using System;
using System.Diagnostics;

class Sample {
  // コンパイラ定数"DEBUG"が定義されている場合のみ有効になるメソッド
  [Conditional("DEBUG")]
  static void Log(string message)
    => Console.WriteLine(message);

  static void Main()
  {
    // Conditionalなメソッドをnameof演算子で参照することはできる
    Console.WriteLine(nameof(Log));

    // Conditionalなメソッドからデリゲートを作成することはできない
    Action<string> log = Log; // error CS1618: 'Sample.Log(string)' またはオーバーライドされるメソッドは条件付き属性なので、この属性でデリゲートを作成できません

    log("Hello, world!");
  }
}
Conditional属性が付与されたメソッドのデリゲートを作成する
Imports System
Imports System.Diagnostics

Class Sample
  ' コンパイラ定数"DEBUG"が定義されている場合のみ有効になるメソッド
  <Conditional("DEBUG")> _
  Shared Sub Log(ByVal message As String)
    Console.WriteLine(message)
  End Sub
  
  Shared Sub Main()
    ' ConditionalなメソッドをNameOf演算子で参照することはできる
    Console.WriteLine(NameOf(Log))

    ' Conditionalなメソッドからデリゲートを作成することができる
    Dim l As Action(Of String) = AddressOf Log ' コンパイルエラーとはならない

    ' Conditionalなメソッドを参照するデリゲートを呼び出すこともできる
    ' Conditional属性は無視されるため、コンパイラ定数に関わらず呼び出せることになる
    l("Hello, world!")
  End Sub
End Class
実行結果
Log
Hello, world!

Conditional属性が付与されたメソッドを参照するデリゲートは扱いが異なる一方、nameof/NameOfによるメソッド名の参照は共通して行えます。 また、リフレクションによって呼び出すことも同様に共通して行えます。

リフレクションによるConditionalなメソッドの呼び出し

Conditional属性が付与されているメソッドの通常の呼び出しはコンパイル結果から削除される一方、メソッド自体は削除されません。 そのため、nameof/NameOf演算子によってメソッド名を取得することができます。

さらに、リフレクションによる呼び出しはコンパイラに検知されないため、Conditional属性が付与されていてもメソッドを呼び出すことができます。 この場合、エラーや警告は一切出力されません。

リフレクションによってConditional属性が付与されたメソッドを呼び出す
using System;
using System.Diagnostics;
using System.Reflection;

class Sample {
  // コンパイラ定数"DEBUG"が定義されている場合のみ有効になるメソッド
  [Conditional("DEBUG")]
  public static void Log(string message)
    => Console.WriteLine(message);

  static void Main()
  {
#if DEBUG
    // コンパイラ定数DEBUGが定義されている場合は表示する
    Console.WriteLine("DEBUG");
#endif

    // リフレクションによって上記のConditionalなメソッドを呼び出す
    // (コンパイルエラー等にはならず、またコンパイラ定数の定義に関わらず呼び出せてしまう)
    typeof(Sample).InvokeMember(nameof(Log), BindingFlags.InvokeMethod, null, null, new object[] {"Hello, world!"});
  }
}
リフレクションによってConditional属性が付与されたメソッドを呼び出す
Imports System
Imports System.Diagnostics
Imports System.Reflection

Class Sample
  ' コンパイラ定数"DEBUG"が定義されている場合のみ有効になるメソッド
  <Conditional("DEBUG")> _
  Shared Sub Log(ByVal message As String)
    Console.WriteLine(message)
  End Sub
  
  Shared Sub Main()
#If DEBUG Then
    ' コンパイラ定数DEBUGが定義されている場合は表示する
    Console.WriteLine("DEBUG")
#End If

    ' リフレクションによって上記のConditionalなメソッドを呼び出す
    ' (コンパイルエラー等にはならず、またコンパイラ定数の定義に関わらず呼び出せてしまう)
    GetType(Sample).InvokeMember(NameOf(Log), BindingFlags.InvokeMethod, Nothing, Nothing, New Object() {"Hello, world!"})
  End Sub
End Class
DEBUGが定義されている場合
実行結果
DEBUG
Hello, world!
DEBUGが定義されていない場合
実行結果
Hello, world!

上記のように、Conditional属性が付与されているメソッドでも、リフレクションを使って呼び出すことはでき、また呼び出し時にメソッドがConditionalであることを理由とした例外がスローされるようなこともありません。

リフレクションによるConditional属性が付与されているメソッドの呼び出しはコンパイラによって一切検知されない一方、逆に言えば、何らかの理由によりコンパイラ定数の定義とは無関係にメソッドを呼び出したい場合には、コンパイラによるメソッド呼び出しの削除を回避して呼び出せるということになります。

いずれにしても、リフレクションを用いる場合はこのようなリスクとメリットを考慮した上で行う必要があります。

属性に対するConditional属性 (Conditionalな属性クラス)

カスタム属性クラス(Attribute派生クラス)にConditional属性を付与すると、その属性が条件付きコンパイルの対象となります。 つまり、コンパイラ定数が定義されている場合のみ付与される属性を定義することができます。

#ifディレクティブでも属性を条件付きコンパイルすることはできますが、Conditional属性を用いることで記述を#ifディレクティブよりも簡素化することができます。 VBの#Ifディレクティブでは完全なステートメントを記述する必要があるため、Conditionalな属性は特に便利です。

カスタム属性クラスにConditional属性を付与する
using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;

// コンパイラ定数"DEBUG"が定義されている場合のみ有効になるカスタム属性
[Conditional("DEBUG")]
[AttributeUsage(AttributeTargets.All)]
class DebugAttribute : Attribute {}

class Sample {
  // コンパイラ定数"DEBUG"が定義されている場合のみ、Debug属性がメソッドに付与される
  // (属性が条件付きコンパイルの対象となる)
  [Debug]
  static void Main()
  {
    // MainメソッドにDebugAttributeが付与されているか調べる
    Console.WriteLine(
      "DebugAttribute? {0}",
      typeof(Sample)
        .GetMethod(nameof(Main), BindingFlags.NonPublic | BindingFlags.Static)
        .GetCustomAttributes(inherit: false)
        .OfType<DebugAttribute>()
        .Any()
    );
  }

  // 比較として、#ifディレクティブで属性を条件付きコンパイルする場合は次のようになる
#if DEBUG
  [Debug]
#endif
  static void M() {}
}
カスタム属性クラスにConditional属性を付与する
Imports System
Imports System.Diagnostics
Imports System.Linq
Imports System.Reflection

' コンパイラ定数"DEBUG"が定義されている場合のみ有効になる属性クラス
<Conditional("DEBUG")> _
<AttributeUsage(AttributeTargets.All)> _
Class DebugAttribute
  Inherits Attribute
End Class

Class Sample
  ' コンパイラ定数"DEBUG"が定義されている場合のみ、Debug属性がメソッドに付与される
  ' (属性が条件付きコンパイルの対象となる)
  <Debug> _
  Shared Sub Main()
    ' MainメソッドにDebugAttributeが付与されているか調べる
    Console.WriteLine( _
      "DebugAttribute? {0}", _
      GetType(Sample). _
        GetMethod(NameOf(Main), BindingFlags.Public Or BindingFlags.Static). _
        GetCustomAttributes(inherit := False). _
        OfType(Of DebugAttribute)(). _
        Any() _
    )
  End Sub

  ' 比較として、#Ifディレクティブで属性を条件付きコンパイルする場合は次のようになる
#If DEBUG Then
  <Debug> Shared Sub M()
#Else
  Shared Sub M()
#End If
  End Sub
End Class
DEBUGが定義されている場合
実行結果
DebugAttribute? True
DEBUGが定義されていない場合
実行結果
DebugAttribute? False

カスタム属性の宣言・実装については属性とメタデータを参照してください。


Conditional属性は、ValidOnAttributeTargets.Classを含んでいます。 これは、Conditional属性を任意のクラスに付与できることを意味していますが、実際にはカスタム属性クラスにのみConditional属性を付与することができます。

カスタム属性クラス以外のクラスにConditional属性を付与すると、C#ではコンパイルエラーとなります。 VBではコンパイルエラーとはならず、単に無視されるだけとなるようです。

Conditional属性はカスタム属性のクラスにのみ付与できる
using System;
using System.Diagnostics;

// カスタム属性クラスにはConditional属性を付与することができる
[Conditional("DEBUG")]
[AttributeUsage(AttributeTargets.All)]
class DebugAttribute : Attribute {
}

// カスタム属性以外のクラスにはConditional属性を付与することはできない
[Conditional("DEBUG")] // error CS1689: 属性 'Conditional' は、メソッドまたは属性クラスでのみ有効です
class Sample {
  static void Main()
  {
    Console.WriteLine("Hello, world!");
  }
}
Conditional属性はカスタム属性のクラスにのみ付与できる
Imports System
Imports System.Diagnostics

' カスタム属性クラスにはConditional属性を付与することができる
<Conditional("DEBUG")> _
<AttributeUsage(AttributeTargets.All)> _
Class TestAttribute
  Inherits Attribute
End Class

' カスタム属性以外のクラスにもConditional属性を付与することはできる
' ただし、エラーや警告とならないだけで、具体的な効果があるわけではない
<Conditional("DEBUG")> _
Class Sample
  ' クラスにConditional属性を付与したからといって、
  ' クラス内のメソッドすべてがConditionalになることはない
  Shared Sub Main()
    Console.WriteLine("Hello, world!")
  End Sub
End Class

Debugクラス・Traceクラス

ログ出力やデバッグ出力を目的としたクラスとして、.NETではDebugクラスTraceクラスが用意されています。 標準出力への出力を行うConsoleクラスと同様に、DebugクラスとTraceクラスにはそれぞれ異なる出力先が割り当てられます。

Debug.WriteLineTrace.WriteLineなど、Debugクラス・Traceクラスの出力メソッドにはConditional属性が付与されています。 DebugクラスはDEBUG、TraceクラスはTRACEが指定されたConditional属性が付与されています。

またDebugクラス・Traceクラスでは、AssertWriteLineIfといったアサーション(assertion, C/C++におけるassert)用のメソッドも用意されていて、これらのメソッド呼び出しはコンパイラ定数が定義されていない場合は削除されるため、診断や動作検証を目的としたコードを記述するのに向いています。

Debugクラス・Traceクラスの条件付き呼び出しで診断情報を出力する
using System;
using System.Diagnostics;
using System.IO;

class Sample {
  static void Main()
  {
    // Traceクラスの出力先(リスナ)にConsoleTraceListener(標準出力)を追加する
    Trace.Listeners.Add(new ConsoleTraceListener());
    // Traceクラスの出力先(リスナ)にTextWriterTraceListener(ファイルなどStreamへの出力)を追加する
    Trace.Listeners.Add(new TextWriterTraceListener(File.OpenWrite("test.log")));

    // Consoleクラスによる標準出力への出力
    // この呼び出しはコンパイラ定数に関わらず行われる
    Console.WriteLine("Hello, stdout!");

    // Debugクラスによる出力 (出力先はIDEのデバッグウィンドウなど、実行環境により異なる)
    // この呼び出しはコンパイラ定数"DEBUG"が定義されている場合のみ行われる
    Debug.WriteLine("Hello, debugger!");

    // Traceクラスによる出力 (出力先はTrace.Listenersに設定されているトレースリスナとなる)
    // この呼び出しはコンパイラ定数"TRACE"が定義されている場合のみ行われる
    Trace.WriteLine("Hello, trace listeners!");

    // Traceクラスへの出力をFlushする
    // (ファイル等へ出力する場合に必要)
    Trace.Flush();
  }
}
Debugクラス・Traceクラスの条件付き呼び出しで診断情報を出力する
Imports System
Imports System.Diagnostics
Imports System.IO

Class Sample
  Shared Sub Main()
    ' Traceクラスの出力先(リスナ)にConsoleTraceListener(標準出力)を追加する
    Trace.Listeners.Add(new ConsoleTraceListener())
    ' Traceクラスの出力先(リスナ)にTextWriterTraceListener(ファイルなどStreamへの出力)を追加する
    Trace.Listeners.Add(new TextWriterTraceListener(File.OpenWrite("test.log")))

    ' Consoleクラスによる標準出力への出力
    ' この呼び出しはコンパイラ定数に関わらず行われる
    Console.WriteLine("Hello, stdout!")

    ' Debugクラスによる出力 (出力先はIDEのデバッグウィンドウなど、実行環境により異なる)
    ' この呼び出しはコンパイラ定数"DEBUG"が定義されている場合のみ行われる
    Debug.WriteLine("Hello, debugger!")

    ' Traceクラスによる出力 (出力先はTrace.Listenersに設定されているトレースリスナとなる)
    ' この呼び出しはコンパイラ定数"TRACE"が定義されている場合のみ行われる
    Trace.WriteLine("Hello, trace listeners!")

    ' Traceクラスへの出力をFlushする
    ' (ファイル等へ出力する場合に必要)
    Trace.Flush()
  End Sub
End Class

Debugクラスの出力メソッドは、IDEの出力ウィンドウやデバッグウィンドウなど、実行環境によって異なる箇所に出力されます。 Linuxでは環境変数COMPlus_DebugWriteToStdErr1設定することで標準出力を出力先とすることができます。

Traceクラスは、デフォルトではDebugクラスと同様の出力先が選択されますが、トレースリスナ(TraceListener派生クラス)を設定することにより出力先を標準出力やファイルとするなど、任意に設定することができます。

Debugクラス・Traceクラスおよびログ出力やデバッグ出力に関して、より詳しくは以下を参照してください

Switchクラス

Switchクラスおよびその派生クラスは、コンパイラ定数に基づく条件分岐を行うものではなく、アプリケーション構成ファイル(*.exe.config)に基づく条件分岐を行うための機構を実装するクラスです。 外部ファイルであるアプリケーション構成ファイルによる切り替え(switch)ができるため、これらのクラスを用いることでコードの再コンパイルなしに動作を変更することができます。

ただし、.NET Core/.NET 5以降ではアプリケーション構成ファイルからSystem.Diagnostics名前空間に関する設定(<system.diagnostics>要素)が読み込まれないため、これらのクラスが完全に機能するのは.NET FrameworkおよびMonoで動作させた場合のみとなります。

例として、BooleanSwitchクラスを用いて、アプリケーション構成ファイルの設定から機能Xを有効にするか無効にするかを切り替える動作を実装すると次のようになります。 このコードを.NET Core/.NET 5以降で実行した場合は、アプリケーション構成ファイルの設定に関わらず常に初期値に基づく動作となります。

BooleanSwitchクラスを用いてアプリケーション構成ファイルでの設定に基づく動作の切り替えを行う
using System;
using System.Diagnostics;

class Sample {
  // 機能Xを切り替えるためのBooleanSwitch
  // (Switch派生クラスはstaticなメンバである必要がある)
  static readonly BooleanSwitch switchFeatureX = new BooleanSwitch(
    displayName: "FeatureX", // スイッチ名を"FeatureX"とする
    description: "機能X",
    defaultSwitchValue: "0" // 初期値を0(false)にする
  );

  static void Main()
  {
    // BooleanSwitchの値によって動作を切り替える
    // (Enabledプロパティの値は、アプリケーション構成ファイルから読み込まれる)
    if (switchFeatureX.Enabled)
      Console.WriteLine($"{switchFeatureX.Description}は有効になっています");
    else
      Console.WriteLine($"{switchFeatureX.Description}は無効になっています");
  }
}
BooleanSwitchクラスを用いてアプリケーション構成ファイルでの設定に基づく動作の切り替えを行う
Imports System
Imports System.Diagnostics

Class Sample
  ' 機能Xを切り替えるためのBooleanSwitch
  ' (Switch派生クラスはSharedなメンバである必要がある)
  Shared ReadOnly switchFeatureX As New BooleanSwitch(
    displayName := "FeatureX", ' スイッチ名を"FeatureX"とする
    description := "機能X",
    defaultSwitchValue := "0" ' 初期値を0(False)にする
  )

  Shared Sub Main()
    ' BooleanSwitchの値によって動作を切り替える
    ' (Enabledプロパティの値は、アプリケーション構成ファイルから読み込まれる)
    If switchFeatureX.Enabled Then
      Console.WriteLine($"{switchFeatureX.Description}は有効になっています")
    Else
      Console.WriteLine($"{switchFeatureX.Description}は無効になっています")
    End If
  End Sub
End Class
アプリケーション構成ファイル(sample.exe.config)
<configuration>
  <system.diagnostics>
    <switches>
      <!-- スイッチ名"FeatureX"のスイッチの値を指定する -->
      <!-- BooleanSwitchの場合、"1"ならTrue、"0"ならFalseとして扱われる -->
      <add name="FeatureX" value="1" />
    </switches>  
  </system.diagnostics>
</configuration>
スイッチの値が"1"の場合
実行結果
機能Xは有効になっています
スイッチの値が"0"またはデフォルトの場合
実行結果
機能Xは無効になっています

.NET Core/.NET 5以降ではアプリケーション構成ファイルを読み込む機能は実装されていないものの、Switch派生クラスの他の機能はすべて実装されており、また初期値の設定はできることから、代替として環境変数を設定値として用いることができます。 これにより、.NET Framework/Monoではアプリケーション構成ファイルの設定、.NET Core/.NETでは環境変数による設定を用いて、Switch派生クラスの動作を切り替えるようにすることができます。

BooleanSwitchクラスを用いてアプリケーション構成ファイルまたは環境変数での設定に基づく動作の切り替えを行う
using System;
using System.Diagnostics;

class Sample {
  // 機能Xを切り替えるためのBooleanSwitch
  // (Switch派生クラスはstaticなメンバである必要がある)
  static readonly BooleanSwitch switchFeatureX = new BooleanSwitch(
    displayName: "FeatureX", // スイッチ名を"FeatureX"とする
    description: "機能X",
    defaultSwitchValue: Environment.GetEnvironmentVariable("FEATURE_X") // 環境変数FEATURE_Xの値を初期値とする
  );

  static void Main()
  {
    // BooleanSwitchの値によって動作を切り替える
    // (Enabledプロパティの値は、アプリケーション構成ファイルから読み込まれる)
    if (switchFeatureX.Enabled)
      Console.WriteLine($"{switchFeatureX.Description}は有効になっています");
    else
      Console.WriteLine($"{switchFeatureX.Description}は無効になっています");
  }
}
BooleanSwitchクラスを用いてアプリケーション構成ファイルまたは環境変数での設定に基づく動作の切り替えを行う
Imports System
Imports System.Diagnostics

Class Sample
  ' 機能Xを切り替えるためのBooleanSwitch
  ' (Switch派生クラスはSharedなメンバである必要がある)
  Shared ReadOnly switchFeatureX As New BooleanSwitch(
    displayName := "FeatureX", ' スイッチ名を"FeatureX"とする
    description := "機能X",
    defaultSwitchValue := Environment.GetEnvironmentVariable("FEATURE_X") ' 環境変数FEATURE_Xの値を初期値とする
  )

  Shared Sub Main()
    ' BooleanSwitchの値によって動作を切り替える
    ' (Enabledプロパティの値は、アプリケーション構成ファイルから読み込まれる)
    If switchFeatureX.Enabled Then
      Console.WriteLine($"{switchFeatureX.Description}は有効になっています")
    Else
      Console.WriteLine($"{switchFeatureX.Description}は無効になっています")
    End If
  End Sub
End Class
環境変数FEATURE_Xまたは構成ファイルでの指定値が"1"の場合
実行結果
機能Xは有効になっています
環境変数FEATURE_Xまたは構成ファイルでの指定値が"0"または未設定の場合
実行結果
機能Xは無効になっています

将来的に.NETでもアプリケーション構成ファイルにおける<system.diagnostics>要素の設定を読み込むようにするのかどうか不明ですが、少なくとも.NET 5時点で実装されていないことから、優先度・可能性は低いものと思われます。

環境変数の取得については環境変数を参照してください。

コンパイラ定数(シンボル)

コンパイラ定数は、コンパイル時のみ、かつコンパイラのみが使用する(コンパイルされるコードからは参照できない)定数です。

C#では、コンパイラ定数はそれが定義されているかどうかのみを表す、trueまたはfalsebool値として評価されます。 C/C++の#defineなどとは異なり、コンパイラ定数に数値や文字列などの値を持たせることはできず、またコード中にコンパイラ定数の値を展開することもできません。 例えば、コンパイラ定数DEBUGに対してbool isDebug = DEBUG;Console.WriteLine(DEBUG);とするようなことはできません。

VBでは、コンパイラ定数はTrueまたはFalseBoolean値のほか、数値や文字列を値として持たせることができます。 ただし、C/C++の#defineなどとは異なり、コード中にコンパイラ定数の値を展開することはできず、コンパイラ定数の値は#Ifディレクティブのみで評価されます。


C#/VBともに、コンパイラ定数をC/C++のマクロのように使用することもできません。 例えば、#define Console.WriteLine Printのような定義を行うことはできません。

型の別名(エイリアス)を宣言する目的には、usingディレクティブ/Importsステートメントを使うことができます。

usingディレクティブで型のエイリアスを宣言する
using System;

// 型System.Int32をエイリアス(別名)で参照できるようにする
using int32 = System.Int32;

class Sample {
  static void Main()
  {
    int32 x = 42;

    Console.WriteLine(x.GetType()); // "System.Int32"
  }
}
Importsステートメントで型のエイリアスを宣言する
Imports System

' 型System.Int32をエイリアス(別名)で参照できるようにする
Imports Integer32 = System.Int32

Class Sample
  Shared Sub Main()
    Dim x As Integer32 = 42

    Console.WriteLine(x.GetType()) ' "System.Int32"
  End Sub
End Class

コンパイラ定数(シンボル)は、通常プロジェクトファイルやコマンドライン引数で定義しますが、#define/#Constディレクティブを用いてコード中で定義することもできます。

コンパイラ定数・const/readonly・他の言語との違い

コンパイラ定数以外にも、C#/VBでは定数と呼ばれるものがいくつか存在します。

C#/VBにおけるコンパイラ定数は、C/C++における#defineなどとは異なりマクロとして使用したりコンパイル時に展開される定数としては定義できません。 コンパイル時定数を定義したい場合はconst/Constを使用します。

宣言時にconst(C#)/Const(VB)を使用すると、コンパイル時定数(compile-time constants)、コンパイル時にリテラルとして展開される定数を定義することができます。 一般的に単に「定数」と呼ばれる場合は主にこれを指します。 const/Constでは、数値や文字列などの基本型の値をコンパイル時定数として定義することができます。 const/ConstはC++のconstexprに相当するものです。

宣言時にreadonly(C#)/ReadOnly(VB)を使用すると、変更できない不変値(immutable values)、実行時に値を再代入できない変数を定義できます。 不変値・固定値の意味合いで「定数」と呼ばれる場合はこれを指している場合があります。 readonly/ReadOnlyでは、基本型や任意の型の変数を読み取り専用として定義することができます(変数に代入されているインスタンス自体に対する変更は行える、つまりインスタンス自体をイミュータブルとするものではない点に注意)。 また、コンパイル時に(最適化される場合を除くと)値はリテラルとして展開されず、都度参照されます。 readonly/ReadOnlyはC/C++のconstに相当するもの、再代入できない変数宣言という点ではJavaScriptのletに相当するものです。

コンパイラ定数・const定数・readonly変数の例
// #defineでファイル内のみの局所的なコンパイラ定数を定義できる
#define TEST

// #defineをマクロのように使用することはできない
#define MAX_USER_COUNT 10 // error CS1025: 単一行コメントか行末が必要です

using System;

class Sample {
  // 物理定数など実行環境などによらず不変な定数・外部に公開されず将来的な変更の余地がない値はconstが向いている
  const double Pi = 3.14;

  // 外部に公開され、かつ将来的に変更される可能性のある値や、外部から読み込まれる設定値(コンパイル時に決定できない値)はreadonlyを用いる
  // クラス外からも参照されるような大域的な不変値など、一般にはstatic readonlyとすることが多い
  public static readonly int MaxUserCount = 10;

  // 定義済みの値を持ったインスタンス・シングルトンインスタンスなどはreadonlyを用いる
  // (クラスインスタンス変数はconstにできない)
  static readonly User SuperUser = new User("root", 0);

  public class User {
    // コンストラクタ引数として与えられ、以降変更されない(参照のみとなる)ような値は、readonlyを用いる
    private readonly string name;
    private readonly int id;

    // 外部に公開されない・局所的に用いられる特殊値の定義などは、constが向いている
    private const int invalid_id = -1;

    public User(string name, int id)
    {
      if (id == invalid_id || id == SuperUser.id)
        throw new ArgumentException(nameof(id));

      // 変数初期化またはコンストラクタでの初期化以外で、readonly変数の値を変更することはできない
      this.name = name;
      this.id = id;

      // コンパイラ定数は#ifなどのディレクティブでのみ使用できる(値を展開することはできない)
#if TEST
      Console.WriteLine($"{name}:{id}");
#endif
    }

    public void Deactivate()
    {
      // readonlyな変数には再代入できない
      name = "(deactivated user)"; // error CS0191: 読み取り専用フィールドに割り当てることはできません (フィールドが定義されている型のコンストラクターか init 専用セッター、または変数初期化子では可)
      id = invalid_id; // error CS0191: 読み取り専用フィールドに割り当てることはできません (フィールドが定義されている型のコンストラクターか init 専用セッター、または変数初期化子では可)
    }
  }
}
コンパイラ定数・const定数・readonly変数の例
' #Constでファイル内のみの局所的なコンパイラ定数を定義できる
#Const TEST = 1

' #Constでコンパイラ定数に値を与えることはできるが、マクロのようにコード内に値を展開することはできない
#Const MAX_USER_COUNT = 10

Imports System
Imports System.Diagnostics

Class Sample
  ' 物理定数など実行環境などによらず不変な定数・外部に公開されず将来的な変更の余地がない値はConstが向いている
  Const Pi As Double = 3.14

  ' 外部に公開され、かつ将来的に変更される可能性のある値や、外部から読み込まれる設定値(コンパイル時に決定できない値)はReadOnlyを用いる
  ' クラス外からも参照されるような大域的な不変値など、一般にはShared ReadOnlyとすることが多い
  Public Shared ReadOnly MaxUserCount As Integer = 10

  ' 定義済みの値を持ったインスタンス・シングルトンインスタンスなどはReadOnlyを用いる
  ' (クラスインスタンス変数はConstにできない)
  Shared ReadOnly SuperUser As New User("root", 0)

  Public Class User
    ' コンストラクタ引数として与えられ、以降変更されない(参照のみとなる)ような値は、ReadOnlyを用いる
    Private ReadOnly _name As String
    Private ReadOnly _id As Integer

    ' 外部に公開されない・局所的に用いられる特殊値の定義などは、Constが向いている
    Private Const invalid_id As Integer = -1

    Public Sub New(ByVal name As String, ByVal id As Integer)
      If id = invalid_id OrElse id = SuperUser._id Then
        Throw New ArgumentException(NameOf(id))
      End If

      ' 変数初期化またはコンストラクタでの初期化以外で、ReadOnly変数の値を変更することはできない
      _name = name
      _id = id

      ' コンパイラ定数は#Ifなどのディレクティブでのみ使用できる(値を展開することはできない)
#If TEST = 1 Then
      Console.WriteLine($"{_name}:{_id}")
#End If
    End Sub

    Public Sub Deactivate()
      ' ReadoOlyな変数には再代入できない
      _name = "(deactivated user)" ' error BC30064: 'ReadOnly' 変数を代入式のターゲットにすることはできません。
      _id = invalid_id ' error BC30064: 'ReadOnly' 変数を代入式のターゲットにすることはできません。
    End Sub
  End Class

  Shared Sub Main()
  End Sub
End Class

コンパイラ定数の定義

コンパイラ定数は通常プロジェクトファイルで定義します。 プロジェクトファイルのプロパティとして<DefineConstants>要素でビルド時に使用する(コンパイラに渡す)コンパイラ定数を定義することができます。 Condition属性で条件式を指定することにより、特定の条件の場合のみコンパイラ定数を定義することもできます。

<DefineConstants>で複数のコンパイラ定数を定義する場合、C#ではセミコロン;、VBではカンマ,で区切ります。

プロジェクトファイルのDefineConstants要素でコンパイラ定数を定義する
  <PropertyGroup>
    <!-- 3つのコンパイラ定数FOO, BAR, BAZを定義する -->
    <DefineConstants>FOO;BAR;BAZ</DefineConstants>

    <!-- ビルド構成(Configuration)がDebugの場合に、DefineConstantsにDEBUG_FOOとDEBUG_BARを追加する -->
    <DefineConstants Condition=" '$(Configuration)' == 'Debug' ">$(DefineConstants);DEBUG_FOO;DEBUG_BAR</DefineConstants>
  </PropertyGroup>
プロジェクトファイルのDefineConstants要素でコンパイラ定数を定義する
  <PropertyGroup>
    <!-- 3つのコンパイラ定数FOO, BAR, BAZを定義する -->
    <DefineConstants>FOO,BAR,BAZ</DefineConstants>

    <!-- ビルド構成(Configuration)がDebugの場合に、DefineConstantsにDEBUG_FOOとDEBUG_BARを追加する -->
    <DefineConstants Condition=" '$(Configuration)' == 'Debug' ">$(DefineConstants),DEBUG_FOO,DEBUG_BAR</DefineConstants>
  </PropertyGroup>

TRACEDEBUGターゲットフレームワークを表すNETNETFRAMEWORKなど、DefineConstantsでの定義とは別に自動的に定義されるコンパイラ定数が含まれることがあります。

このほかdotnet buildコマンドのコマンドライン引数で指定することもできます。 コンパイラ定数の定義についてより詳しくはプロジェクトファイル §.コンパイラ定数 (<DefineConstants>)で解説しています。

ターゲットフレームワークごとに定義されるコンパイラ定数

.NET Core/.NET 5以降の形式のプロジェクトファイルでは、ターゲットフレームワークに応じたコンパイラ定数が自動的に定義されます。 例として、.NET 5以降はNET、.NET FrameworkはNETFRAMEWORKなど、コンパイル時に対象とするフレームワークの種類毎にコンパイラ定数が定義されます。 またフレームワークのバージョンに応じてNET5_0(.NET 5)、NET48(.NET Framework 4.8)などの定数も合わせて定義されます。

フレームワークごとに提供されるAPIが異なる場合など、その差を吸収するコードを記述する必要がある場合にはこれらのコンパイラ定数を使用することができます。 ターゲットフレームワークごとに定義されるコンパイラ定数の詳細はプロジェクトファイル §..NET形式のプロジェクトファイルで定義されるコンパイラ定数を参照してください。

各フレームワークでShift_JISのEncodingを取得する(CodePagesEncodingProviderから直接取得する)
using System;
using System.Text;

class Sample {
  static readonly Encoding shift_jis =
#if NETFRAMEWORK
    // .NET FrameworkではEncodingクラスから直接Shift_JISエンコーディングを取得できる
    Encoding.GetEncoding("shift_jis");
#else
    // それ以外(.NET 5以降/.NET Core)ではSystem.Text.Encoding.CodePages.dllの
    // CodePagesEncodingProviderからShift_JISエンコーディングを取得する必要がある
    CodePagesEncodingProvider.Instance.GetEncoding("shift_jis");
#endif

  static void Main()
  {
    shift_jis.GetBytes("こんにちは、世界!");
  }
}
各フレームワークでShift_JISのEncodingを取得する(CodePagesEncodingProviderから直接取得する)
Imports System
Imports System.Diagnostics
Imports System.Text

Class Sample
#If NETFRAMEWORK Then
  ' .NET FrameworkではEncodingクラスから直接Shift_JISエンコーディングを取得できる
  Shared ReadOnly shift_jis As Encoding = Encoding.GetEncoding("shift_jis")
#Else
  ' それ以外(.NET 5以降/.NET Core)ではSystem.Text.Encoding.CodePages.dllの
  ' CodePagesEncodingProviderからShift_JISエンコーディングを取得する必要がある
  Shared ReadOnly shift_jis As Encoding = CodePagesEncodingProvider.Instance.GetEncoding("shift_jis")
#End If

  Shared Sub Main()
    shift_jis.GetBytes("こんにちは、世界!")
  End Sub
End Class
各フレームワークでShift_JISのEncodingを取得する(Encoding.GetEncodingメソッドで取得する)
using System;
using System.Diagnostics;
using System.Text;

class Sample {
  // .NET 5以降/.NET Core/.NET Standardでは、Encoding.GetEncodingメソッドで
  // Shift_JISエンコーディングを取得する場合、Encodingクラスに
  // CodePagesEncodingProviderを登録しておく必要がある
  [Conditional("NET")]
  [Conditional("NETCOREAPP")]
  [Conditional("NETSTANDARD")]
  static void RegisterCodePagesEncodingProvider()
  {
#if !NETFRAMEWORK
    Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
#endif
  }

  static void Main()
  {
    RegisterCodePagesEncodingProvider();

    Encoding.GetEncoding("shift_jis").GetBytes("こんにちは、世界!");
  }
}
各フレームワークでShift_JISのEncodingを取得する(Encoding.GetEncodingメソッドで取得する)
Imports System
Imports System.Diagnostics
Imports System.Text

Class Sample
  ' .NET 5以降/.NET Core/.NET Standardでは、Encoding.GetEncodingメソッドで
  ' Shift_JISエンコーディングを取得する場合、Encodingクラスに
  ' CodePagesEncodingProviderを登録しておく必要がある
  <Conditional("NET")>
  <Conditional("NETCOREAPP")>
  <Conditional("NETSTANDARD")>
  Shared Sub RegisterCodePagesEncodingProvider()
#If NETFRAMEWORK <> True Then
    Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)
#End If
  End Sub

  Shared Sub Main()
    RegisterCodePagesEncodingProvider()

    Encoding.GetEncoding("shift_jis").GetBytes("こんにちは、世界!")
  End Sub
End Class

コード中でのコンパイラ定数の定義 (#define/#Const)

#defineディレクティブ(C#)/#Constディレクティブ(VB)を用いることで、コード中でコンパイラ定数を定義することができます。 このディレクティブで定義したコンパイラ定数は、ファイル内でのみ有効となる局所的なコンパイラ定数となります。

C#では、#undefディレクティブを用いることで定義済みのコンパイラ定数を未定義状態にすることができます。 #undefディレクティブも#defineと同様ファイル内のみで有効となるため、特定のファイル内でのみコンパイラ定数を無効としたい場合に用いることができます。 VBでは、#Constディレクティブでコンパイラ定数の値をNothingとして再定義することにより、未定義状態にすることができます。

C#では、#define/#undefはファイルの先頭(usingディレクティブより前)にのみ記述することができます。 クラス内やメソッド内など、コードの途中に記述することはできません。

#define/#undefディレクティブでファイル内のみのコンパイラ定数を定義/未定義化する
#define FOO // このファイル内でのみ有効なコンパイラ定数FOOを定義する
#undef DEBUG // コンパイラ定数DEBUGを未定義化する

using System;

class Sample {
  static void Main()
  {
#if FOO // このファイル内では常にFOOが定義されるので、このコードは常に有効になる
    Console.WriteLine("FOO");
#endif

#if DEBUG // このファイル内では常にDEBUGが未定義化されるので、このコードは常に無効になる
    Console.WriteLine("DEBUG");
#endif

// ファイルの先頭以外で#define/#undefを記述することはできない
#define BAR // error CS1032: ファイルの最初のトークンの後でプリプロセッサのシンボルの定義または定義の解除を行えませんでした
  }
}
#Constディレクティブでファイル内のみのコンパイラ定数を定義/未定義化する
#Const FOO = True ' このファイル内でのみ有効なコンパイラ定数FOOを定義する
#Const DEBUG = Nothing ' コンパイラ定数DEBUGを未定義化する

Imports System

Class Sample
  Shared Sub Main()
#If FOO Then ' このファイル内では常にFOOが定義されるので、このコードは常に有効になる
    Console.WriteLine("FOO")
#End If

#If DEBUG Then ' このファイル内では常にDEBUGが未定義化されるので、このコードは常に無効になる
    Console.WriteLine("DEBUG")
#End If

' ファイルの先頭以外でも#Constを記述することができる
#Const FOO = Nothing

#If FOO Then ' 直上でFOOが未定義化されているので、このコードは常に無効になる
    Console.WriteLine("FOO-2")
#End If
  End Sub
End Class