C#にifdefはない?#if・#defineで条件付きコンパイルする方法をわかりやすく解説

はじめに

C#でC/C++のように「#ifdefを使いたい」と思って検索すると、最初に知っておくべき結論はシンプルです。C#には#ifdefディレクティブはありません。代わりに#if#else#elif#endif#define#undefを使って条件付きコンパイルを行います。 MicrosoftのC#リファレンスでも、条件付きコンパイルを制御するディレクティブとして#if#elif#else#endifが説明されています。

たとえば、Debugビルドのときだけログを出したい場合は、次のように書きます。

C#
#if DEBUG
Console.WriteLine("Debug build only");
#endif

この記事では、「c# ifdef」で調べている人向けに、C#で#ifdefの代わりに何を使えばよいのか、#if#defineの基本、Visual Studioやcsprojでの設定方法、実務でよく使うサンプル、よくあるエラーまでわかりやすく解説します。

1. C#にifdefはない?まず知っておきたい結論

1-1. C#では「ifdef」ではなく「#if」を使う

C#では、C/C++で使われる#ifdef DEBUGのような書き方は使いません。C#で同じようなことをしたい場合は、次のように#if DEBUGと書きます。

C#
#if DEBUG
Console.WriteLine("DEBUGシンボルが定義されているときだけコンパイルされます");
#endif

C#の#ifは、「指定したシンボルが定義されているか」を判定します。DEBUGが定義されていればブロック内のコードがコンパイル対象になり、定義されていなければコンパイル対象から除外されます。

1-2. C/C++の#ifdefとC#の#ifの違い

C/C++では、次のように#ifdefを使います。

C
#ifdef DEBUG
printf("debug mode");
#endif

一方、C#では次のように書きます。

C#
#if DEBUG
Console.WriteLine("debug mode");
#endif

見た目は似ていますが、C#のプリプロセッサディレクティブはC/C++のマクロ機能とは異なります。C#の#defineではシンボルを定義できますが、C/C++のように値付きマクロや関数風マクロを作ることはできません。Microsoftのドキュメントでも、C#の#defineではシンボルに値を割り当てられず、マクロ作成や定数定義には使えないと説明されています。

1-3. C#で条件付きコンパイルが必要になる主な場面

C#で条件付きコンパイルを使う場面は、主に次のようなケースです。

  • Debugビルドだけログや検証コードを有効にしたい

  • Releaseビルドではデバッグ用処理を完全に除外したい

  • OSやターゲットフレームワークごとに使うAPIを切り替えたい

  • UnityでWindows、iOS、Androidなどプラットフォーム別に処理を分けたい

  • テスト用コードや実験中の機能をビルド時に切り替えたい

  • 特定の機能フラグを有効にしたビルドを作りたい

通常のif文でも処理の分岐はできますが、#ifを使うと、条件に合わないコードそのものをコンパイル対象から外せます。この点が、実行時のif文との大きな違いです。

1-4. 「c# ifdef」で検索する人が最初に知りたいポイント

「c# ifdef」で検索している場合、まず押さえるべきポイントは次の4つです。

1つ目は、C#に#ifdefはなく、代わりに#if SYMBOLを使うことです。

2つ目は、#define SYMBOLでシンボルを定義できるものの、値は持たせられないことです。

3つ目は、Visual StudioやcsprojのDefineConstantsでプロジェクト全体にシンボルを定義できることです。DefineConstantsは、プロジェクト内のすべてのソースファイルに対してシンボルを定義するため、ファイル単位の#defineよりチーム開発で扱いやすい設定方法です。

4つ目は、条件付きコンパイルは便利ですが、使いすぎるとコードが読みにくくなるため、用途を絞って使うべきという点です。

2. C#の条件付きコンパイルとは

2-1. 条件付きコンパイルの基本的な仕組み

条件付きコンパイルとは、定義されているシンボルに応じて、特定のコードをコンパイル対象に含めたり除外したりする仕組みです。

C#
#if FEATURE_NEW_LOGIN
Console.WriteLine("新しいログイン機能を使います");
#else
Console.WriteLine("従来のログイン機能を使います");
#endif

