C#でプログラムを開発していると、値を変更したくない変数や、定数として扱いたい値が出てくることがよくあります。
その際に利用するのがconstキーワードとreadonly修飾子です。
一見するとどちらも「値を変更できない」という同じ役割を持っているように見えますが、実は値が決まるタイミングやメモリへの展開方法、使用できるデータの型など、その性質は大きく異なります。
これらの違いを正しく理解せずに使い分けてしまうと、思わぬバグの原因になったり、将来的なライブラリのアップデートで不整合が発生したりするリスクがあります。
本記事では、C#におけるconstとreadonlyの違いを初心者から中級者の方まで分かりやすく徹底的に解説します。

constキーワードの基本と特徴
constは「コンパイル時定数」と呼ばれます。
その名前の通り、プログラムをコンパイル(ビルド)する時点で値が確定している必要がある定数です。
constの定義と基本的な使い方
constを使用する場合、変数の宣言と同時に必ず初期値を代入しなければなりません。
また、一度定義した値を後から変更することは一切できません。
using System;
namespace ConstExample
{
class Program
{
// クラスレベルでのconst定義
public const string AppName = "MyCoolApp";
public const int MaxUserCount = 100;
static void Main(string[] args)
{
// ローカル変数としてのconst定義
const double Pi = 3.14159;
Console.WriteLine($"AppName: {AppName}");
Console.WriteLine($"MaxUserCount: {MaxUserCount}");
Console.WriteLine($"Pi: {Pi}");
// 以下のコードはコンパイルエラーになります
// Pi = 3.14;
}
}
}
AppName: MyCoolApp
MaxUserCount: 100
Pi: 3.14159
constの内部的な仕組み
constで定義された値は、コンパイルの過程でその値自体が利用箇所に直接埋め込まれます。
これを「インライン化」と呼びます。
例えば、上記のMaxUserCountを参照しているコードは、コンパイル後のIL(中間言語)コードでは単なる「100」という数値に置き換わります。
この仕組みにより、constは実行時のパフォーマンスが非常に高いというメリットがあります。
変数を参照するためのメモリアクセスが発生せず、即値として扱われるためです。
constで使用できる型
constには厳しい制限があります。
それは「リテラルとして表現できる組み込み型」しか指定できないという点です。
- 数値型(int, double, long, decimalなど)
- 論理型(bool)
- 文字・文字列型(char, string)
- 列挙型(enum)
- nullを代入した参照型
独自のクラス(newキーワードを使ってインスタンス化するもの)をconstに指定することはできません。
readonly修飾子の基本と特徴
一方で、readonlyは「実行時定数(読み取り専用フィールド)」と呼ばれます。
これは、プログラムが実行されている最中に、一度だけ値を決めることができる変数のような性質を持っています。
readonlyの定義と柔軟な初期化
readonlyの最大の特徴は、宣言時だけでなくコンストラクタの中でも値を代入できる点にあります。
これにより、実行時の状況(設定ファイルの内容や計算結果など)に応じて値を固定することが可能になります。
using System;
namespace ReadonlyExample
{
class AppConfig
{
// 宣言時に初期化
public readonly string Version = "1.0.0";
// コンストラクタで初期化
public readonly DateTime CreatedTime;
public AppConfig()
{
// 実行時の現在時刻を代入できる
CreatedTime = DateTime.Now;
}
}
class Program
{
static void Main(string[] args)
{
AppConfig config = new AppConfig();
Console.WriteLine($"Version: {config.Version}");
Console.WriteLine($"CreatedTime: {config.CreatedTime}");
// 以下のコードはコンパイルエラーになります
// config.Version = "2.0.0";
}
}
}
Version: 1.0.0
CreatedTime: 2024/05/20 10:30:15 (実行時の時刻)

参照型とreadonlyの注意点
readonlyを参照型(クラスなど)に使用する場合、注意が必要です。
変更できないのは「インスタンスの参照(住所)」であり、その中身(プロパティなど)ではないからです。
public class User
{
public string Name { get; set; }
}
public class MyClass
{
public readonly User AdminUser = new User { Name = "Alice" };
public void UpdateName()
{
// これはOK(中身の変更は可能)
AdminUser.Name = "Bob";
// これはNG(参照そのものの差し替えは不可)
// AdminUser = new User { Name = "Charlie" };
}
}
このように、readonlyは「その変数が別のインスタンスを指すこと」を禁止しますが、インスタンス内部の状態変更まで防ぐものではありません。
constとreadonlyの徹底比較
両者の違いを明確にするために、主要なポイントを比較表にまとめました。
| 比較項目 | const | readonly |
|---|---|---|
| 確定タイミング | コンパイル時 | 実行時 |
| 初期化場所 | 宣言時のみ | 宣言時またはコンストラクタ内 |
| 型制限 | 組み込み型のみ | 制限なし(全ての型で使用可能) |
| 静的/インスタンス | 常に静的(暗黙的にstatic) | 静的(static)もインスタンスも可 |
| メモリ展開 | 値が直接埋め込まれる(インライン) | メモリ上の変数を参照する |
| ローカル変数 | 使用可能 | 使用不可(フィールドのみ) |
1. 静的(static)かインスタンスか
constは、クラスに紐づく値として扱われるため、暗黙的にstatic(静的)となります。
インスタンスを生成しなくても「クラス名.定数名」でアクセスできます。
対してreadonlyは、デフォルトではインスタンスごとに値を持つことができます。
もし全てのインスタンスで共有する読み取り専用の値を定義したい場合は、明示的にstatic readonlyと記述する必要があります。
2. ローカル変数での利用
constはメソッドの内部でローカル定数として宣言できますが、readonlyはフィールド(クラスのメンバ変数)としてしか宣言できません。
メソッドの中で一度決めたら変えたくない値がある場合は、constを使うか、通常の変数として宣言して変更しないように運用するしかありません。

