はじめに
C#のコードを読んでいたら、同じような定数定義で2つの書き方を見かけました。
public const string ServiceKey = "Sample";
public static readonly string ServiceKey = "Sample";
「どっちも定数っぽいけど、何が違うの?」と思って調べたので、学んだことをまとめます。
環境
- .NET 10.0
- C# 13.0
- Visual Studio 2026
そもそも何のためにあるのか
まず、なぜこの2つの書き方が存在するのかを理解するために、それぞれの目的を整理しました。
const(定数)
constはコンパイル時定数を定義するためのキーワードです。値がコンパイル時に確定し、変更されることがない完全な定数を表現します。
public const int MaxRetryCount = 3;
public const string ApiVersion = "v1";
static readonly(静的読み取り専用フィールド)
static readonlyは実行時に初期化される読み取り専用フィールドです。初期化後は変更できませんが、初期化のタイミングに柔軟性があります。
public static readonly DateTime StartTime = DateTime.Now;
public static readonly Guid ApplicationId = Guid.NewGuid();
この時点で「あ、初期化のタイミングが違うんだ」ということに気づきました。
調べてわかった5つの違い
Microsoftのドキュメントやいくつかの技術記事を読んで、主な違いを整理しました。
1. 初期化のタイミング
これが一番重要な違いでした。
const(コンパイル時)
public const string ServiceKey = "Sample";
コンパイル時に値が確定します。コンパイラが値を直接呼び出し側のコードに埋め込む(インライン化)ため、実行時にフィールドとしてアクセスされることはありません。
static readonly(実行時)
public static readonly string ServiceKey = "Sample";
実行時に値が初期化されます。静的コンストラクタでも設定できます。
public class Config
{
public static readonly string ServiceKey;
static Config()
{
// 実行時に環境変数から取得するなども可能
ServiceKey = Environment.GetEnvironmentVariable("SERVICE_KEY") ?? "Default";
}
}
2. 使える型の制約
const(制限あり)
コンパイル時定数なので、使える型が限られています。
使える型は以下のみです。
- プリミティブ型(
int、long、doubleなど) stringenum-
null(参照型の場合)
// ✅ OK
public const int Count = 10;
public const string Name = "Sample";
public const double Pi = 3.14159;
// ❌ コンパイルエラー
public const DateTime Now = DateTime.Now;
public const Guid Id = Guid.NewGuid();
static readonly(制限なし)
任意の型が使えます。
// ✅ すべてOK
public static readonly DateTime StartTime = DateTime.Now;
public static readonly Guid ApplicationId = Guid.NewGuid();
public static readonly ImmutableList<string> AllowedValues = ImmutableList.Create("A", "B", "C");
3. メモリ上の扱いとパフォーマンス
const(インライン化)
コンパイル時に値が参照元のコードに直接埋め込まれます。
// 定義側のライブラリ
public class Constants
{
public const int MaxSize = 100;
}
// 使用側のコード
var size = Constants.MaxSize;
// ↓ コンパイル後は実質これと同じ(インライン化される)
var size = 100;
メモリアクセスが不要なため、わずかに高速です。ただし、定義側のライブラリで値を変更した場合、使用側も再コンパイルしないと変更が反映されません。
static readonly(参照)
実行時にメモリ上の値を参照します。
// 定義側のライブラリ
public class Settings
{
public static readonly int MaxSize = 100;
}
// 使用側のコード
var size = Settings.MaxSize;
// ↓ 実行時にSettingsクラスのMaxSizeフィールドを参照
参照のオーバーヘッドがありますが、定義側のライブラリを更新すれば使用側の再コンパイルなしで変更が反映されます。
実際のパフォーマンス差
理論上はconstの方が高速ですが、実際にはJITコンパイラがstatic readonlyのプリミティブ値を最適化するため、パフォーマンスの差はほとんど無視できるレベルです。
4. バージョニングへの影響
これはライブラリやNuGetパッケージを公開する際に重要な違いです。
constの落とし穴
例えば、NuGetパッケージで定数を公開していたとします。
// バージョン 1.0.0
public class ApiConfig
{
public const string BaseUrl = "https://api.example.com/v1";
}
これを使うアプリケーション側では以下のようになります。
var url = ApiConfig.BaseUrl;
// → コンパイル時に "https://api.example.com/v1" が埋め込まれる
その後、パッケージ側でURLを変更したとします。
// バージョン 1.1.0
public class ApiConfig
{
public const string BaseUrl = "https://api.example.com/v2"; // v1 → v2 に変更
}
問題: アプリケーション側はパッケージを1.1.0に更新しただけでは変更が反映されません。アプリケーション自体も再コンパイルが必要です。
static readonlyなら
public class ApiConfig
{
public static readonly string BaseUrl = "https://api.example.com/v2";
}
パッケージを更新すれば、アプリケーションの再コンパイルなしで変更が反映されます。
5. ローカル変数としての使用
const(ローカル変数でも使える)
public void Method()
{
const int MaxAttempts = 3; // ✅ OK
// ...
}
static readonly(クラスレベルのみ)
public void Method()
{
static readonly int MaxAttempts = 3; // ❌ コンパイルエラー
// static readonlyはクラスやstructのメンバーとしてのみ定義可能
}
ローカルスコープで定数が必要な場合はconstを使います。
違いを表で整理
調べた内容を表にまとめました。
| 観点 | const | static readonly |
|---|---|---|
| 初期化タイミング | コンパイル時 | 実行時(静的コンストラクタでも可) |
| 使える型 | プリミティブ型、string、enum、null | 任意の型 |
| メモリの扱い | 値が直接埋め込まれる(インライン化) | 実行時に参照される |
| パフォーマンス | JIT最適化によりほぼ差なし | JIT最適化によりほぼ差なし |
| バージョン管理 | 参照側の再コンパイル必要 | ライブラリ更新のみで反映 |
| スコープ | ローカル変数でも使える | クラス/structメンバーのみ |
| 値の変更 | 絶対に変更不可 | 初期化後は変更不可 |
どう使い分けるか
調べた内容から、個人的に以下の基準を設けました。
constを使う場合
以下のすべてを満たす場合にconstを選びます。
- 値が絶対に変わらない(数学定数、固定文字列など)
- プリミティブ型かstringである
- ローカルスコープで定数が必要
// ✅ constが適切な例
public const double Pi = 3.14159265359;
public const int DaysInWeek = 7;
public const string CompanyName = "Contoso";
public void Calculate()
{
const int BufferSize = 1024; // ローカル定数
// ...
}
static readonlyを使う場合
以下のいずれかに該当する場合はstatic readonlyを選びます。
- 値が実行時に決まる(
DateTime.Now、Guid.NewGuid()など) - プリミティブ型以外の型を使う
- ライブラリやパッケージとして公開する(バージョン管理の柔軟性が必要)
- 静的コンストラクタで初期化したい
// ✅ static readonlyが適切な例
public static readonly DateTime ApplicationStartTime = DateTime.Now;
public static readonly Guid SessionId = Guid.NewGuid();
public static readonly ImmutableArray<string> SupportedLanguages = ["ja", "en", "zh"];
// 環境変数から取得する例
public static readonly string DatabaseConnectionString;
static MyClass()
{
DatabaseConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION")
?? throw new InvalidOperationException("DB_CONNECTION not set");
}
迷ったときの判断フロー
以下のような判断フローを作ってみました。
値は実行時に決まる?
└─ YES → static readonly
値の型はプリミティブ型 or string?
└─ NO → static readonly
└─ YES
└─ ライブラリとして公開する?
└─ YES → static readonly(バージョン管理の柔軟性)
└─ NO → const
実際のコード例
NG例: constを使うべきでないケース
// ❌ コンパイルエラー: DateTime.Nowは実行時の値
public const DateTime CacheExpiration = DateTime.Now.AddHours(1);
// ❌ コンパイルエラー: Guidは定数にできない
public const Guid RequestId = Guid.NewGuid();
// ⚠️ 動くけど問題あり: ライブラリとして公開する場合
// → 値を変更してもパッケージ利用側が再コンパイルしないと反映されない
public const string ApiEndpoint = "https://api.example.com";
OK例: 適切な使い分け
public class ApplicationSettings
{
// ✅ 数学定数 → const
public const double GoldenRatio = 1.618033988749;
// ✅ 固定文字列 → const
public const string DefaultCulture = "ja-JP";
// ✅ 実行時の値 → static readonly
public static readonly DateTime DeploymentTime = DateTime.UtcNow;
// ✅ 複雑な型 → static readonly
public static readonly ImmutableDictionary<string, string> ErrorMessages;
// ✅ 環境依存の設定 → static readonly
public static readonly string LogDirectory;
static ApplicationSettings()
{
ErrorMessages = ImmutableDictionary.CreateRange(new[]
{
KeyValuePair.Create("E001", "入力エラー"),
KeyValuePair.Create("E002", "接続エラー"),
});
LogDirectory = Environment.GetEnvironmentVariable("LOG_DIR") ?? "/var/log/app";
}
}
まとめ
- constはコンパイル時定数で、値が絶対に変わらない場合に使う
- static readonlyは実行時に初期化される読み取り専用フィールドで、柔軟性が必要な場合に使う
-
主な判断基準
- 実行時に値が決まる →
static readonly - プリミティブ型以外 →
static readonly - ライブラリとして公開 →
static readonly(バージョン管理の柔軟性) - 完全な定数でローカルスコープ →
const
- 実行時に値が決まる →
- constの落とし穴: 値を変更しても参照側が再コンパイルしないと反映されない
調べてみると、単なる「書き方の違い」ではなく、用途によって明確に使い分けるべきものだということがわかりました。特にライブラリやパッケージを作る際は、バージョン管理への影響を考慮して選択する必要がありそうです。
参考になったら いいね や ストック をお願いします!
同じような経験をされた方のコメントもお待ちしています。
この記事のコメントの内容も非常に参考になりました。
ぜひ、一読ください。
>> staticフィールドの落とし穴
参考
- const (C# リファレンス) - Microsoft Learn
- readonly (C# リファレンス) - Microsoft Learn
- Static Constructors (C# Programming Guide) - Microsoft Learn
- Constants (C# Programming Guide) - Microsoft Learn
関連リンク
技術ブログでも学びや検証内容をまとめています。
アウトプットで手当がもらえる会社 ONE WEDGE
株式会社ONE WEDGE では一緒に働く仲間を募集中!
技術記事を書くと手当がもらえる「IT系記事寄稿特別手当」という制度があります。
興味があればぜひカジュアルに話しましょう!
👉 採用サイト