この例では、FEATURE_NEW_LOGINが定義されていれば上側のコードがコンパイルされ、定義されていなければ#else側のコードがコンパイルされます。C#では#if#else#elif#endif#define#undefを組み合わせることで、シンボルの有無に応じてコードを含めたり除外したりできます。

2-2. 実行時のif文との違い

通常のif文は、プログラムが実行されている最中に条件を判定します。

C#
if (isDebugMode)
{
Console.WriteLine("debug");
}

この場合、コード自体はコンパイルされます。条件がfalseなら実行されないだけです。

一方、#ifはコンパイル時に判定されます。

C#
#if DEBUG
Console.WriteLine("debug");
#endif

DEBUGが定義されていない場合、このConsole.WriteLineはコンパイル対象から外れます。つまり、実行時に通らないコードではなく、ビルド結果に含まれないコードになります。

2-3. コンパイル対象からコードを除外できる理由

#ifは、C#コンパイラがソースコードを処理する段階で評価されます。指定されたシンボルが条件を満たす場合だけ、その範囲のコードがコンパイル対象になります。条件を満たさないコードは、ビルドされたアセンブリに含まれません。

そのため、Debugビルドでだけ使いたい診断コードや、特定のターゲットフレームワークでしか存在しないAPI呼び出しを安全に切り替える用途に向いています。Microsoftのドキュメントでも、条件付きコンパイルはデバッグビルドや特定構成向けのコンパイルに役立つと説明されています。

2-4. 条件付きコンパイルを使うメリットと注意点

条件付きコンパイルのメリットは、不要なコードをビルド成果物から除外できることです。たとえば、デバッグ用ログ、検証用メソッド、テスト用の仮実装などをReleaseビルドに含めないようにできます。

一方で、注意点もあります。#ifが多すぎると、実際にどのコードがコンパイルされるのか追いにくくなります。また、環境ごとにコンパイルされるコードが変わるため、すべてのビルド構成でテストしないと、特定構成だけコンパイルエラーになることもあります。

条件付きコンパイルは強力ですが、通常の設定ファイル、DI、環境変数、実行時のif文で解決できる問題まで無理に#ifで処理しないことが大切です。

3. C#で#ifdefの代わりに使う#ifの基本構文

3-1. #ifの基本的な書き方

C#で#ifdefの代わりに使う基本形は、次のとおりです。

C#
#if SYMBOL_NAME
// SYMBOL_NAMEが定義されている場合だけコンパイルされる
#endif

具体例は次のようになります。

C#
#if DEBUG
Console.WriteLine("デバッグビルドです");
#endif

DEBUGシンボルが定義されている場合、Console.WriteLineがコンパイルされます。定義されていない場合、その行はコンパイル対象から除外されます。

3-2. #elseを使って条件を分岐する方法

#elseを使うと、シンボルが定義されている場合とされていない場合で処理を分けられます。

C#
#if DEBUG
Console.WriteLine("Debugビルドです");
#else
Console.WriteLine("Releaseビルドなど、DEBUGが未定義のビルドです");
#endif

#elseは、直前の#ifまたは#elifの条件が成立しなかった場合に使われます。実行時のif...elseと似ていますが、判定されるタイミングはコンパイル時です。

3-3. #elifで複数条件を分岐する方法

複数のシンボルを順番に判定したい場合は、#elifを使います。

C#
#if DEVELOPMENT
Console.WriteLine("開発環境向けビルド");
#elif STAGING
Console.WriteLine("ステージング環境向けビルド");
#elif PRODUCTION
Console.WriteLine("本番環境向けビルド");
#else
Console.WriteLine("環境シンボルが定義されていません");
#endif

#elifを使うと、複数のビルド構成を1つの条件ブロックで整理できます。ただし、条件が増えすぎると読みづらくなるため、環境差分が大きい場合は設計を見直すことも必要です。

3-4. #endifで条件ブロックを閉じる方法

#ifで始めた条件ブロックは、必ず#endifで閉じます。

C#
#if DEBUG
Console.WriteLine("debug");
#endif

