概要
定数を宣言する際に使用する、C#のconstとstatic readonlyの違いについてまとめてみました。
目次
const
「コンパイル時定数」ともいわれ、ビルド時に値が埋め込まれます (インライン展開)。また、暗黙的にstaticとなります。
C# 1.0 (.NET Framework 1.0) から存在していますが、const自体はもっと古くCやC++の時代からの伝統的なキーワードです。
内部的にstaticとなるため、static const
という記述はエラーになります。
インライン展開とは
コンパイル時に値がコードへ直接埋め込まれることです。以下にイメージを記載します。
例:メソッドのインライン展開のイメージ
using System;
using System.Runtime.CompilerServices;
public static class MathEx
{
public static int Add(int x, int y)
{
return x + y;
}
}
public class Program
{
public static void Main()
{
int a = 10;
int b = 20;
int s = MathEx.Add(a, b); // ← メソッド呼び出しで a + b を実施
Console.WriteLine(s);
}
}
上記がコンパイル後、下記のようなイメージになります。
// 実際のコンパイラ出力ではありません。説明用のイメージです。
public class Program
{
public static void Main()
{
int a = 10;
int b = 20;
int s = a + b; // ← 呼び出しが消え、メソッド本体がここに展開されたイメージ
Console.WriteLine(s);
}
}
このようにインライン展開を行うと、関数の呼び出し (関数がおかれているメモリに移動して関数を実行する) 過程がなくなるため、プログラムの高速化に繋がります。
上記の例では関数呼び出しが一回だけですが、ループ内で数千回呼ばれる場合などはインライン展開しておくと、関数呼び出しのオーバーヘッド (本質的な処理以外の追加コスト) を削減できます。
上記を踏まえて、constも以下のようにインライン展開されています。
例:constのインライン展開のイメージ
public class Config
{
public const int BufferSize = 1024;
}
public class App
{
public void Run()
{
int size = Config.BufferSize;
Console.WriteLine(size);
}
}
public void Run()
{
int size = 1024; // ← Config.BufferSize が「値」に置き換わっている
Console.WriteLine(size);
}
constで使用できる型
constで使用できる型は限られています。
- OK:数値、bool、char、string、enum、decimal (特例)
- NG:DateTime、Guid、List、配列など (newやメソッド呼び出しが必要なもの)
constの初期化子は「コンパイル時に完全評価できる定数式」でなければならないためです。つまり、コンストラクタ呼び出し (new) やメソッド呼び出し・プロパティ参照を伴うものはconstでは使用不可になります。DateTimeなどは構造体でも作成にコンストラクタが必要なのでconstにできません。
static readonly
static readonlyは実行時に一度だけ値を決定します。値は埋め込まれず、参照される形になります。また、staticが付いた静的コンストラクタ内で動的に値を決定できます。
C# 1.0 (.NET Framework 1.0) から存在していますがconstと違い、C#固有の設計思想 (.NETの型安全性や実行時初期化のため) のためにC#から追加されたものです。
readonlyとは
readonlyとstatic readonlyはどちらも「再代入できないフィールド」を作るための修飾子ですが、どこに属するか (インスタンス or 型全体) が違います。
まずreadonlyはインスタンスごとに別々の値を持てますが、static readonlyはそのクラスで一つの値のみを持てます。
代入する方法も、readonlyでは宣言時かインスタンスのコンストラクタ内、static readonlyは宣言時か、静的コンストラクタ内で値を代入できます。
インスタンスごとに異なる値はreadonly、どのインスタンスでも共通な値はstatic readonlyを使用すると良さそうです。
public class Point
{
// インスタンスごとに不変にしたいなら readonly
public readonly int X;
public readonly int Y;
// 共有の不変インスタンスは static readonly
public static readonly Z;
public Point(int x, int y)
{
X = x; // XとYはreadonlyなのでコンストラクタ内で代入可 (以後は不可)
Y = y;
}
static Point() // 静的CLRが自動的に呼び出すため、アクセス修飾子は付けられない (引数も付けられない)
{
Z = 1; // Zはstatic readonlyなので静的コンストラクタ内で代入可 (以後は不可)
}
}
使い分け
値がコンパイル時に決まる場合はconst、実行時に値が決まる場合は、static readonlyがよいです。そのほかにも外部公開で将来変わる可能性がある場合もstatic readonlyがよさそうです。
ただし、constを使用する際は以下の注意点があります。
注意点
constは値が埋め込まれるため、ライブラリ更新時に再ビルドが必要です。static readonlyはフィールド参照なので、ライブラリ差し替えで即反映されます。
// ライブラリ側(Assembly A)
public static class Api
{
public const int VersionConst = 1;
public static readonly int VersionRo = 1;
}
// アプリ側(Assembly B)
public class App
{
public void Run()
{
// const は「値そのもの」がビルド時に埋め込まれる
// そのため、ライブラリ(Assembly A)のVersionConstを 2 に上げても、アプリ(Assembly B)を再ビルドしない限り 1 のままになる
int v1 = Api.VersionConst;
// static readonly はフィールド参照
// そのため、ライブラリ更新で 2 になれば、アプリ再ビルドなしでも 2 を見る
int v2 = Api.VersionRo;
}
}
以下に、パターン別にどれを使用すればよいか記載してみました。
1) 固定リテラル (将来も絶対に変わらない値) → const
public static class Config
{
public const int BufferSize = 1024;
public const string AppName = "SampleApp";
public const decimal TaxRate = 0.10m; // decimalもconst可
}
2) 属性引数 / switch の case ラベル → const
switchのcaseではstatic readonlyは使用できません。caseはコンパイル時定数が必要なため、実行時に値が決まるstatic readonlyは使えないです。
ただif文の条件式は実行時に評価される仕組みなので、static readonlyでも使用できます。
swich文はコンパイル時にジャンプテーブルと呼ばれる分岐先の一覧を作成します。このためswich文は、実行時では値をみて即ジャンプできるので高速になります。
例えば10個の条件があれば、if-else
は条件を順番に評価するので最高10回も比較が発生します。
switchのジャンプテーブルを使用すると、条件の数にとらわれず1回の計算で目的の分岐に飛べます。
using System;
public static class Messages
{
public const string Title = "Welcome";
}
public static class Demo
{
public const int One = 1;
public static string Label(int n)
{
switch (n)
{
case One: // ← caseはコンパイル時定数が必要
return "one";
default:
return "other";
}
}
}
3) 公開APIで“将来変わるかもしれない値” → static readonly(バージョン不整合を避ける)
注意点で記載した内容ですが、public constで定義すると、呼び出し側に値が埋め込まれるため、public constの値が更新されてそのDLLだけ差し替えても反映されません。
そこで値を埋め込ませないstatic readonlyを使います。
// constは呼び出し側に値が埋め込まれる
public static class ApiV1
{
public const int Version = 1;
}
// readonlyはフィールド参照になる (呼び出し側に値は埋め込まれない)
public static class ApiV2
{
public static readonly int Version = 1;
}
4) 実行時に決まる値(時刻・GUID など) → static readonly (readonly)
DateTime.UtcNowやGuid.NewGuid()はコンパイル時に決まらないので、static readonlyを使用します (constは使用できません) 。また、インスタンスごとに値が違うならstatic readonlyではなくreadonlyが良いです。
using System;
public static class RuntimeValues
{
public static readonly DateTime BuildAt = DateTime.UtcNow;
public static readonly Guid AppId = Guid.NewGuid();
}
5) パース結果や環境変数、計算で決まる設定 → static readonly
実行環境に依存する値もstatic readonlyを使用します (constは使用できません) 。
using System;
public static class Settings
{
public static readonly int Port =
int.Parse(Environment.GetEnvironmentVariable("APP_PORT") ?? "8080");
public static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30);
}
6) Guid / Version / Regex など“new が必要”な型 → static readonly
new (コンストラクタ呼び出し) がある時点でconstにはできません。
using System;
using System.Text.RegularExpressions;
public static class Types
{
public static readonly Version ApiVersion = new Version(1, 2, 0);
public static readonly Regex IdPattern =
new Regex(@"^[A-Z]{2}\d{4}$", RegexOptions.Compiled);
}
7) 配列・コレクション → static readonly (readonly)
static readonly は参照の再代入を禁止するだけで、中身の変更は可能です(ミュータブル)。これも、インスタンスごとに値が違うならstatic readonlyではなくreadonlyが良いです。
using System.Collections.Generic;
public static class Collections
{
public static readonly int[] Sizes = new[] { 16, 32, 64 };
public static readonly List<string> Names = new() { "A", "B" };
// 参照の再代入は不可だが、中身の変更は可能 (必要なら不変コレクションを使用)
}
参考
ミュータブル (mutable) とは、「オブジェクトの状態 (中身) を後から変更できる」という意味です。逆にイミュータブル (immutable) は「一度作ったら中身を変更できない」オブジェクトです。
ミュータブルな型は以下のようなものがあります。
- 配列
- List
- Dictionary
- StringBuilder
- DataTable
- DataSet
イミュータブルな型は以下のようなものがあります。
- int
- double
- decimal
- string
- bool
- DateTime
- Guid
public static class CollectionsDemo
{
public static readonly int[] Sizes = new[] { 16, 32, 64 };
public static readonly List<string> Names = new() { "A", "B" };
public static void MutateContents()
{
// 中身の変更は可能 (配列の要素を書き換え / List に追加)
Sizes[0] = 99;
Names.Add("C");
}
public static void TryReassign()
{
// フィールド自体の再代入は不可 (コンパイルエラー)
// Sizes = new[] { 1, 2, 3 };
// Names = new List<string>();
}
}
readonlyは「参照を変えない」だけを保証しているため、オブジェクトの中身がミュータブルなら変更可能です。完全に不変にしたい場合はイミュータブル型 (ImmutableArray<T>
など) を使用します。
ちなみにconstはコンパイル時に完全に決まる値しか指定できないため、そもそもミュータブルな型を指定できません。なので「中身を変えられる」ことは発生しません。
8) 他の定数から組み立てる“本当に固定の式” → const
「+」や「*」など定数式だけで組み立てられるならconstが使用できます。ただメソッド呼び出し(Math.Abs
など)が入ると使用できなくなります。
public static class MathConst
{
public const int BaseValue = 10;
public const int DoubleBase = BaseValue * 2; // 定数式ならOK
}
9) 文字列の固定キーやメッセージ → const
固定文字列はconstが最適です。文字列補間も、式がすべて定数なら大丈夫です。ただ公開APIの “ブランド名” のように変わる可能性がある文字列はstatic readonlyが安全です。
public static class Keys
{
public const string CacheKeyUser = "cache:user";
public const string LogTag = "APP";
}
10) 同一プロセス内で“起動時に一度だけ決める”値 → static readonly + 静的コンストラクタ
静的コンストラクタを使うと、初期化処理を一度だけ実行できます。
public static class PathConfig
{
public static readonly string BaseDir;
static PathConfig()
{
BaseDir = Environment.CurrentDirectory; // 起動環境に依存して一度だけ確定
}
}
補足
上記のようにケースによってどちらを使用するかという判断はありますが、実務的には「迷ったらstatic readonly」が安全だと感じます。ただし、swich文のcaseのようなコンパイル時定数が必要な場面ではconst一択になります。
constは「この値は絶対に変わらない」という強い意味を示せますが、万が一想定外でconstにしていたところの値が変わってしまった場合、その値を参照していたモジュールも再ビルドが必要になってしまいます。
違いの一覧表 (参考まで)
修飾子 | 再代入 | 中身変更 | 初期化タイミング | 型の制約 |
---|---|---|---|---|
const | 不可 | 不可 | コンパイル時 | 定数式のみ |
readonly | 不可 | 型次第 | 宣言時 or コンストラクタ | 制約なし |
static readonly | 不可 | 型次第 | 宣言時 or 静的コンストラクタ | 制約なし |
終わりに
const と static readonly は似ているようで役割が違います。仕組みを理解して正しく使い分けることで、コードの安全性と保守性が少し高まるかと思いました。