【C#】#defineとは?使い方・定数との違い・#ifによる条件付きコンパイルを解説

はじめに

C#には#defineというプリプロセッサディレクティブがあります。名前だけを見ると、C言語やC++の#defineのように「定数」や「マクロ」を定義する機能だと思うかもしれません。

しかし、C#の#defineはC言語の#defineとは役割が大きく異なります。C#では、#defineを使って数値や文字列を定義したり、コード片をマクロとして置き換えたりすることはできません。

C#の#defineは、主に#if#else#elif#endifと組み合わせて、条件付きコンパイルを行うために使います。

この記事では、C#の#defineとは何か、基本的な使い方、constreadonlyとの違い、Visual Studioやcsprojでのシンボル定義方法、実務での使いどころまで解説します。

1. C#の#defineとは?最初に結論を解説

C#の#defineは、コンパイル時に使う「シンボル」を定義するためのプリプロセッサディレクティブです。

まずは、C#における#defineの役割を正しく理解しておきましょう。

1-1. #defineは「シンボル」を定義するプリプロセッサディレクティブ

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

たとえば、次のように書きます。

C#
#define MY_SYMBOL

using System;

class Program
{
static void Main()
{
#if MY_SYMBOL
Console.WriteLine("MY_SYMBOLが定義されています");
#endif
}
}

この例では、ファイルの先頭でMY_SYMBOLというシンボルを定義しています。

その後、#if MY_SYMBOLによって、MY_SYMBOLが定義されている場合だけコードをコンパイル対象に含めています。

つまり、#defineは実行時の処理を切り替えるものではなく、コンパイル時にコードを含めるかどうかを切り替えるためのものです。

1-2. C言語の#defineのように定数やマクロを定義するものではない

C言語やC++では、#defineを使って次のように定数やマクロを定義できます。

C
#define MAX_COUNT 100
#define SQUARE(x) ((x) * (x))

しかし、C#ではこのような使い方はできません。

次のようなコードはC#ではエラーになります。

C#
#define MAX_COUNT 100

C#の#defineで定義できるのは、値を持たないシンボルだけです。

そのため、数値や文字列などの固定値を定義したい場合は、#defineではなくconstreadonlyを使います。

C#
const int MaxCount = 100;

C#の#defineは、あくまで「このシンボルが定義されているかどうか」を判定するためのものです。

1-3. 主な用途は#ifと組み合わせた条件付きコンパイル

C#の#defineは、単独で使うよりも#ifと組み合わせて使うのが一般的です。

C#
#define DEBUG_LOG

using System;

class Program
{
static void Main()
{
#if DEBUG_LOG
Console.WriteLine("デバッグログを出力します");
#endif

Console.WriteLine("通常処理を実行します");
}
}

このコードでは、DEBUG_LOGが定義されている場合だけ、デバッグログ出力用のコードがコンパイルされます。

DEBUG_LOGを定義しなければ、その部分のコードはコンパイル対象から除外されます。

C#
// #define DEBUG_LOG

using System;

class Program
{
static void Main()
{
#if DEBUG_LOG
Console.WriteLine("デバッグログを出力します");
#endif

Console.WriteLine("通常処理を実行します");
}
}

このように、C#の#defineは「特定のコードをビルドに含めるかどうか」を切り替えるために使います。

2. C#の#defineの基本構文と使い方

ここからは、C#の#defineの基本構文と使い方を見ていきます。

#define自体はシンプルですが、書ける場所や書き方にルールがあります。

2-1. #define シンボル名 の書き方

C#の#defineは、次のように書きます。

C#
#define シンボル名

具体例は次のとおりです。

C#
#define DEBUG_MODE
#define FEATURE_A
#define LOCAL_ENV

シンボル名は、条件付きコンパイルで判定するための名前です。

C#
#define FEATURE_A

using System;

class Program
{
static void Main()
{
#if FEATURE_A
Console.WriteLine("FEATURE_Aが有効です");
#endif
}
}

FEATURE_Aが定義されているため、#if FEATURE_Aの中のコードはコンパイル対象になります。

2-2. #defineはファイルの先頭に書く必要がある

C#の#defineは、基本的にファイルの先頭に書く必要があります。

正しい例は次のとおりです。

C#
#define SAMPLE

using System;

class Program
{
static void Main()
{
#if SAMPLE
Console.WriteLine("SAMPLEが定義されています");
#endif
}
}

一方で、次のようにコードの途中に#defineを書くとエラーになります。

C#
using System;

#define SAMPLE

class Program
{
static void Main()
{
}
}

#define#undefは、そのファイル内の実際のコードより前に記述する必要があります。

コメントや空行は問題ありませんが、usingやクラス定義などのコードより後には書けません。

2-3. セミコロンは不要

C#の#defineにはセミコロンを付けません。

正しい書き方は次のとおりです。