バージョニング問題(DLL Hellのリスク)
実務において最も重要な違いの一つが、外部ライブラリ(DLL)として提供する場合の挙動です。
これは「バージョニング問題」と呼ばれ、深刻なバグを引き起こす可能性があります。
constで発生する問題のシナリオ
- Aというライブラリ(DLL)で
public const int MaxValue = 100;と定義する。 - Bという実行ファイルが、Aを参照して
MaxValueを利用する。このとき、Bには「100」という数値が直接埋め込まれる。 - 後日、Aの
MaxValueを「200」に変更してビルドし、DLLだけを差し替える。 - Bを再ビルドせずに実行すると、Bは古い「100」という値を使い続けてしまう</cst-red。
これは、Bが参照しているのが「Aのメモリ上の値」ではなく、コンパイル時にコピーした「即値」だからです。
readonlyによる解決
同じ状況でstatic readonlyを使用していた場合、Bは実行時にAのメモリへ値を見に行きます。
そのため、AのDLLを差し替えるだけで、Bを再ビルドすることなく「200」という新しい値が反映されます。
将来的に値が変わる可能性がある公開定数には、constではなくstatic readonlyを使うのが鉄則です。

使い分けのポイントと推奨される指針
これまでの内容を踏まえ、どのように使い分けるべきかのガイドラインをまとめます。
constを使うべきケース
- 数学的な定数(円周率、物理定数など)。
- プロトコルのバージョンなど、絶対に変わることがない値。
switch文のcaseラベルに使用したい値(caseにはコンパイル時定数しか指定できません)。- 属性(Attribute)の引数に渡す値。
readonlyを使うべきケース
- 設定ファイルや環境変数、データベースから取得した値を固定したい場合。
newを使って生成するオブジェクト(Listや独自クラスなど)を固定したい場合。- ライブラリとして公開し、将来的に値を変更する可能性がある場合。
- 静的な値ではなく、インスタンスごとに異なる固定値を持ちたい場合。
C# 12以降の最新機能との関連
C#の進化に伴い、readonlyの利用シーンはさらに広がっています。
特に注目したいのが「プライマリコンストラクタ」と「readonly構造体」です。
プライマリコンストラクタでの利用
C# 12で導入されたプライマリコンストラクタでは、クラス宣言のすぐ後ろに引数を記述できます。
この引数はクラス内部で利用できますが、デフォルトではreadonlyではありません。
意図的に値を固定したい場合は、明示的なreadonlyフィールドへの代入が必要です。
// C# 12 プライマリコンストラクタの例
public class UserService(int id, string name)
{
// 引数id, nameは書き換え可能。
// 固定したい場合はreadonlyフィールドに格納する
private readonly int _id = id;
private readonly string _name = name;
public void PrintInfo() => Console.WriteLine($"{_id}: {_name}");
}
readonly structによるパフォーマンス向上
構造体(struct)全体をreadonlyとして宣言することもできます。
これにより、その構造体の全てのフィールドが読み取り専用であることを保証し、コンパイラによる最適化(コピーの回避など)が効きやすくなり、パフォーマンスが向上します。
public readonly struct Point
{
public double X { get; }
public double Y { get; }
public Point(double x, double y) => (X, Y) = (x, y);
}
このように、現代のC#開発では、不変性(イミュータビリティ)を保つためにreadonlyを積極的に活用する設計が推奨されています。
まとめ
C#におけるconstとreadonlyは、どちらもプログラムの堅牢性を高めるために欠かせない要素です。
「コンパイル時に決まる絶対的な不変値」にはconstを使い、「実行時に決まる値や、柔軟性・安全性を重視する値」にはreadonlyを選択するのが基本です。
特に、外部に公開するライブラリを作成する場合や、複雑なオブジェクトを扱う場合には、readonly(またはstatic readonly)の方がバージョニングのトラブルを避けやすく、安全です。
それぞれの特性を正しく理解して使い分けることで、バグが少なくメンテナンス性の高い、高品質なC#コードを記述できるようになります。
今回の解説が、日々のコーディングの判断基準として役立てば幸いです。
