#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クラスについても取り上げます。 前提となる事項として、コンパイラ定数とその定義方法についても解説します。 const
やreadonly
などのいわゆる定数との違い・比較については§.コンパイラ定数・const/readonly・他の言語との違いを参照してください。
このほか、アプリケーション構成ファイルに基づいて実行コードを切り替えるSwitchクラスについても触れています。
概略
#ifディレクティブとConditional属性は、どちらもコンパイラ定数(シンボルとも呼ばれる)に基づいて条件付きのコンパイル(conditional compilation)を行うための機構です。
コンパイラ定数と条件付きコンパイルを用いると、なんらかの条件付きでコンパイルしたいコードを、コンパイル結果(成果物)に含めるかどうかコンパイル時に選択させることができます。 例えば、ログ出力やデバッグ出力といった、特定のビルド構成(あるいは単に構成、configuration, 詳細)のみで有効としたいコード、あるいは特定のランタイム・プラットフォームなどの対象(target)によって異なる実装となるコードなどがこれにあたります。
#ifディレクティブとConditional属性はどちらもコンパイラ定数に基づく条件付きコンパイルの対象となる点は同じですが、#ifディレクティブではディレクティブ内のコードブロックが条件付きでコンパイルされる一方、Conditional属性ではこの属性が付与されたメソッド自体ではなく、そのメソッドの呼び出しが条件付きでコンパイルされます。
#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ディレクティブは入れ子にすることができます。 演算子として、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
が定義されていない場合このメソッドはコンパイルされず、メソッド自体が存在しないものとなり、呼び出しはエラーとなります。
そこで、呼び出し側も#ifディレクティブで条件付きコンパイルされるようにします。 これで呼び出し側がコンパイルエラーとなることはなくなります。
しかし、呼び出し箇所が数箇所に限られる場合ならともかく、その数が多くなる場合を考慮すると、呼び出しすべてに#ifディレクティブを記述する必要があり、これは非常に面倒です。
また、使用する定数をDEBUG
以外に変更する場合や、条件を他の定数との組み合わせに変える(例えばDEBUG && LOG
などに変更する)場合を考慮すると、保守性にも欠けます。
C/C++のマクロに相当するものがあれば、条件付きコンパイルされる呼び出しをマクロ化して使うことができますが、C#/VBにはそれに相当するものがありません。
今度は、メソッドと呼び出しの両方を条件付きコンパイルするのではなく、メソッド内のコードのみを条件付きコンパイルするように変えます。 これで、目的を一応達成することができます。 記述の煩雑さもなく、保守性の点でも良好なので、場合によってはこれで良しとすることもできます。
ただし、メソッド呼び出し自体は行われるため、引数の評価も行われることになります。 つまり、メソッド内の処理はDEBUG
が定義されている場合にのみ実行される一方、そのメソッドの呼び出し側の処理はDEBUG
が定義されていなくても実行されることになります。 何らかのメソッド呼び出しの結果やプロパティの値を引数として渡す場合、そのメソッドの実行・プロパティの参照自体は行われます。
このため、何もしないメソッドのために、不要な処理が行われる、無用なオーバーヘッドが生じることになります。
これまでのように、引数として評価される可能性のあるメソッド・プロパティの実装を条件付きコンパイルするようにすることもできますが、呼び出し自体が行われることに変わりはなく、また呼び出される側が多くなれば、その分だけ記述が煩雑化することになります。
Conditional属性を使うと、ここまでに挙げた問題の解決、つまり条件付きのメソッド呼び出しと引数評価の無効化を同時に行うことができます。
Conditional属性が指定されたメソッドでは、コンパイラ定数が定義されている場合は通常のメソッドと同様に動作します。 一方、コンパイラ定数が定義されていない場合は、そのメソッドの呼び出し自体がコンパイル時に削除されます。 メソッド呼び出し自体が削除されるため、それに伴う実行時の引数の評価自体も行われなくなります。
(何も出力されない)
Conditional属性
Conditional属性は、コンパイラ定数に従って条件付きのメソッド呼び出しを行うための属性です。 この属性が付与されたメソッドを呼び出す場合、属性で指定されているコンパイラ定数が定義されている場合に限り呼び出しが行われます。
Conditional属性を用いると#ifディレクティブと同様にコンパイラ定数に基づいた条件付きの呼び出しを行うことができます。 一方で#ifディレクティブと異なる点もいくつかあります。
Conditional属性が付与されたメソッドは、コンパイル結果からその呼び出しが削除されます。 そのため、#ifディレクティブとは異なり、コードのコンパイル自体は行われ、メソッド呼び出し部分の文法チェックや引数の型チェックは通常のメソッド呼び出しと同様に行われます。 それらのチェックとコンパイルが行われた後に、最終的なコンパイル結果からメソッド呼び出し部分が除外されることになります。
また、#ifディレクティブとは異なり、Conditional属性が付与されたメソッド自体は削除されません(コンパイル結果に含まれる)。 そのため、nameof
/NameOf
演算子でメソッドを参照することができ、リフレクションにより呼び出すこともできます。
Conditional属性による条件付き呼び出しは、コンパイラによって提供される機能です。 このため、Conditional属性が付与されたメソッドのデリゲートを作成する場合や、Conditional属性が付与されたメソッドをオーバーロードする場合・オーバーロードしてConditional属性を付与する場合の動作は、言語(コンパイラ)によって扱いが異なります。
Conditional属性が付与されている場合、メソッド呼び出しが削除されます。 これに伴い、メソッドに渡される引数の実行時における評価も行われなくなります。 このため、別のメソッドの結果を直接引数として渡すような場合、そのメソッドの呼び出しも行われなくなることになります。
インクリメント(++
/--
)した結果を引数として渡す場合や、参照渡し引数のあるメソッド呼び出しの結果を渡す場合なども同様で、何らかの処理を行った結果を引数として直接渡すような場面では、コンパイラ定数が定義されているかどうかによって動作や結果が変わることになるため、注意が必要です。
Conditional属性では、コンパイラ定数の大文字小文字が区別されます。 そのため、何らかの設定やプロジェクトプロパティからコンパイラ定数の定義を行う場合は、大文字小文字の扱いの違いに注意する必要があります。
Conditionalなメソッドでの戻り値・out引数
Conditional属性が付与されたメソッドの呼び出しはコンパイル時に削除されるという動作となるため、Conditional属性を付与できるメソッドは値を返さない(void
/Sub
であること)、またC#ではout
引数を持たないメソッドに限られます。 これは、メソッド呼び出しが削除されると、戻り値やout
引数に設定されるべき値が確定できなくなるためです。 また、同様の理由からプロパティやコンストラクタにはConditional属性を付与することはできません。
何らかの結果を出力するメソッドにConditional属性を適用したい場合は、戻り値ではなく参照渡し引数ref
/ByRef
を用いることにより、結果を出力させることができます。 ただし、コンパイラ定数の定義の有無により参照渡し引数の値が異なることになるため、一見すると不可解な動作となりうる点に注意が必要です。
複数のコンパイラ定数に対するConditional属性
Conditional属性は、ひとつのメソッドに対して複数付与することができます。 Conditional属性で複数のコンパイラ定数を指定したい場合は、定数ひとつごとにConditional属性を付与します。 ひとつのConditional属性に複数のコンパイラ定数を指定することはできません。
Conditional属性が複数付与されている場合、定義されているコンパイラ定数がひとつでもあれば、そのメソッドに対する呼び出しは削除されません。 つまり、複数のConditional属性はAND演算ではなくOR演算で結合されると見ることができます。
(何も出力されない)
否定条件のConditional属性
Conditional属性では、否定条件の指定、つまりコンパイラ定数が定義されていない場合のみ有効とすることはできません。 NotConditionalのような属性もありません。 このため、否定条件でConditional属性を指定したい場合は、例えばDEBUG
に対するNOT_DEBUG
を定義するなど、否定した条件で定義されるコンパイラ定数を別に用意するのが最も有効な手段となります。
不完全な手段のひとつとして、ファイル内でコンパイラ定数を定義する#define/#Constを使用して条件を否定したコンパイラ定数を定義する方法があります。
この方法で定義したコンパイラ定数はファイル内のみで有効となるため、同一ファイル内での呼び出しに対しては正しく機能するものの、ファイル外からの呼び出しに対しては機能しない点に注意する必要があります。 完全に機能させるには、呼び出し元となるすべてのファイルで条件を否定したコンパイラ定数を定義する必要があります。
Conditionalなメソッドのオーバーライド
C#では、Conditional属性を付与したメソッドをオーバーライドして、さらにConditional属性を付与することはできません。 また、Conditional属性を付与したメソッドをオーバーライドした場合は、基底クラスのメソッドと同様に条件付きの呼び出しが行われます。
VBでは、Conditional属性を付与したメソッドをオーバーライドして、さらにConditional属性を付与すると、基底クラスのConditional属性は上書きされ、オーバーライドしたメソッドのConditional属性が適用されます。
Conditionalなメソッドからのデリゲートの作成
C#では、Conditional属性が付与されたメソッドからデリゲートを作成しようとするとコンパイルエラーとなる一方、VBではデリゲートを作成して呼び出すこともできます。 この場合、コンパイラ定数の定義は無視されることになります。
Conditional属性が付与されたメソッドを参照するデリゲートは扱いが異なる一方、nameof
/NameOf
によるメソッド名の参照は共通して行えます。 また、リフレクションによって呼び出すことも同様に共通して行えます。
リフレクションによるConditionalなメソッドの呼び出し
Conditional属性が付与されているメソッドの通常の呼び出しはコンパイル結果から削除される一方、メソッド自体は削除されません。 そのため、nameof
/NameOf
演算子によってメソッド名を取得することができます。
さらに、リフレクションによる呼び出しはコンパイラに検知されないため、Conditional属性が付与されていてもメソッドを呼び出すことができます。 この場合、エラーや警告は一切出力されません。
上記のように、Conditional属性が付与されているメソッドでも、リフレクションを使って呼び出すことはでき、また呼び出し時にメソッドがConditionalであることを理由とした例外がスローされるようなこともありません。
リフレクションによるConditional属性が付与されているメソッドの呼び出しはコンパイラによって一切検知されない一方、逆に言えば、何らかの理由によりコンパイラ定数の定義とは無関係にメソッドを呼び出したい場合には、コンパイラによるメソッド呼び出しの削除を回避して呼び出せるということになります。
いずれにしても、リフレクションを用いる場合はこのようなリスクとメリットを考慮した上で行う必要があります。
属性に対するConditional属性 (Conditionalな属性クラス)
カスタム属性クラス(Attribute派生クラス)にConditional属性を付与すると、その属性が条件付きコンパイルの対象となります。 つまり、コンパイラ定数が定義されている場合のみ付与される属性を定義することができます。
#ifディレクティブでも属性を条件付きコンパイルすることはできますが、Conditional属性を用いることで記述を#ifディレクティブよりも簡素化することができます。 VBの#Ifディレクティブでは完全なステートメントを記述する必要があるため、Conditionalな属性は特に便利です。
カスタム属性の宣言・実装については属性とメタデータを参照してください。
Conditional属性は、ValidOnにAttributeTargets.Classを含んでいます。 これは、Conditional属性を任意のクラスに付与できることを意味していますが、実際にはカスタム属性クラスにのみConditional属性を付与することができます。
カスタム属性クラス以外のクラスにConditional属性を付与すると、C#ではコンパイルエラーとなります。 VBではコンパイルエラーとはならず、単に無視されるだけとなるようです。
Debugクラス・Traceクラス
ログ出力やデバッグ出力を目的としたクラスとして、.NETではDebugクラスとTraceクラスが用意されています。 標準出力への出力を行うConsoleクラスと同様に、DebugクラスとTraceクラスにはそれぞれ異なる出力先が割り当てられます。
Debug.WriteLineやTrace.WriteLineなど、Debugクラス・Traceクラスの出力メソッドにはConditional属性が付与されています。 DebugクラスはDEBUG
、TraceクラスはTRACE
が指定されたConditional属性が付与されています。
またDebugクラス・Traceクラスでは、AssertやWriteLineIfといったアサーション(assertion, C/C++におけるassert
)用のメソッドも用意されていて、これらのメソッド呼び出しはコンパイラ定数が定義されていない場合は削除されるため、診断や動作検証を目的としたコードを記述するのに向いています。
Debugクラスの出力メソッドは、IDEの出力ウィンドウやデバッグウィンドウなど、実行環境によって異なる箇所に出力されます。 Linuxでは環境変数COMPlus_DebugWriteToStdErr
に1
設定することで標準出力を出力先とすることができます。
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以降で実行した場合は、アプリケーション構成ファイルの設定に関わらず常に初期値に基づく動作となります。
.NET Core/.NET 5以降ではアプリケーション構成ファイルを読み込む機能は実装されていないものの、Switch派生クラスの他の機能はすべて実装されており、また初期値の設定はできることから、代替として環境変数を設定値として用いることができます。 これにより、.NET Framework/Monoではアプリケーション構成ファイルの設定、.NET Core/.NETでは環境変数による設定を用いて、Switch派生クラスの動作を切り替えるようにすることができます。
将来的に.NETでもアプリケーション構成ファイルにおける<system.diagnostics>
要素の設定を読み込むようにするのかどうか不明ですが、少なくとも.NET 5時点で実装されていないことから、優先度・可能性は低いものと思われます。
環境変数の取得については環境変数を参照してください。
コンパイラ定数(シンボル)
コンパイラ定数は、コンパイル時のみ、かつコンパイラのみが使用する(コンパイルされるコードからは参照できない)定数です。
C#では、コンパイラ定数はそれが定義されているかどうかのみを表す、true
またはfalse
のbool
値として評価されます。 C/C++の#define
などとは異なり、コンパイラ定数に数値や文字列などの値を持たせることはできず、またコード中にコンパイラ定数の値を展開することもできません。 例えば、コンパイラ定数DEBUG
に対してbool isDebug = DEBUG;
やConsole.WriteLine(DEBUG);
とするようなことはできません。
VBでは、コンパイラ定数はTrue
またはFalse
のBoolean
値のほか、数値や文字列を値として持たせることができます。 ただし、C/C++の#define
などとは異なり、コード中にコンパイラ定数の値を展開することはできず、コンパイラ定数の値は#If
ディレクティブのみで評価されます。
C#/VBともに、コンパイラ定数をC/C++のマクロのように使用することもできません。 例えば、#define Console.WriteLine Print
のような定義を行うことはできません。
型の別名(エイリアス)を宣言する目的には、using
ディレクティブ/Imports
ステートメントを使うことができます。
コンパイラ定数(シンボル)は、通常プロジェクトファイルやコマンドライン引数で定義しますが、#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
に相当するものです。
コンパイラ定数の定義
コンパイラ定数は通常プロジェクトファイルで定義します。 プロジェクトファイルのプロパティとして<DefineConstants>
要素でビルド時に使用する(コンパイラに渡す)コンパイラ定数を定義することができます。 Condition
属性で条件式を指定することにより、特定の条件の場合のみコンパイラ定数を定義することもできます。
<DefineConstants>
で複数のコンパイラ定数を定義する場合、C#ではセミコロン;
、VBではカンマ,
で区切ります。
TRACE
やDEBUG
、ターゲットフレームワークを表すNET
やNETFRAMEWORK
など、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形式のプロジェクトファイルで定義されるコンパイラ定数を参照してください。
コード中でのコンパイラ定数の定義 (#define/#Const)
#define
ディレクティブ(C#)/#Const
ディレクティブ(VB)を用いることで、コード中でコンパイラ定数を定義することができます。 このディレクティブで定義したコンパイラ定数は、ファイル内でのみ有効となる局所的なコンパイラ定数となります。
C#では、#undef
ディレクティブを用いることで定義済みのコンパイラ定数を未定義状態にすることができます。 #undef
ディレクティブも#define
と同様ファイル内のみで有効となるため、特定のファイル内でのみコンパイラ定数を無効としたい場合に用いることができます。 VBでは、#Const
ディレクティブでコンパイラ定数の値をNothing
として再定義することにより、未定義状態にすることができます。
C#では、#define
/#undef
はファイルの先頭(using
ディレクティブより前)にのみ記述することができます。 クラス内やメソッド内など、コードの途中に記述することはできません。