#endifを書き忘れると、コンパイラが条件ブロックの終わりを判断できず、コンパイルエラーになります。ネストした条件を書く場合は、どの#ifに対応する#endifなのかコメントを添えると読みやすくなります。

C#
#if DEBUG
Console.WriteLine("debug");

#if VERBOSE
Console.WriteLine("verbose debug log");
#endif // VERBOSE

#endif // DEBUG

3-5. !・&&・||を使った条件式の書き方

C#の#ifでは、!&&||==!=、かっこを使って条件を組み合わせられます。#if DEBUG#if (DEBUG == true)は同じ意味で、&&||で複数のシンボルを組み合わせることもできます。

C#
#if DEBUG && VERBOSE
Console.WriteLine("詳細なデバッグログを出力します");
#endif

否定条件は!を使います。

C#
#if !DEBUG
Console.WriteLine("DEBUGが定義されていないビルドです");
#endif

複雑な条件はかっこでグループ化できます。

C#
#if (DEBUG || STAGING) && FEATURE_PAYMENT
Console.WriteLine("検証用の決済機能を有効化");
#endif

ただし、C#の#ifはあくまでシンボルの有無を判定する仕組みです。C/C++のように#define VERSION 3として、#if VERSION >= 2のような数値比較をする用途には使えません。

4. #defineでシンボルを定義する方法

4-1. #defineの基本構文

C#では、#defineを使って条件付きコンパイル用のシンボルを定義できます。

C#
#define VERBOSE

using System;

class Program
{
static void Main()
{
#if VERBOSE
Console.WriteLine("詳細ログを出力します");
#endif
}
}

この例では、ファイル先頭でVERBOSEを定義しているため、#if VERBOSEの中のコードがコンパイルされます。

4-2. #defineをファイル先頭に書く必要がある理由

C#の#defineは、ファイル内で通常のコードより前に書く必要があります。Microsoftのドキュメントでも、#defineはプリプロセッサディレクティブではない命令より前に記述する必要があると説明されています。

次のような書き方は避けるべきです。

C#
using System;

#define VERBOSE // この位置では不適切

正しくは次のように、usingより前に書きます。

C#
#define VERBOSE

using System;

4-3. #undefでシンボルを無効化する方法

#undefを使うと、定義済みのシンボルを未定義にできます。

C#
#define VERBOSE
#undef VERBOSE

using System;

class Program
{
static void Main()
{
#if VERBOSE
Console.WriteLine("このコードはコンパイルされません");
#endif
}
}

#undef VERBOSEによってVERBOSEが未定義になるため、#if VERBOSEの中はコンパイル対象になりません。#undef#defineと同じく、通常のコードより前に書くのが基本です。

4-4. #defineは値を持てない点に注意

C#の#defineで特に間違えやすいのは、値を持てない点です。次のようなC/C++風の書き方はできません。

C#
#define VERSION 3
#define API_URL "https://example.com"

C#の#defineは、あくまで「シンボルが定義されているかどうか」を表すだけです。Microsoftのドキュメントでも、C#の#defineではシンボルに値を割り当てられないため、定数値を宣言する用途には使えないと説明されています。

4-5. #defineと定数・変数の違い

#defineはコンパイル条件を表すためのシンボルです。一方、constや変数はC#のコードとして扱われます。

C#
const int MaxRetryCount = 3;

このMaxRetryCountはプログラム内で値として使えます。しかし、#if MaxRetryCount > 2のようには使えません。

条件付きコンパイルで使うのはDEBUGTRACEFEATURE_Xのようなシンボルです。アプリケーションの設定値として使うものは、conststatic readonly、設定ファイル、環境変数などで管理しましょう。

5. DEBUGやTRACEを使った実践例

5-1. DEBUGビルド時だけログを出力する方法

C#で最もよく使われる条件付きコンパイルの例が、DEBUGシンボルです。

C#
#if DEBUG
Console.WriteLine("デバッグ情報: userId = " + userId);
#endif