C#
#define MY_SYMBOL

次のようにセミコロンを付ける必要はありません。

C#
#define MY_SYMBOL;

#defineはC#の通常の文ではなく、プリプロセッサディレクティブです。

そのため、int x = 10;のような通常のC#文とは異なり、末尾にセミコロンを書きません。

2-4. #undefで定義済みシンボルを解除する方法

#defineで定義したシンボルは、#undefを使って未定義にできます。

C#
#define FEATURE_A
#undef FEATURE_A

using System;

class Program
{
static void Main()
{
#if FEATURE_A
Console.WriteLine("FEATURE_Aは有効です");
#else
Console.WriteLine("FEATURE_Aは無効です");
#endif
}
}

この例では、最初にFEATURE_Aを定義していますが、その後に#undef FEATURE_Aで解除しています。

そのため、#if FEATURE_Aの条件は成立せず、#else側のコードがコンパイルされます。

#undef#defineと同じく、ファイル内のコードより前に書く必要があります。

2-5. #defineで値を代入できない理由

C#の#defineでは、値を代入できません。

次のような書き方はできません。

C#
#define MAX_COUNT 100
#define APP_NAME "SampleApp"

C#の#defineは、値を持つ定数を作るための仕組みではなく、条件付きコンパイルで使うシンボルを定義するための仕組みだからです。

数値や文字列などの値を扱いたい場合は、constreadonlyを使います。

C#
const int MaxCount = 100;
const string AppName = "SampleApp";

C#では、#defineで値を持たせるのではなく、次のように役割を分けて考えると分かりやすいです。

C#
#define USE_SAMPLE_FEATURE

const int MaxCount = 100;

USE_SAMPLE_FEATUREは、機能を有効化するかどうかをコンパイル時に判定するためのシンボルです。

一方、MaxCountはプログラム内で使う値です。

3. #ifによる条件付きコンパイルの使い方

C#の#defineを理解するうえで欠かせないのが、#ifによる条件付きコンパイルです。

#ifを使うと、特定のシンボルが定義されている場合だけ、そのコードをコンパイル対象にできます。

3-1. #ifでシンボルが定義されている場合だけコードを有効にする

#ifは、指定したシンボルが定義されているかどうかを判定します。

C#
#define ENABLE_LOG

using System;

class Program
{
static void Main()
{
#if ENABLE_LOG
Console.WriteLine("ログ出力が有効です");
#endif

Console.WriteLine("メイン処理を実行します");
}
}

この例では、ENABLE_LOGが定義されているため、ログ出力のコードがコンパイルされます。

もし#define ENABLE_LOGを削除またはコメントアウトすると、#if ENABLE_LOGから#endifまでのコードはコンパイル対象から除外されます。

C#
// #define ENABLE_LOG

using System;

class Program
{
static void Main()
{
#if ENABLE_LOG
Console.WriteLine("ログ出力が有効です");
#endif

Console.WriteLine("メイン処理を実行します");
}
}

この場合、ログ出力のコードはコンパイルされないため、実行時にも存在しません。

3-2. #elseで条件に合わない場合の処理を書く

#elseを使うと、シンボルが定義されていない場合のコードを書けます。

C#
#define USE_NEW_FEATURE

using System;

class Program
{
static void Main()
{
#if USE_NEW_FEATURE
Console.WriteLine("新機能を使用します");
#else
Console.WriteLine("旧機能を使用します");
#endif
}
}

USE_NEW_FEATUREが定義されている場合は、#if側のコードがコンパイルされます。

定義されていない場合は、#else側のコードがコンパイルされます。

C#
// #define USE_NEW_FEATURE

using System;

class Program
{
static void Main()
{
#if USE_NEW_FEATURE
Console.WriteLine("新機能を使用します");
#else
Console.WriteLine("旧機能を使用します");
#endif
}
}

このように、#elseを使うことで、ビルド時の条件に応じてコンパイルするコードを切り替えられます。

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

複数の条件を分岐したい場合は、#elifを使います。

C#
#define ENV_DEVELOPMENT

using System;

class Program
{
static void Main()
{
#if ENV_DEVELOPMENT
Console.WriteLine("開発環境です");
#elif ENV_STAGING
Console.WriteLine("ステージング環境です");
#elif ENV_PRODUCTION
Console.WriteLine("本番環境です");
#else
Console.WriteLine("環境が指定されていません");
#endif
}
}

この例では、ENV_DEVELOPMENTが定義されているため、「開発環境です」と表示するコードだけがコンパイルされます。

#elifを使うと、複数のシンボルを順番に判定できます。

ただし、環境ごとの処理をすべて条件付きコンパイルで分けると、コードが読みづらくなることがあります。

単なる設定値の切り替えであれば、appsettings.jsonや環境変数を使うほうが適している場合もあります。

3-4. #endifで条件付きコンパイルを終了する

#ifを使ったら、必ず#endifで条件付きコンパイルの範囲を終了します。

C#
#define SAMPLE

using System;

class Program
{
static void Main()
{
#if SAMPLE
Console.WriteLine("SAMPLEが定義されています");
#endif
}
}

#endifを書き忘れると、コンパイルエラーになります。

C#
#define SAMPLE

using System;

class Program
{
static void Main()
{
#if SAMPLE
Console.WriteLine("SAMPLEが定義されています");
}
}

このように#endifがないと、どこまでが条件付きコンパイルの対象なのか判断できません。

#if#else#elifを使う場合は、必ず最後に#endifを書く必要があります。

3-5. #if DEBUGを使ったデバッグ用コードの例

C#では、Debug構成でビルドするとDEBUGシンボルが定義されることがあります。

そのため、#if DEBUGを使うと、デバッグビルド時だけ有効になるコードを書けます。

C#
using System;

class Program
{
static void Main()
{
#if DEBUG
Console.WriteLine("デバッグビルドです");
#endif

Console.WriteLine("アプリケーションを実行します");
}
}

Debug構成では、#if DEBUGの中のコードがコンパイルされます。

Release構成でDEBUGが定義されていない場合、そのコードはコンパイル対象から除外されます。

デバッグ時だけ詳細なログを出したい場合や、開発中だけ検証コードを入れたい場合に便利です。

C#
#if DEBUG
Console.WriteLine($"現在の値: {value}");
#endif

ただし、本格的なログ出力にはILoggerなどのロギング機能を使うほうが適していることも多いです。

#if DEBUGは、あくまで「本番ビルドに含めたくないコード」を除外する目的で使うのが基本です。

4. #defineと定数const・readonlyの違い

C#の#defineを学ぶときに混同しやすいのが、constreadonlyとの違いです。

結論からいうと、#defineは値を定義するものではありません。

値を扱うならconstreadonlyを使い、コンパイル対象を切り替えるなら#defineや条件付きコンパイルシンボルを使います。

4-1. #defineはコンパイル対象を切り替えるためのシンボル

#defineは、条件付きコンパイルに使うシンボルを定義します。

C#
#define USE_FEATURE_X

using System;

class Program
{
static void Main()
{
#if USE_FEATURE_X
Console.WriteLine("Feature Xを使用します");
#endif
}
}

このコードでは、USE_FEATURE_Xというシンボルが定義されているかどうかによって、コードをコンパイル対象に含めるかどうかが決まります。

USE_FEATURE_X自体に値はありません。

つまり、#defineは次のような目的で使います。

C#
#if USE_FEATURE_X
// このコードをコンパイルする
#endif

「100という値を使いたい」「アプリ名を文字列として定義したい」といった用途には向いていません。

4-2. constは値を持つコンパイル時定数

constは、コンパイル時に値が確定する定数を定義するために使います。

C#
class AppSettings
{
public const int MaxRetryCount = 3;
public const string AppName = "SampleApp";
}

constで定義した値は、実行中に変更できません。

C#
Console.WriteLine(AppSettings.MaxRetryCount);
Console.WriteLine(AppSettings.AppName);

constは、数値や文字列などの固定値を定義したい場合に使います。

たとえば、次のような値はconstに向いています。

C#
public const double TaxRate = 0.1;
public const int DefaultPageSize = 20;
public const string DefaultLanguage = "ja-JP";

ただし、constはコンパイル時定数であるため、コンパイル時に値が決まっている必要があります。

実行時に計算した値や、コンストラクターで初期化したい値には使えません。

4-3. readonlyは実行時に初期化できる読み取り専用フィールド

readonlyは、初期化後に変更できないフィールドを定義するために使います。

C#
class UserService
{
private readonly string _connectionString;

public UserService(string connectionString)
{
_connectionString = connectionString;
}
}

readonlyフィールドは、宣言時またはコンストラクター内で値を設定できます。

C#
class Sample
{
private readonly DateTime _createdAt = DateTime.Now;

public DateTime CreatedAt => _createdAt;
}

constとは異なり、readonlyは実行時に決まる値を保持できます。

たとえば、設定ファイルから読み込んだ値、DIで渡された値、現在時刻などはreadonlyに向いています。

C#
class AppConfig
{
public readonly string EnvironmentName;

public AppConfig(string environmentName)
{
EnvironmentName = environmentName;
}
}

readonlyは「初期化後は変更したくないが、値は実行時に決まる」という場合に使います。

4-4. 値を扱うならconst・readonlyを使う

数値や文字列などの値を扱いたい場合は、#defineではなくconstまたはreadonlyを使います。

固定値がコンパイル時に決まるならconstが適しています。

C#
public const int MaxItemCount = 100;

実行時に値が決まるならreadonlyが適しています。

C#
public readonly string ApiEndpoint;