DEBUGが定義されているビルドではログ出力コードが含まれ、定義されていないビルドではコード自体が除外されます。Microsoftのドキュメントでは、DEBUGシンボルはビルド構成プロパティ、つまりDebugまたはReleaseモードに応じて自動的に設定される例として説明されています。

5-2. Releaseビルドでは処理を除外する方法

Releaseビルドでデバッグ用コードを含めたくない場合は、#if DEBUGで囲むのが基本です。

C#
public void Save(User user)
{
#if DEBUG
ValidateForDebug(user);
#endif

SaveToDatabase(user);
}

この場合、ReleaseビルドでDEBUGが定義されていなければ、ValidateForDebug(user);はコンパイルされません。重い検証処理や一時的なデバッグ出力をReleaseビルドから確実に外したいときに便利です。

5-3. TRACEシンボルの役割

TRACEは、トレース出力や診断用コードを制御するためによく使われるシンボルです。DEBUGは主にデバッグビルド向け、TRACEは診断・追跡ログ向けという感覚で使い分けられます。

C#
#if TRACE
Console.WriteLine("処理を開始しました");
#endif

プロジェクト設定によっては、DebugビルドとReleaseビルドの両方でTRACEが定義されることがあります。そのため、Releaseビルドにも残してよい診断ログなのか、Debug専用のログなのかを意識して使い分ける必要があります。

5-4. デバッグ用コードを安全に管理するコツ

デバッグ用コードを#if DEBUGで管理するときは、次の点に注意しましょう。

まず、機密情報をログに出さないことです。Debugビルドだけのつもりでも、設定ミスでログが出力される可能性はあります。

次に、#if DEBUGの中だけで変数を宣言し、その外側で使うような構造にしないことです。

C#
#if DEBUG
var message = "debug";
#endif

Console.WriteLine(message); // Releaseビルドでコンパイルエラーになる

このコードは、Debugビルドでは動いてもReleaseビルドではmessageが存在しないためコンパイルエラーになります。条件付きコンパイルを使う場合は、すべてのビルド構成でコンパイルできるか確認しましょう。

5-5. Console.WriteLineやDebug.WriteLineとの使い分け

Console.WriteLineはコンソールに出力する通常のメソッドです。#if DEBUGで囲まなければ、Releaseビルドにもコードが残ります。

C#
Console.WriteLine("常にコンパイルされる");

一方、Debug.WriteLineはデバッグ用途でよく使われます。

C#
System.Diagnostics.Debug.WriteLine("debug log");

ただし、#if DEBUGで囲むかどうかによって、コードをコンパイル対象に含めるかどうかの意味が変わります。コード自体を除外したいなら#if DEBUG、診断用メソッド呼び出しとして整理したいならDebug.WriteLineConditional属性の利用を検討するとよいでしょう。

6. Visual Studioやcsprojで条件付きコンパイルシンボルを設定する方法

6-1. Visual Studioのプロジェクト設定から定義する方法

Visual Studioでは、プロジェクトのビルド設定から条件付きコンパイルシンボルを定義できます。一般的には、プロジェクトのプロパティを開き、ビルド関連の設定で「条件付きコンパイル シンボル」またはそれに相当する項目にシンボル名を追加します。

たとえば、FEATURE_PAYMENTを定義しておくと、コード内で次のように使えます。

C#
#if FEATURE_PAYMENT
PaymentService.Enable();
#endif

Visual Studioで設定したシンボルは、プロジェクト設定として管理されるため、ソースファイルごとに#defineを書くよりも一貫性を保ちやすくなります。

6-2. csprojのDefineConstantsで定義する方法

SDKスタイルのC#プロジェクトでは、.csprojDefineConstantsを書いてシンボルを定義できます。

XML
<PropertyGroup>
<DefineConstants>FEATURE_PAYMENT;USE_MOCK_API</DefineConstants>
</PropertyGroup>

DefineConstantsで定義したシンボルは、プロジェクト内のすべてのソースコードファイルに対して有効になります。Microsoftのドキュメントでも、DefineConstantsはプログラムのすべてのソースコードファイル内のシンボルを定義し、#defineと同じ効果を持つ一方で、プロジェクト全体に効く点が異なると説明されています。