public SampleService(string apiEndpoint)
{
ApiEndpoint = apiEndpoint;
}

C#では、C言語のように#defineで値を定義するのではなく、値の性質に応じてconstreadonlyを使い分けます。

C#
// 値を扱う場合
const int MaxCount = 10;

// 実行時に決まる読み取り専用値
readonly string _baseUrl;

#defineは値を持たないため、値の管理には使わないようにしましょう。

4-5. コードの有効・無効を切り替えるなら#defineを使う

一方で、コード自体をコンパイル対象に含めるかどうかを切り替えたい場合は、#defineや条件付きコンパイルシンボルを使います。

C#
#define ENABLE_EXPERIMENTAL_FEATURE

using System;

class Program
{
static void Main()
{
#if ENABLE_EXPERIMENTAL_FEATURE
Console.WriteLine("実験的な機能を有効にします");
#endif
}
}

この場合、ENABLE_EXPERIMENTAL_FEATUREが定義されていないビルドでは、#if内のコードはそもそもコンパイルされません。

これは通常のif文とは異なります。

C#
if (isEnabled)
{
Console.WriteLine("実行時に判定します");
}

通常のif文は、コード自体はコンパイルされ、実行時に条件を判定します。

一方、#ifはコンパイル時にコードを含めるかどうかを判定します。

5. Visual Studioやプロジェクト設定でシンボルを定義する方法

C#の条件付きコンパイルシンボルは、ファイル内の#defineだけでなく、Visual Studioやプロジェクトファイルでも定義できます。

実務では、ファイルごとに#defineを書くよりも、プロジェクト設定やビルド構成で定義することが多いです。

5-1. プロジェクトのビルド設定で条件付きコンパイルシンボルを指定する

Visual Studioでは、プロジェクトのビルド設定から条件付きコンパイルシンボルを指定できます。

たとえば、CUSTOM_FEATUREというシンボルを定義すると、コード内で次のように判定できます。

C#
#if CUSTOM_FEATURE
Console.WriteLine("CUSTOM_FEATUREが有効です");
#endif

プロジェクト設定で定義したシンボルは、対象プロジェクト内のソースコードで利用できます。

ファイルごとに#defineを書く必要がないため、複数ファイルで共通の条件付きコンパイルを行いたい場合に便利です。

開発環境、本番環境、特定機能の有効化など、ビルド構成ごとに切り替えたい場合は、プロジェクト設定で管理するほうが分かりやすくなります。

5-2. Debug構成で定義されるDEBUGシンボル

C#プロジェクトでは、Debug構成でDEBUGシンボルが定義されることがあります。

そのため、デバッグ専用のコードを書くときに#if DEBUGがよく使われます。

C#
#if DEBUG
Console.WriteLine("デバッグモードで実行中です");
#endif

Debug構成ではこのコードがコンパイルされ、Release構成でDEBUGが定義されていなければコンパイル対象から除外されます。

デバッグ時だけ詳細情報を出力したい場合や、開発中の検証コードを一時的に有効にしたい場合に便利です。

ただし、DEBUGシンボルが定義されるかどうかはプロジェクト設定に依存します。

必要に応じて、ビルド設定やcsprojを確認しましょう。

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

TRACEシンボルは、トレース出力に関連して使われる条件付きコンパイルシンボルです。

たとえば、System.Diagnostics.Traceを使った出力は、トレース用途のログに利用されます。

C#
using System.Diagnostics;

class Program
{
static void Main()
{
Trace.WriteLine("トレース出力です");
}
}

また、#if TRACEを使って、TRACEシンボルが定義されている場合だけコードをコンパイルできます。

C#
#if TRACE
Console.WriteLine("TRACEシンボルが定義されています");
#endif

DEBUGは主にデバッグビルド向け、TRACEはトレース出力向けに使われることが多いです。

ただし、どの構成でどのシンボルが定義されるかは、プロジェクト設定によって変わる場合があります。

5-4. csprojでDefineConstantsを指定する方法

条件付きコンパイルシンボルは、csprojファイルのDefineConstantsで指定できます。

たとえば、次のように書きます。

XML
<PropertyGroup>
<DefineConstants>DEBUG;TRACE;CUSTOM_FEATURE</DefineConstants>
</PropertyGroup>

この設定では、DEBUGTRACECUSTOM_FEATUREの3つのシンボルが定義されます。

コード側では次のように使用できます。

C#
#if CUSTOM_FEATURE
Console.WriteLine("CUSTOM_FEATUREが有効です");
#endif

ビルド構成ごとに分けたい場合は、条件付きのPropertyGroupを使います。

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

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

このように設定すると、DebugビルドではLOCAL_ENVを、ReleaseビルドではPRODUCTION_ENVを定義できます。

複数ファイルで共通して使うシンボルは、csprojで管理すると保守しやすくなります。

5-5. ファイル内の#defineとプロジェクト設定の違い

ファイル内の#defineとプロジェクト設定のシンボル定義には、大きな違いがあります。

ファイル内で定義した#defineは、基本的にそのファイル内でのみ有効です。

C#
#define LOCAL_SYMBOL

class Sample
{
#if LOCAL_SYMBOL
void Method()
{
}
#endif
}

このLOCAL_SYMBOLは、そのソースファイル内で使うためのシンボルです。

一方、プロジェクト設定やcsprojで定義したシンボルは、対象プロジェクト全体で利用できます。

XML
<PropertyGroup>
<DefineConstants>GLOBAL_SYMBOL</DefineConstants>
</PropertyGroup>
C#
#if GLOBAL_SYMBOL
Console.WriteLine("プロジェクト全体で使用できます");
#endif

一時的な確認や、そのファイルだけで使う条件であれば#defineでも問題ありません。

しかし、複数ファイルで共通して使いたいシンボルは、プロジェクト設定で定義するほうが適しています。

6. C#の#defineを使う具体例

ここでは、C#の#defineや条件付きコンパイルシンボルを使う具体例を紹介します。

実務では、デバッグ用コードの切り替え、環境ごとの処理の分岐、機能フラグのような使い方が考えられます。

6-1. デバッグ時だけログを出力する

デバッグ時だけログを出力したい場合、#if DEBUGを使えます。

C#
using System;

class Program
{
static void Main()
{
int result = Calculate();

#if DEBUG
Console.WriteLine($"計算結果: {result}");
#endif

Console.WriteLine("処理が完了しました");
}

static int Calculate()
{
return 10 + 20;
}
}

Debug構成では、計算結果を表示するコードがコンパイルされます。

Release構成でDEBUGが定義されていなければ、そのコードはコンパイル対象から除外されます。

デバッグ用の一時的な出力を本番ビルドに含めたくない場合に便利です。

ただし、アプリケーションの正式なログ出力には、ロギングライブラリやILoggerなどを使うことも検討しましょう。

6-2. 開発環境と本番環境で処理を分ける

開発環境と本番環境で処理を分けたい場合にも、条件付きコンパイルを使えます。

C#
#define DEVELOPMENT

using System;

class Program
{
static void Main()
{
#if DEVELOPMENT
Console.WriteLine("開発環境用の処理を実行します");
#else
Console.WriteLine("本番環境用の処理を実行します");
#endif
}
}

この例では、DEVELOPMENTが定義されている場合は開発環境用の処理がコンパイルされます。

定義されていない場合は、本番環境用の処理がコンパイルされます。

ただし、実際の環境切り替えでは、appsettings.jsonや環境変数を使うほうが適していることも多いです。

条件付きコンパイルは「本番ビルドに含めたくないコード」を除外したい場合に有効です。

一方で、接続文字列やAPIのURLなど、実行時に切り替える設定は設定ファイルで管理するのが一般的です。

6-3. 特定の機能を一時的に有効化・無効化する

開発中の機能を一時的に有効化・無効化したい場合にも、#defineを使えます。

C#
#define ENABLE_BETA_FEATURE

using System;

class Program
{
static void Main()
{
Console.WriteLine("通常機能を実行します");

#if ENABLE_BETA_FEATURE
Console.WriteLine("ベータ機能を実行します");
#endif
}
}

ENABLE_BETA_FEATUREが定義されている場合、ベータ機能のコードがコンパイルされます。

定義を外すと、そのコードはコンパイル対象から除外されます。

C#
// #define ENABLE_BETA_FEATURE

ただし、機能の有効・無効を頻繁に切り替える場合は、設定ファイルやFeature Flagサービスを使うほうが向いています。

#defineによる切り替えは、ビルド時に確定するため、実行中に変更できない点に注意しましょう。

6-4. プラットフォームごとにコードを切り替える

プラットフォームごとに異なる処理を書きたい場合にも、条件付きコンパイルが使われます。

C#
using System;

class Program
{
static void Main()
{
#if WINDOWS
Console.WriteLine("Windows向けの処理です");
#elif LINUX
Console.WriteLine("Linux向けの処理です");
#elif MACOS
Console.WriteLine("macOS向けの処理です");
#else
Console.WriteLine("その他のプラットフォームです");
#endif
}
}

プラットフォームごとにAPIや処理が異なる場合、条件付きコンパイルでコードを切り替えることがあります。

ただし、プラットフォーム固有の処理が多くなると、1つのファイル内に条件分岐が増えて読みづらくなります。

その場合は、インターフェイスを使って実装を分けたり、プラットフォームごとにファイルを分けたりする設計も検討しましょう。

条件付きコンパイルは便利ですが、使いすぎるとコードの見通しが悪くなります。

6-5. テスト用コードをコンパイル対象に含める