6-3. DebugとReleaseでシンボルを切り替える方法

DebugビルドとReleaseビルドでシンボルを切り替えたい場合は、Condition付きのPropertyGroupを使います。

XML
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<DefineConstants>DEBUG;TRACE;USE_MOCK_API</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DefineConstants>TRACE;PRODUCTION</DefineConstants>
</PropertyGroup>

このように設定すると、DebugビルドではUSE_MOCK_APIを有効にし、ReleaseビルドではPRODUCTIONを有効にする、といった切り替えができます。

既存のシンボルを残しながら追加したい場合は、次のように$(DefineConstants)を含めます。

XML
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<DefineConstants>$(DefineConstants);USE_MOCK_API</DefineConstants>
</PropertyGroup>

6-4. dotnet buildでシンボルを指定する方法

コマンドラインから一時的にシンボルを指定したい場合は、dotnet buildでMSBuildプロパティを渡せます。

Bash
dotnet build -p:DefineConstants="FEATURE_PAYMENT;USE_MOCK_API"

Release構成と組み合わせる場合は、次のように指定します。

Bash
dotnet build -c Release -p:DefineConstants="PRODUCTION;FEATURE_PAYMENT"

CI/CDで特定機能を有効にしたビルドを作る場合や、環境ごとにシンボルを切り替えたい場合に便利です。

6-5. チーム開発ではソース内#defineよりプロジェクト設定が向いている理由

チーム開発では、ソースファイル先頭に#defineを書くより、Visual Studioやcsprojでシンボルを管理する方が向いています。

理由は、#defineは基本的にそのファイル内での定義になるため、複数ファイルに同じシンボルを効かせたい場合に管理が煩雑になるからです。一方、DefineConstantsならプロジェクト全体に適用できます。

また、Debug、Release、Staging、Productionなどの構成ごとに定義を切り替える場合も、プロジェクト設定やCI/CDのビルドパラメータで管理した方が変更履歴を追いやすく、属人化しにくくなります。

7. よくある用途別のサンプルコード

7-1. OSごとに処理を切り替える

OSごとに処理を切り替える場合、ターゲットフレームワークやプロジェクト設定で定義されたシンボル、または独自に定義したシンボルを使います。.NETのSDKスタイルプロジェクトでは、ターゲットフレームワークを表す定義済みプリプロセッサシンボルも利用できます。

C#
#if WINDOWS
Console.WriteLine("Windows向け処理");
#elif LINUX
Console.WriteLine("Linux向け処理");
#elif MACOS
Console.WriteLine("macOS向け処理");
#else
Console.WriteLine("その他のOS向け処理");
#endif

ただし、実行環境のOSを判定したいだけなら、条件付きコンパイルではなく実行時判定の方が適していることもあります。

C#
if (OperatingSystem.IsWindows())
{
Console.WriteLine("Windowsで実行中");
}

ビルド成果物自体をOSごとに変えたいなら#if、同じ成果物の中で実行時に切り替えたいなら通常のif文、という基準で使い分けましょう。

7-2. 開発環境・本番環境で処理を切り替える

開発環境と本番環境で処理を切り替える例です。

C#
#if DEVELOPMENT
var apiBaseUrl = "https://dev-api.example.com";
#elif STAGING
var apiBaseUrl = "https://stg-api.example.com";
#elif PRODUCTION
var apiBaseUrl = "https://api.example.com";
#else
#error 環境シンボルが定義されていません
#endif

ただし、APIのURLや接続文字列のような値は、通常は設定ファイルや環境変数で管理する方が柔軟です。#ifで環境を切り替えるのは、ビルド成果物そのものを環境ごとに明確に分けたい場合に限定するとよいでしょう。

7-3. Unityでプラットフォーム別に処理を分ける

Unityでは、プラットフォーム別の条件付きコンパイルがよく使われます。Unityのドキュメントでも、定義済みのスクリプティングシンボルに応じてコードをコンパイル対象に含めたり除外したりできると説明されています。