テスト時だけ特定のコードをコンパイル対象に含めたい場合にも、条件付きコンパイルを利用できます。

C#
#define INCLUDE_TEST_CODE

using System;

class Program
{
static void Main()
{
Console.WriteLine("通常処理を実行します");

#if INCLUDE_TEST_CODE
RunTestCode();
#endif
}

#if INCLUDE_TEST_CODE
static void RunTestCode()
{
Console.WriteLine("テスト用コードを実行します");
}
#endif
}

この例では、INCLUDE_TEST_CODEが定義されている場合だけ、RunTestCodeメソッドがコンパイルされます。

定義されていない場合、メソッド自体がコンパイル対象から除外されます。

テスト用コードを本番ビルドに含めたくない場合に使えます。

ただし、単体テストや結合テストは、通常はテストプロジェクトとして分けて管理するのが一般的です。

#defineは一時的な検証や、特別なビルド条件でのみ必要なコードに限定して使うとよいでしょう。

7. C#の#defineでよくあるエラー・注意点

C#の#defineはシンプルですが、C言語の#defineと混同するとエラーや誤解につながります。

ここでは、よくある間違いと注意点を解説します。

7-1. #defineをコードの途中に書くとエラーになる

#defineはファイルの先頭に書く必要があります。

次のように、usingの後やクラス定義の中に書くことはできません。

C#
using System;

#define SAMPLE

class Program
{
static void Main()
{
}
}

また、次のようにメソッド内に書くこともできません。

C#
class Program
{
static void Main()
{
#define SAMPLE
}
}

#define#undefは、ソースファイル内の実際のコードより前に記述します。

C#
#define SAMPLE

using System;

class Program
{
static void Main()
{
#if SAMPLE
Console.WriteLine("SAMPLEが定義されています");
#endif
}
}

条件付きコンパイルを使う場合は、まずファイル先頭でシンボルを定義し、その後に#ifで判定しましょう。

7-2. #defineに値を設定しようとしてしまう

C#の#defineで値を設定することはできません。

次のような書き方は間違いです。

C#
#define MAX_SIZE 100
#define ENV "Development"

C#の#defineは、値を持たないシンボルを定義するだけです。

正しくは、次のようにシンボル名だけを書きます。

C#
#define DEVELOPMENT
#define ENABLE_LOG

値を定義したい場合は、constreadonlyを使います。

C#
const int MaxSize = 100;
const string EnvironmentName = "Development";

C#で固定値を管理したい場合に#defineを使おうとすると、設計としても不自然になります。

値はconstreadonly、設定ファイル、環境変数などで管理しましょう。

7-3. ファイル単位でしか有効にならないケースに注意する

ソースファイル内に書いた#defineは、そのファイル内でのみ有効です。

たとえば、FileA.csに次のように書いたとします。

C#
#define FEATURE_A

class FileA
{
#if FEATURE_A
void MethodA()
{
}
#endif
}

このFEATURE_Aは、別のFileB.csで自動的に使えるわけではありません。

C#
class FileB
{
#if FEATURE_A
void MethodB()
{
}
#endif
}

FileB.csでもFEATURE_Aを使いたい場合は、プロジェクト設定やcsprojで条件付きコンパイルシンボルとして定義するのが一般的です。

XML
<PropertyGroup>
<DefineConstants>FEATURE_A</DefineConstants>
</PropertyGroup>

複数ファイルで共通して使う条件は、ファイル内の#defineではなくプロジェクト全体の設定で管理しましょう。

7-4. 条件付きコンパイルを多用すると可読性が下がる

#if#defineは便利ですが、多用するとコードが読みにくくなります。

C#
#if FEATURE_A
#if DEBUG
Console.WriteLine("Feature A Debug");
#else
Console.WriteLine("Feature A Release");
#endif
#else
#if FEATURE_B
Console.WriteLine("Feature B");
#endif
#endif

このように条件が入れ子になると、どのコードがどのビルドで有効になるのか分かりにくくなります。

条件付きコンパイルが増えすぎる場合は、設計を見直したほうがよいことがあります。

たとえば、次のような方法を検討できます。

C#
interface IFeatureService
{
void Execute();
}

環境や機能ごとに実装クラスを分ければ、#ifを減らせる場合があります。

条件付きコンパイルは、必要な箇所に限定して使うことが大切です。

7-5. 未使用コードが増えて保守しにくくなる

#ifで囲んだコードは、条件によっては長期間コンパイルされないことがあります。

C#
#if OLD_FEATURE
OldMethod();
#endif

OLD_FEATUREが定義されない状態が続くと、このコードはコンパイルされません。

その結果、周辺コードの変更に気づかず、いざ有効化したときにエラーになる可能性があります。

条件付きコンパイルで無効化したコードは、見た目上は残っていても、実際には使われていないコードになりがちです。