C#
#if UNITY_EDITOR
Debug.Log("Unity Editorで実行中");
#elif UNITY_ANDROID
Debug.Log("Android向けビルド");
#elif UNITY_IOS
Debug.Log("iOS向けビルド");
#else
Debug.Log("その他のプラットフォーム");
#endif

Unityでは、UNITY_EDITORUNITY_ANDROIDUNITY_IOSなどのシンボルを使って、エディタ上だけの処理や、モバイル向けの処理を分けられます。

7-4. テスト用コードだけを有効にする

テスト用の補助コードを特定ビルドだけで有効にしたい場合は、独自シンボルを使います。

C#
#if ENABLE_TEST_HELPERS
public static class TestDataFactory
{
public static User CreateDummyUser()
{
return new User
{
Name = "Test User",
Email = "test@example.com"
};
}
}
#endif

このようにしておけば、ENABLE_TEST_HELPERSが定義されたビルドだけでTestDataFactoryがコンパイルされます。本番ビルドにテスト用コードを含めたくない場合に有効です。

7-5. 特定機能の有効・無効をビルド時に切り替える

フィーチャーフラグのように、特定機能をビルド時に切り替えることもできます。

C#
public void Execute()
{
#if FEATURE_COUPON
ApplyCoupon();
#endif

Checkout();
}

この方法では、FEATURE_COUPONが定義されていないビルドでは、ApplyCoupon();の呼び出し自体が含まれません。

ただし、機能のON/OFFを運用中に切り替えたい場合は、条件付きコンパイルよりも設定ファイル、DB、リモート設定、Feature Managementライブラリなどの方が向いています。#ifは、ビルド時に固定してよい機能の切り替えに使いましょう。

8. C#の#ifでよくあるエラーと解決方法

8-1. 「シンボルを定義したのに#ifが有効にならない」原因

#ifが期待どおりに有効にならない場合、まず確認すべきなのはシンボル名の一致です。

C#
#if FEATURE_LOGIN
Console.WriteLine("login feature");
#endif

このコードを有効にするには、FEATURE_LOGINという名前で正確に定義する必要があります。FEATURE-LOGINFEATURE_LOGIN Feature_Loginのように名前がずれていると一致しません。

また、csprojでDefineConstantsを上書きしてしまい、既存のDEBUGTRACEが消えているケースもあります。既存値に追加したい場合は、次のように書きます。

XML
<DefineConstants>$(DefineConstants);FEATURE_LOGIN</DefineConstants>

8-2. #defineを書く位置を間違えているケース

#defineはファイルの先頭、通常のコードより前に書く必要があります。次のような位置に書くと問題になります。

C#
using System;

#define FEATURE_LOGIN

正しくは次のようにします。

C#
#define FEATURE_LOGIN

using System;

#defineを書いたのに有効にならない場合は、まずファイル内の位置を確認しましょう。#defineは、プリプロセッサディレクティブではない通常の命令より前に書く必要があります。

8-3. true/falseや数値比較が使えないケース

C#の#ifでは、シンボルの定義有無をtrueまたはfalseのように扱えます。

C#
#if DEBUG == true
Console.WriteLine("debug");
#endif

しかし、C/C++のように値付きマクロを定義して数値比較することはできません。

C#
#define VERSION 3

#if VERSION >= 2
// C#ではこのような使い方はできない
#endif

バージョンや数値で処理を変えたい場合は、ターゲットフレームワーク用のシンボルを使うか、通常のC#コードで定数や設定値を使って判定しましょう。

8-4. 条件付きコンパイルでコードが読みにくくなる問題

#ifを多用すると、コードが断片化して読みづらくなります。

C#
#if A
DoA();
#endif

Common();

#if B
DoB();
#endif

#if C
DoC();
#endif

このようなコードが増えると、どのビルドでどの処理が有効なのか把握しにくくなります。対策としては、条件分岐をメソッド単位やクラス単位に閉じ込めることが有効です。

C#
public void Execute()
{
RunEnvironmentSpecificProcess();
RunCommonProcess();
}