不要になったコードは、コメントアウトや#ifで残し続けるのではなく、削除してバージョン管理システムで履歴を追うほうがよい場合もあります。

#define#ifは、一時的な切り替えには便利ですが、長期的な保守性には注意が必要です。

8. C#の#defineを使うべきケース・使わないほうがよいケース

C#の#defineは、使いどころを間違えなければ便利です。

一方で、固定値や実行時設定の管理に使うと、分かりにくいコードになってしまいます。

ここでは、#defineを使うべきケースと、使わないほうがよいケースを整理します。

8-1. 使うべきケース:ビルド構成ごとに処理を切り替えたい場合

#defineや条件付きコンパイルシンボルは、ビルド構成ごとに処理を切り替えたい場合に向いています。

C#
#if DEBUG
Console.WriteLine("Debugビルドです");
#else
Console.WriteLine("Debug以外のビルドです");
#endif

Debugビルド、Releaseビルド、検証用ビルドなどで、コンパイルするコードを変えたい場合に便利です。

たとえば、開発中だけ有効にしたい検証コードや、特定ビルドでのみ必要な処理を分けられます。

C#
#if INTERNAL_BUILD
Console.WriteLine("社内向けビルドの処理です");
#endif

ビルド時にコードを完全に含めるか除外するかを決めたい場合は、条件付きコンパイルが適しています。

8-2. 使うべきケース:デバッグ専用コードを本番ビルドから除外したい場合

デバッグ専用コードを本番ビルドに含めたくない場合、#if DEBUGは有効です。

C#
#if DEBUG
Console.WriteLine("デバッグ情報を出力します");
Console.WriteLine($"変数xの値: {x}");
#endif

ReleaseビルドでDEBUGが定義されていなければ、このコードはコンパイルされません。

つまり、実行時に条件が false になるのではなく、コード自体がビルド結果に含まれません。

本番環境に不要な検証コードや、一時的なデバッグ出力を除外したい場合に便利です。

ただし、ログ出力全般を#if DEBUGだけで管理するのはおすすめできません。

本番環境でも必要なログは、ログレベルや設定で制御するほうが適しています。

8-3. 使わないほうがよいケース:単なる固定値を定義したい場合

単なる固定値を定義したい場合、#defineは使いません。

次のような目的には不向きです。

C#
#define MAX_COUNT 100

C#ではこのような書き方はできないため、固定値はconstを使います。

C#
public const int MaxCount = 100;

また、実行時に決まる値であればreadonlyを使います。

C#
public readonly string BaseUrl;

public ApiClient(string baseUrl)
{
BaseUrl = baseUrl;
}

C#の#defineは、値を定義するためのものではありません。

固定値を扱うならconst、実行時に初期化する読み取り専用値ならreadonlyを選びましょう。

8-4. 使わないほうがよいケース:実行時に切り替えたい設定を扱う場合

実行時に切り替えたい設定を#defineで管理するのは適していません。

#define#ifによる条件付きコンパイルは、ビルド時に決まります。

そのため、アプリケーションを起動した後に設定を変更して処理を切り替えることはできません。

たとえば、次のようなものは#defineではなく設定ファイルや環境変数で管理するのが一般的です。

C#
// appsettings.jsonや環境変数で管理するほうがよい例
// - 接続文字列
// - APIエンドポイント
// - ログレベル
// - 外部サービスの有効・無効
// - 環境名

実行時に設定を変更したい場合は、appsettings.json、環境変数、コマンドライン引数、データベース設定などを使いましょう。

#defineは、あくまでビルド時にコードを切り替えるための仕組みです。

8-5. appsettingsや環境変数との使い分け

#defineappsettings.json、環境変数は、それぞれ役割が異なります。

#defineは、コンパイル時にコードを含めるかどうかを決めるために使います。

C#
#if DEBUG
Console.WriteLine("デバッグ専用コード");
#endif

一方、appsettings.jsonや環境変数は、実行時の設定を切り替えるために使います。

JSON
{
"FeatureSettings": {
"EnableNewFeature": true
}
}

アプリケーションの起動後に設定値として参照したいものは、#defineではなく設定ファイルや環境変数に置くほうが自然です。

判断基準は次のように考えると分かりやすいです。

コンパイル対象から完全に除外したいコードには、#defineや条件付きコンパイルシンボルを使います。

実行時に値を変更したい設定には、appsettings.jsonや環境変数を使います。

値そのものをコード内で定義したい場合は、constreadonlyを使います。

9. C#の#defineに関するよくある質問

最後に、C#の#defineについてよくある質問をまとめます。

9-1. C#の#defineで文字列や数値は定義できる?

C#の#defineでは、文字列や数値を定義できません。

次のような書き方はできません。

C#
#define APP_NAME "SampleApp"
#define MAX_COUNT 100

C#の#defineで定義できるのは、値を持たないシンボルだけです。