private void RunEnvironmentSpecificProcess()
{
#if DEVELOPMENT
RunDevelopmentProcess();
#elif PRODUCTION
RunProductionProcess();
#endif
}

条件付きコンパイルの範囲を局所化すると、読みやすさと保守性を保ちやすくなります。

8-5. IDE上でグレーアウトされるコードの意味

Visual StudioやJetBrains RiderなどのIDEでは、現在のビルド構成でコンパイル対象にならない#ifブロック内のコードがグレーアウトされることがあります。

C#
#if DEBUG
Console.WriteLine("debug");
#endif

Release構成を選んでいてDEBUGが定義されていない場合、この中のコードがグレーアウトされることがあります。これはエラーではなく、「現在の構成ではこのコードはコンパイルされない」という意味です。

ただし、別の構成に切り替えたときにはコンパイルされる可能性があります。グレーアウトされているコードでも、対象構成では正しくコンパイルできるように管理する必要があります。

9. 条件付きコンパイルを使うときのベストプラクティス

9-1. #ifを使いすぎない

#ifは便利ですが、使いすぎるとコードの見通しが悪くなります。特に、1つのメソッド内に複数の#ifが入り込むと、実際にどの処理がコンパイルされるのか追いにくくなります。

基本方針として、条件付きコンパイルは「ビルド成果物からコードを除外する必要がある場合」に限定して使いましょう。実行時の設定で十分な場合は、通常のif文や設定ファイルを使う方が保守しやすくなります。

9-2. 条件分岐はできるだけ局所化する

条件付きコンパイルは、できるだけ狭い範囲に閉じ込めるのが理想です。

悪い例は、メソッド全体に条件が散らばっているコードです。

C#
public void Process()
{
#if DEBUG
Console.WriteLine("start");
#endif

DoWork();

#if DEBUG
Console.WriteLine("end");
#endif
}

改善例として、デバッグログ用のメソッドに分離できます。

C#
public void Process()
{
LogDebug("start");
DoWork();
LogDebug("end");
}

private void LogDebug(string message)
{
#if DEBUG
Console.WriteLine(message);
#endif
}

こうすると、#ifの影響範囲が小さくなり、メイン処理が読みやすくなります。

9-3. シンボル名は意味が伝わる名前にする

シンボル名は、何を表しているのか一目でわかる名前にしましょう。

C#
#if FEATURE_PAYMENT
#endif

#if USE_MOCK_API
#endif

#if ENABLE_TEST_HELPERS
#endif

逆に、次のような名前は避けた方がよいです。

C#
#if FLAG1
#endif

#if TEMP
#endif

#if X
#endif

短すぎる名前や一時的な名前は、後から見たときに意味がわからなくなります。チーム開発では、シンボル名の命名ルールを決めておくと混乱を防げます。

9-4. 環境差分は設定ファイルやDIで扱うべき場合もある

開発環境、本番環境、ステージング環境の違いをすべて#ifで扱うのはおすすめできません。

たとえば、APIのURL、ログレベル、接続文字列、外部サービスのキーなどは、ビルド時ではなく実行時に切り替えたいことが多い項目です。このような値は、appsettings.json、環境変数、Secret Manager、クラウド環境の設定、DIで注入するオプションなどで管理する方が自然です。

#ifが向いているのは、特定のコードをビルド成果物に含めるかどうかを決めたい場合です。設定値を変えたいだけなら、条件付きコンパイルではなく設定管理の仕組みを使いましょう。

9-5. メンテナンスしやすい条件付きコンパイルの設計

メンテナンスしやすくするには、次のような設計を意識します。

まず、シンボルはプロジェクト設定やCI/CDで一元管理します。ファイルごとの#defineは、サンプルや一時的な検証には便利ですが、実務では見落としの原因になりやすいです。

次に、#ifの範囲を小さくします。メソッド全体、クラス全体、ファイル全体など、単位を明確にすると読みやすくなります。

さらに、すべてのビルド構成でコンパイルテストを行います。Debugでは通るがReleaseでは壊れる、特定シンボルを有効にしたときだけ壊れる、という問題は条件付きコンパイルでよく起こります。CIで複数構成をビルドする仕組みを用意しておくと安心です。