C#
#define ENABLE_LOG

文字列や数値を定義したい場合は、constを使います。

C#
public const string AppName = "SampleApp";
public const int MaxCount = 100;

実行時に値を決めたい場合は、readonlyや設定ファイルを使いましょう。

9-2. #defineで定義したシンボルは別ファイルでも使える?

ソースファイル内に書いた#defineは、基本的にそのファイル内でのみ有効です。

たとえば、A.csに次のように書いても、別のB.csで自動的に有効になるわけではありません。

C#
#define SAMPLE

複数ファイルで同じシンボルを使いたい場合は、プロジェクト設定やcsprojで条件付きコンパイルシンボルを定義します。

XML
<PropertyGroup>
<DefineConstants>SAMPLE</DefineConstants>
</PropertyGroup>

このように定義すれば、対象プロジェクト内の複数ファイルで#if SAMPLEを使えます。

C#
#if SAMPLE
Console.WriteLine("SAMPLEが定義されています");
#endif

そのファイルだけで使う一時的なシンボルなら#define、プロジェクト全体で使うシンボルならプロジェクト設定を使うとよいでしょう。

9-3. #defineと#define DEBUGは同じ?

#defineはシンボルを定義するためのディレクティブです。

#define DEBUGは、DEBUGという名前のシンボルを定義する書き方です。

C#
#define DEBUG

これにより、そのファイル内では#if DEBUGの条件が成立します。

C#
#if DEBUG
Console.WriteLine("DEBUGが定義されています");
#endif

ただし、実務ではDEBUGシンボルはファイル内で手動定義するよりも、プロジェクトのDebug構成で定義されることが多いです。

ファイルごとに#define DEBUGを書くと、ビルド構成との関係が分かりにくくなることがあります。

通常は、DEBUGのような共通シンボルはプロジェクト設定で管理するほうがよいでしょう。

9-4. #ifとif文の違いは?

#ifと通常のif文は、判定されるタイミングが違います。

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

C#
#if DEBUG
Console.WriteLine("デバッグ用コード");
#endif

DEBUGが定義されていない場合、このコードはコンパイル対象から除外されます。

一方、通常のif文は実行時に判定されます。

C#
if (isDebugMode)
{
Console.WriteLine("デバッグ用コード");
}

この場合、コード自体はコンパイルされます。

そして、アプリケーションの実行中にisDebugModeの値を見て、処理を実行するかどうかを決めます。

つまり、コンパイル時にコードを含めるか除外するなら#if、実行時に条件分岐するならif文を使います。

9-5. #defineは本番コードで使ってもよい?

#defineや条件付きコンパイルは、本番コードで使っても問題ありません。

ただし、使いすぎには注意が必要です。

本番ビルドからデバッグ専用コードを除外したい場合や、特定のビルド構成でのみコードを有効にしたい場合には有効です。

C#
#if DEBUG
Console.WriteLine("デバッグ専用の出力です");
#endif

一方で、条件付きコンパイルが多すぎると、どのビルドでどのコードが有効になるのか分かりにくくなります。

また、長期間コンパイルされないコードは、気づかないうちに壊れている可能性があります。

本番コードで使う場合は、目的を明確にし、必要最小限にすることが大切です。

設定値の切り替えや機能のオン・オフを実行時に行いたい場合は、appsettings.jsonや環境変数、Feature Flagなどを使うほうが適しています。

まとめ

C#の#defineは、条件付きコンパイルで使うシンボルを定義するためのプリプロセッサディレクティブです。

C言語の#defineのように、数値や文字列の定数を定義したり、マクロを作ったりするものではありません。

C#の#defineは、主に#if#else#elif#endifと組み合わせて使います。

C#
#define ENABLE_LOG

#if ENABLE_LOG
Console.WriteLine("ログを出力します");
#endif

#defineで定義したシンボルがある場合だけ、#if内のコードがコンパイル対象になります。

値を扱いたい場合は、#defineではなくconstreadonlyを使います。

C#
public const int MaxCount = 100;
public readonly string BaseUrl;

また、複数ファイルで共通して使うシンボルは、ファイル内の#defineではなく、Visual Studioのビルド設定やcsprojDefineConstantsで定義するのが一般的です。

XML
<PropertyGroup>
<DefineConstants>DEBUG;TRACE;CUSTOM_FEATURE</DefineConstants>
</PropertyGroup>

C#の#defineは、ビルド構成ごとに処理を切り替えたい場合や、デバッグ専用コードを本番ビルドから除外したい場合に便利です。

一方で、固定値の定義や実行時に変更したい設定の管理には向いていません。

コンパイル時にコードを切り替えるなら#define、値を定義するならconstreadonly、実行時の設定を切り替えるならappsettings.jsonや環境変数を使う、と役割を分けて考えるとよいでしょう。