10. C#のifdefに関するよくある質問

10-1. C#で#ifdefは使える?

C#で#ifdefは使えません。C#では、#ifdef DEBUGではなく、次のように#if DEBUGを使います。

C#
#if DEBUG
Console.WriteLine("debug");
#endif

C#の条件付きコンパイルでは、#if#elif#else#endifを使ってシンボルの有無を判定します。

10-2. #if DEBUGはどこで定義されている?

DEBUGは、Visual Studioや.NETのビルド構成によって定義される代表的な条件付きコンパイルシンボルです。一般的にはDebug構成で定義され、Release構成では定義されない形で使われます。MicrosoftのC#リファレンスでも、DEBUGシンボルはビルド構成プロパティに応じて自動的に設定される例として説明されています。

プロジェクトによっては、csprojのDefineConstantsに明示的に書かれている場合もあります。

XML
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>

10-3. #defineで文字列や数値を定義できる?

C#の#defineでは、文字列や数値を定義できません。次のような書き方はC#ではできません。

C#
#define NAME "sample"
#define COUNT 10

C#の#defineは、シンボルの有無を表すためだけに使います。文字列や数値の定数が必要な場合は、conststatic readonly、設定ファイルなどを使いましょう。C#の#defineではシンボルに値を割り当てられないことが公式ドキュメントでも説明されています。

10-4. #ifとConditional属性の違いは?

#ifは、囲まれたコードそのものをコンパイル対象に含めるか除外するかを制御します。

C#
#if DEBUG
LogDebug("debug");
#endif

一方、Conditional属性は、指定したシンボルが定義されている場合だけメソッド呼び出しを含める仕組みです。

C#
using System.Diagnostics;

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

呼び出し側は通常どおり書けます。

C#
Logger.DebugLog("debug message");

Conditional属性が付いたメソッドの呼び出しは、指定シンボルが定義されていない場合にコンパイラによって省略されます。Conditionalはメソッドまたは属性クラスに適用でき、条件付きメソッドはvoidである必要があります。Microsoftのドキュメントでも、Conditional属性は#if...#endifでメソッドを囲むよりクリーンでエラーが起きにくい方法として説明されています。

10-5. 条件付きコンパイルとプリプロセッサディレクティブの違いは?

プリプロセッサディレクティブとは、#if#define#region#nullableなど、#で始まるC#の特別な指示の総称です。

その中でも条件付きコンパイルは、#if#elif#else#endif#define#undefなどを使って、コードをコンパイル対象に含めるか除外する仕組みを指します。

つまり、条件付きコンパイルはプリプロセッサディレクティブの用途の一つです。すべてのプリプロセッサディレクティブが条件付きコンパイルのために使われるわけではありません。

まとめ

C#には、C/C++のような#ifdefはありません。「c# ifdef」と検索してたどり着いた場合は、まずC#では#ifdefの代わりに#ifを使うと覚えましょう。

基本形は次のとおりです。

C#
#if DEBUG
Console.WriteLine("DEBUGが定義されているときだけコンパイル");
#endif

シンボルをファイル内で定義する場合は、次のように#defineを使います。

C#
#define FEATURE_X

ただし、C#の#defineは値を持てません。文字列や数値の定数を定義するものではなく、条件付きコンパイル用のシンボルを定義するものです。

実務では、ファイル先頭の#defineよりも、Visual Studioのプロジェクト設定やcsprojのDefineConstantsで管理する方が安全です。プロジェクト全体で同じシンボルを使えるため、チーム開発やCI/CDとの相性もよくなります。

条件付きコンパイルは、Debugビルドだけのログ出力、Releaseビルドからのデバッグ処理除外、Unityのプラットフォーム別処理、テスト用コードの切り替え、機能単位のビルド切り替えなどに便利です。一方で、使いすぎるとコードが読みにくくなり、構成ごとの差分も追いにくくなります。

C#でifdef相当の処理をしたいときは、#if#else#elif#endifを正しく使い、シンボルの管理場所と用途を明確にすることが重要です。