C#の定数クラスは必要?const・readonly・static readonlyの使い分けと設計のベストプラクティス

はじめに

C#では、固定値を表現するためにconstreadonlystatic readonlyを利用できます。また、複数の固定値をstaticクラスにまとめ、いわゆる「定数クラス」として管理する方法もよく使われます。

ただし、定数クラスは常に必要なわけではありません。値の種類によっては、enumや設定クラス、値オブジェクト、リソースファイルなどを使ったほうが、型安全性や保守性を高められます。

この記事では、C#の定数クラスの基本から、constreadonlystatic readonlyの違い、使い分け、定数クラスが不要といわれる理由、保守しやすい設計のベストプラクティスまで詳しく解説します。

1. C#の定数クラスとは?必要性を判断する前に知っておきたい基本

1-1. 定数クラスは複数の定数をまとめて管理するためのクラス

定数クラスとは、アプリケーション内で共通して利用する定数や読み取り専用の値を、一つのクラスにまとめたものです。

たとえば、HTTPヘッダー名、入力文字数の上限、日付形式、キャッシュキーなどを、次のように管理できます。

public static class HttpHeaderNames{public const string Authorization = "Authorization";public const string ContentType = "Content-Type";}

利用側では、値を直接記述する代わりに、定数名を通じて参照します。

request.Headers.Add(HttpHeaderNames.Authorization, token);

文字列を直接記述する場合よりも、値の意味が伝わりやすく、入力ミスも防ぎやすくなります。

1-2. C#に「定数クラス」という専用機能はない

C#の言語仕様に「定数クラス」という専用の構文や型が存在するわけではありません。

一般的に定数クラスと呼ばれているものは、次のようなメンバーをまとめたクラスです。

  • constフィールド

  • static readonlyフィールド

  • 読み取り専用の静的プロパティ

多くの場合、インスタンス化する必要がないため、static classとして定義します。

public static class ApplicationLimits{public const int MaximumUserNameLength = 50;public const int MaximumRetryCount = 3;}

つまり、定数クラスはC#の専用機能ではなく、固定値を整理するための設計パターンの一つです。

1-3. staticクラスを定数クラスとして使う基本例

定数クラスは、基本的にstatic classとして作成します。

public static class DateFormats{public const string Date = "yyyy-MM-dd";public const string DateTime = "yyyy-MM-dd HH:mm:ss";public const string YearMonth = "yyyy-MM";}

利用側では、クラス名と定数名を指定します。

string text = DateTime.Now.ToString(DateFormats.DateTime);

static classには、次の特徴があります。

  • インスタンス化できない

  • 継承できない

  • インスタンスメンバーを定義できない

  • メンバーへクラス名から直接アクセスできる

固定値の置き場所として使うクラスには、これらの特徴が適しています。

1-4. 定数クラスが使われる代表的なケース

定数クラスが使われる代表的なケースには、次のようなものがあります。

public static class ValidationRules{public const int MinimumPasswordLength = 12;public const int MaximumDisplayNameLength = 100;}

public static class CacheKeys{public const string CurrentUser = "current-user";public const string ProductList = "product-list";}

public static class RouteNames{public const string Home = "Home";public const string Login = "Login";}

特に、次の条件を満たす値は定数クラスで管理しやすいでしょう。

  • 複数の場所で同じ値を利用する

  • 実行環境によって変化しない

  • 値同士に明確な関連性がある

  • 値の意味を名前で表現したい

  • 選択肢ではなく、単独の固定値として使う

一方、環境ごとに変わるURLや接続先、運用中に変更される可能性がある制限値は、定数クラスではなく設定ファイルで管理するのが基本です。

2. C#のconst・readonly・static readonlyの違い

2-1. constはコンパイル時に値が確定する定数

constは、コンパイル時に値が確定する定数を定義するためのキーワードです。

public const int MaximumRetryCount = 3;public const string DefaultLanguage = "ja";

constには、次の特徴があります。

  • 宣言時に値を設定する必要がある

  • コンパイル時に値が確定する

  • 宣言後に変更できない

  • 暗黙的にstaticとして扱われる

  • 参照側のコードへ値が埋め込まれる

そのため、次のようにstaticを重ねて指定することはできません。

// コンパイルエラーpublic static const int MaximumRetryCount = 3;

constは、円周率の独自近似値やプロトコル上の固定文字列など、将来も変更されないことが明確な値に向いています。

2-2. readonlyはインスタンス生成時に値を設定できる読み取り専用フィールド

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

public sealed class User{public readonly Guid Id;

public User(Guid id){Id = id;}

}

Idはユーザーごとに異なりますが、オブジェクトの生成後には再代入できません。

一般的には、フィールドを直接公開するよりも、読み取り専用プロパティを使う設計もよく採用されます。

public sealed class User{public Guid Id { get; }

public User(Guid id){Id = id;}

}

readonlyが適しているのは、インスタンスごとに値が異なり、生成後は変更させたくない場合です。

2-3. static readonlyは実行時に初期化できるクラス共通の読み取り専用フィールド

static readonlyは、クラス全体で共有しながら、実行時に初期化できる読み取り専用フィールドです。

public static class SystemValues{public static readonly DateTime ServiceStartedAt = DateTime.UtcNow;public static readonly Guid ApplicationId = Guid.NewGuid();}

constとは異なり、メソッドの実行結果やコンストラクターで生成したオブジェクトを設定できます。

静的コンストラクターで初期化することも可能です。

public static class RuntimeSettings{public static readonly string MachineName;

static RuntimeSettings(){MachineName = Environment.MachineName;}

}

static readonlyの値を代入できるのは、フィールド宣言時または同じ型の静的コンストラクター内だけです。

2-4. const・readonly・static readonlyの違いを比較表で確認

項目constreadonlystatic readonly
値が確定する時点コンパイル時インスタンス生成時実行時
クラス全体で共有はいいいえはい
インスタンスごとに異なる値不可可能不可
コンストラクターで代入不可インスタンスコンストラクターで可能静的コンストラクターで可能
メソッドの戻り値を設定不可可能可能
オブジェクトを設定原則不可可能可能
暗黙的にstaticはいいいえ明示的に指定
参照側への値の埋め込みありなしなし

値がコンパイル時に確定するか、実行時に決まるかが、選択する際の大きな判断基準です。

2-5. 定数として利用できる型と初期化タイミングの違い

constとして宣言できる型は限定されています。代表的には、次の型を利用できます。

  • 整数型

  • 浮動小数点型

  • decimal

  • bool

  • char

  • string

  • enum

  • 参照型に対するnull

たとえば、次の定義は有効です。

public const int MaximumCount = 100;public const decimal TaxRate = 0.10m;public const string DefaultName = "Guest";public const DayOfWeek FirstDay = DayOfWeek.Monday;

一方、DateTime、配列、Guid、独自クラスのインスタンスなどは、constとして定義できません。

// コンパイルエラー// public const DateTime ReleaseDate = new DateTime(2026, 1, 1);// public const int[] SupportedCodes = new[] { 1, 2, 3 };

これらをクラス共通の固定値として扱う場合は、static readonlyを使います。

public static readonly DateTime ReleaseDate =new DateTime(2026, 1, 1);

public static readonly Guid SystemId =Guid.Parse("98f79cbe-d82b-430d-b29d-dbfadce70ea2");

3. const・readonly・static readonlyの使い分け

3-1. 絶対に変わらないプリミティブ値にはconstを使う

コンパイル時に確定し、将来も変更されない値にはconstが適しています。

public static class MathematicalConstants{public const int DegreesInCircle = 360;}

public static class ProtocolValues{public const string JsonMediaType = "application/json";}

ただし、「現在は変更しない予定」というだけでは、constを選ぶ理由として不十分です。

たとえば、最大アップロードサイズ、タイムアウト秒数、リトライ回数は、運用中に変更される可能性があります。このような値は設定ファイルへ移動できないか検討しましょう。

3-2. インスタンスごとに異なる固定値にはreadonlyを使う

オブジェクトごとに異なるものの、生成後は変更しない値にはreadonlyまたはgetterのみのプロパティを使います。

public sealed class Order{public readonly Guid OrderId;public readonly DateTime CreatedAt;

public Order(Guid orderId, DateTime createdAt){OrderId = orderId;CreatedAt = createdAt;}

}

プロパティを使用する場合は次のようになります。

public sealed class Order{public Guid OrderId { get; }public DateTime CreatedAt { get; }

public Order(Guid orderId, DateTime createdAt){OrderId = orderId;CreatedAt = createdAt;}

}

値のカプセル化や将来の変更を考慮すると、公開APIではプロパティを選ぶケースが一般的です。

3-3. 実行時に生成する共通値にはstatic readonlyを使う

クラス全体で一つの値を共有し、実行時の処理によって初期化する場合はstatic readonlyを使用します。

public static class Identifiers{public static readonly Guid ApplicationId =Guid.Parse("53f02628-a542-46f7-9df1-cb6f5def9432");}

次のような値もstatic readonlyの対象です。

  • DateTime

  • Guid

  • 正規表現オブジェクト

  • 読み取り専用コレクション

  • 独自クラスのインスタンス

  • メソッドの戻り値

  • 実行環境から取得した値

using System.Text.RegularExpressions;

public static class ValidationPatterns{public static readonly Regex PostalCode =new(@"^\d{3}-\d{4}$", RegexOptions.Compiled);}

3-4. string・DateTime・配列・オブジェクトを定義する場合の選び方

stringconststatic readonlyの両方で定義できます。

public const string MediaType = "application/json";

public static readonly string MachineName =Environment.MachineName;

リテラルとして固定でき、将来も変わらない文字列ならconstを利用できます。実行時に生成する文字列や、外部ライブラリへ公開する変更可能性のある文字列にはstatic readonlyが適しています。

DateTimeconstにできないため、static readonlyを使います。

public static readonly DateTime ServiceReleaseDate =new(2026, 4, 1);

配列もconstにできません。ただし、配列をstatic readonlyとして定義しても、配列の要素までは読み取り専用になりません。

public static readonly int[] Codes = { 100, 200, 300 };

// フィールドの再代入はできないが、要素は変更できるCodes[0] = 999;

コレクションを安全に公開する場合は、読み取り専用ラッパーや不変コレクションを利用します。

独自オブジェクトをstatic readonlyで公開するときも同様です。フィールドの参照先は変更できませんが、オブジェクト自体が可変なら、その内部状態は変更できます。

3-5. public constをライブラリで公開するときの注意点

public constの値は、利用側のアセンブリへコンパイル時に埋め込まれます。

たとえば、ライブラリ側に次の定数があるとします。

public static class LibraryDefaults{public const int TimeoutSeconds = 30;}

利用側がこの値を参照してコンパイルすると、利用側のコードには30が埋め込まれます。

その後、ライブラリだけを更新して値を60へ変更しても、利用側のアプリケーションを再コンパイルしない限り、古い値が使われる可能性があります。

変更される可能性がある公開値は、public static readonlyまたは静的プロパティにするほうが安全です。

public static class LibraryDefaults{public static readonly int TimeoutSeconds = 30;}

constは、値の変更が事実上あり得ない場合に限定して公開するのが基本です。

3-6. 判断に迷ったときの選定フローチャート

次の順番で考えると、適切な方法を選びやすくなります。

値は環境や運用によって変わるか?├─ はい│  └─ appsettings.jsonやOptionsパターンを検討する└─ いいえ├─ 選択肢や状態を表しているか?│  └─ enumや専用型を検討する└─ 単独の固定値か?├─ インスタンスごとに異なるか?│  └─ readonlyまたはgetterのみのプロパティ└─ クラス全体で共通か?├─ コンパイル時に確定できるか?│  ├─ はい → const│  └─ いいえ → static readonly└─ 公開ライブラリで将来変更される可能性があるか?└─ はい → static readonlyまたは静的プロパティ

単に「変更したくない」という理由だけで定数クラスへ入れず、その値が何を表しているかまで考えることが重要です。

4. C#で定数クラスを作るメリット

4-1. マジックナンバーや重複文字列を減らせる

コード内に意味の分からない数値や文字列を直接記述すると、処理の意図を理解しにくくなります。

if (retryCount >= 3){// 処理}

定数へ置き換えると、値の意味が明確になります。

if (retryCount >= RetryPolicy.MaximumRetryCount){// 処理}

同じ文字列を複数箇所へ記述する場合も、定数化によってタイプミスを防げます。

4-2. 関連する値を一か所に整理できる

同じ用途の値を一つのクラスへまとめると、関連性を把握しやすくなります。

public static class FileExtensions{public const string Json = ".json";public const string Csv = ".csv";public const string Pdf = ".pdf";}

ファイル拡張子に関する値を探すときは、FileExtensionsクラスを確認すれば済みます。

4-3. 値の意味が明確になりコードの可読性が高まる

次のコードでは、100が何を表しているのか分かりません。

if (name.Length > 100){throw new ArgumentException();}

名前付き定数を使うと、値の意味が明確になります。

if (name.Length > UserValidation.MaximumDisplayNameLength){throw new ArgumentException();}

コードを読む人が、数値の意味を調査する手間も減らせます。

4-4. 変更箇所を集約して保守しやすくできる

同じ値が複数箇所に直接記述されていると、変更時にすべての箇所を修正しなければなりません。

定数として一か所にまとめれば、基本的には定義元を変更するだけで済みます。

ただし、public constを別アセンブリから参照している場合は、参照側の再コンパイルが必要になる可能性があります。また、環境によって変更する値は、定数ではなく設定ファイルへ集約するべきです。

4-5. IDEの補完機能で定数を見つけやすくなる

用途別に定数クラスを分けると、IDEのコード補完から利用可能な値を確認できます。

string format = DateFormats.

ここまで入力すれば、DateDateTimeYearMonthなどの候補が表示されます。

クラス名によって値の分類が明確になっていれば、開発者が定数の存在を発見しやすくなります。

5. C#の定数クラスが不要・アンチパターンといわれる理由

5-1. 無関係な定数を集めた巨大クラスになりやすい

定数クラスを一つだけ作ると、さまざまな値が追加され続ける可能性があります。

public static class Constants{public const int MaximumRetryCount = 3;public const string DateFormat = "yyyy-MM-dd";public const string AdminRole = "Admin";public const string ProductCacheKey = "products";public const int DefaultPageSize = 20;}

リトライ回数、日付形式、権限、キャッシュ、ページサイズには、それぞれ異なる責務があります。

一つのクラスへ混在させると、目的の値を探しにくくなり、変更の影響範囲も把握しづらくなります。

5-2. Constantsという汎用的な名前では値の責務が伝わらない

Constantsという名前からは、そのクラスが何の値を管理しているのか判断できません。

Constants.MaximumLength

何の最大長なのかが分かりにくく、利用側のコードだけでは意味を理解できない可能性があります。

用途を表す名前を付けると、責務が明確になります。

UserValidation.MaximumDisplayNameLengthUploadLimits.MaximumFileSizeBytesRetryPolicy.MaximumRetryCount

「定数であること」よりも、「何に関する値であるか」をクラス名で表現することが重要です。

5-3. グローバル変数のように依存箇所が増えやすい

定数クラスはどこからでも簡単に参照できます。そのため、無計画に利用すると、多数のクラスが一つの定数クラスへ依存する状態になります。

値の変更が多くの機能へ影響するだけでなく、本来は特定の機能内に閉じるべき知識が、アプリケーション全体へ広がる原因にもなります。

定数を配置するときは、可能な限り利用するクラスや機能の近くに置きましょう。

5-4. 本来はenumや設定クラスで表現すべき値まで定数化しやすい

注文状態を文字列定数で表現すると、任意の文字列を渡せてしまいます。

public static class OrderStatuses{public const string Pending = "Pending";public const string Paid = "Paid";public const string Shipped = "Shipped";}

メソッドがstringを受け取る場合、存在しない状態も指定できます。

void ChangeStatus(string status){}

ChangeStatus("Unknown");

選択肢が限定されているなら、enumのほうが型安全です。

public enum OrderStatus{Pending,Paid,Shipped}

また、APIのURLやタイムアウト値など、環境によって変わる値は設定クラスで管理するべきです。

5-5. 変更される可能性がある値を定数にすると設計が硬直化する

事業ルールや運用ルールは、将来変更される可能性があります。

public const int FreeTrialDays = 30;

無料期間がキャンペーンや契約プランによって変わるようになれば、単純な定数では表現できません。

値を定数として埋め込む前に、次の点を確認しましょう。

  • 環境ごとに変わらないか

  • 顧客やプランごとに変わらないか

  • 管理画面から変更する可能性はないか

  • 将来、計算ルールへ発展しないか

  • 外部サービスから取得する可能性はないか

変更可能性が高い値は、設定、データベース、ポリシークラスなどに持たせるほうが柔軟です。

5-6. 定数クラスを使わないほうがよいケース

次のようなケースでは、定数クラス以外の設計を優先しましょう。

  • 開発環境と本番環境で値が異なる

  • 運用中に値を変更する可能性がある

  • 複数の選択肢や状態を表している

  • 値に検証処理や振る舞いが必要

  • 特定のクラスでしか使わない

  • 表示言語によって文字列が変わる

  • 値を差し替えてテストしたい

  • 顧客や契約プランごとに値が異なる

定数クラスは、変更されない単純な値を整理するための手段です。設定管理やドメインモデリングの代わりにはなりません。

6. 定数クラスの代わりに検討すべき設計

6-1. 選択肢や状態を表す値にはenumを使う

選択肢が有限である場合は、文字列定数や数値定数よりもenumが適しています。

public enum PaymentStatus{Pending,Completed,Failed,Refunded}

メソッドの引数にもenumを指定できます。

public void UpdateStatus(PaymentStatus status){}

これにより、定義されていない文字列を誤って渡すリスクを減らせます。

ただし、外部APIへ送信する文字列や、データベース上のコードとenumの名前が一致するとは限りません。必要に応じて変換処理を用意しましょう。

6-2. 環境ごとに変わる値にはappsettings.jsonを使う

APIのURL、タイムアウト、ファイル保存先などは、環境ごとに変わる可能性があります。

そのような値は、ASP.NET Coreのappsettings.jsonなどで管理します。

{"ExternalApi": {"BaseUrl": "https://api.example.com","TimeoutSeconds": 30}}

環境別の設定ファイルを用意すれば、本番環境と開発環境で値を切り替えられます。

appsettings.jsonappsettings.Development.jsonappsettings.Production.json

機密情報は設定ファイルへ直接書き込まず、環境変数、シークレット管理サービス、開発用シークレットなどを利用します。

6-3. 型安全な設定値にはOptionsパターンを使う

ASP.NET Coreでは、設定値を専用クラスへバインドするOptionsパターンを利用できます。

public sealed class ExternalApiOptions{public const string SectionName = "ExternalApi";

public required string BaseUrl { get; init; }public int TimeoutSeconds { get; init; }

}

サービス登録時に設定をバインドします。

builder.Services.AddOptions<ExternalApiOptions>().BindConfiguration(ExternalApiOptions.SectionName).ValidateDataAnnotations().ValidateOnStart();

利用するサービスでは、IOptions<T>などを受け取ります。

using Microsoft.Extensions.Options;

public sealed class ExternalApiClient{private readonly ExternalApiOptions _options;

public ExternalApiClient(IOptions&lt;ExternalApiOptions&gt; options){_options = options.Value;}

}

設定値を文字列キーで直接取得するよりも、型安全でテストしやすい設計になります。

6-4. ドメイン固有の値は値オブジェクトや専用型に持たせる

メールアドレス、金額、商品コードなどは、単なる文字列や数値ではなく、独自の制約や振る舞いを持つことがあります。

その場合は、値オブジェクトや専用型として表現します。

public sealed record ProductCode{public const int MaximumLength = 20;

public string Value { get; }public ProductCode(string value){if (string.IsNullOrWhiteSpace(value)){throw new ArgumentException("商品コードは必須です。",nameof(value));}if (value.Length &gt; MaximumLength){throw new ArgumentException($"商品コードは{MaximumLength}文字以内で指定してください。",nameof(value));}Value = value;}public override string ToString() =&gt; Value;

}

この例では、最大長がProductCode型のルールであることが明確です。アプリケーション全体のConstantsクラスへ置く必要はありません。

6-5. クラス固有の定数は利用するクラス内に配置する

一つのクラスでしか使わない定数は、そのクラスの内部へ置くほうが自然です。

public sealed class RetryService{private const int MaximumRetryCount = 3;

public async Task ExecuteAsync(Func&lt;Task&gt; action,CancellationToken cancellationToken){for (int attempt = 1; attempt &lt;= MaximumRetryCount; attempt++){try{await action();return;}catch when (attempt &lt; MaximumRetryCount){await Task.Delay(1000, cancellationToken);}}}

}

この定数はRetryServiceの実装詳細です。外部へ公開しなければ、依存範囲を小さくできます。

6-6. 文字列の分類にはスマートEnumや静的プロパティを検討する

単純なenumでは、表示名、外部コード、検証処理などを持たせにくい場合があります。

そのような場合は、スマートEnumに相当する専用型を作る方法があります。

public sealed record ShippingMethod{public static ShippingMethod Standard { get; } =new("standard", "通常配送");

public static ShippingMethod Express { get; } =new("express", "速達配送");public string Code { get; }public string DisplayName { get; }private ShippingMethod(string code, string displayName){Code = code;DisplayName = displayName;}

}

文字列定数よりも、コードと表示名の関係を型として表現できます。

ただし、独自実装ではシリアライズやデータベース保存への対応も必要です。単純な選択肢であれば、まずenumを検討しましょう。

6-7. リソース文字列やエラーメッセージは専用の仕組みで管理する

画面表示やエラーメッセージを定数クラスへ大量にまとめると、多言語対応や文言変更が難しくなります。

public static class ErrorMessages{public const string UserNotFound ="ユーザーが見つかりません。";}

表示言語を切り替える可能性があるなら、リソースファイルやローカライズ機能を利用しましょう。

また、例外メッセージを文字列比較して処理を分岐させる設計は避け、エラーコードや専用例外型を使います。

public static class ErrorCodes{public const string UserNotFound = "USER_NOT_FOUND";}

表示用メッセージと、プログラムが判定する識別子は分離することが重要です。

7. 保守しやすい定数クラスの設計ベストプラクティス

7-1. 何でも入れるConstantsクラスを作らない

アプリケーション全体で一つのConstantsクラスを作ると、無関係な値が集まりやすくなります。

// 避けたい例public static class Constants{public const string DateFormat = "yyyy-MM-dd";public const string AdminRole = "Admin";public const int MaximumRetryCount = 3;}

値の用途が異なる場合は、別々のクラスへ分割します。

public static class DateFormats{public const string Standard = "yyyy-MM-dd";}

public static class RoleNames{public const string Administrator = "Admin";}

public static class RetryPolicy{public const int MaximumRetryCount = 3;}

7-2. 用途やドメインごとにクラスを分割する

定数クラスの分割単位は、型ではなく用途やドメインを基準にします。

たとえば、すべての文字列をStringConstantsへ入れる方法は適切ではありません。

// 責務が分かりにくいpublic static class StringConstants{public const string Admin = "Admin";public const string Json = "application/json";public const string Date = "yyyy-MM-dd";}

次のように分類したほうが意味が伝わります。

public static class RoleNames{public const string Administrator = "Admin";}

public static class MediaTypes{public const string Json = "application/json";}

public static class DateFormats{public const string Date = "yyyy-MM-dd";}

7-3. クラス名と定数名から値の意味が分かるようにする

曖昧な名前は避け、値の対象や単位が分かる名前を付けます。

// 意味が曖昧public const int Max = 10;public const int Timeout = 30;

次のようにすると、用途を理解しやすくなります。

public const int MaximumLoginAttempts = 10;public const int RequestTimeoutSeconds = 30;

クラス名と組み合わせたときに重複が多くなる場合は、適度に短くしても構いません。

UploadLimits.MaximumFileSizeBytesPaginationDefaults.PageSizeRetryPolicy.MaximumAttempts

7-4. static classでインスタンス化と継承を防ぐ

定数だけを提供するクラスは、原則としてstatic classにします。

public static class MediaTypes{public const string Json = "application/json";}

通常クラスにすると、不要なインスタンスを作成できてしまいます。

public class MediaTypes{public const string Json = "application/json";}

// 意味のないインスタンス化が可能var mediaTypes = new MediaTypes();

static classを使うことで、このクラスが状態を持たないユーティリティであることも明示できます。

7-5. アクセス修飾子を必要最小限にする

定数を何でもpublicにすると、外部からの依存が増えます。

同じクラス内でしか使わない場合はprivate、同じアセンブリ内だけで使う場合はinternalを選びましょう。

internal static class DatabaseColumnLengths{internal const int UserName = 100;}

公開ライブラリのpublicメンバーは、後から削除や変更をしにくくなります。必要性が明確でない定数は公開しないことが重要です。

7-6. mutableな配列やコレクションをそのまま公開しない

次のコードでは、配列フィールド自体を再代入できませんが、要素は変更できます。

public static class SupportedValues{public static readonly string[] Languages ={"ja","en"};}

利用側から内容を書き換えられます。

SupportedValues.Languages[0] = "unknown";

読み取り専用ラッパーを使用すると、外部からの変更を防ぎやすくなります。

using System.Collections.ObjectModel;

public static class SupportedValues{private static readonly string[] LanguageValues ={"ja","en"};

public static ReadOnlyCollection&lt;string&gt; Languages { get; } =Array.AsReadOnly(LanguageValues);

}

不変コレクションを使う方法もあります。

using System.Collections.Immutable;

public static class SupportedValues{public static ImmutableArray<string> Languages { get; } =ImmutableArray.Create("ja", "en");}

7-7. 値の単位・形式・使用条件をコメントで明示する

数値には単位を名前に含めると、誤用を防ぎやすくなります。

public const int RequestTimeoutSeconds = 30;public const long MaximumFileSizeBytes = 10 * 1024 * 1024;

特殊な制約がある場合は、XMLドキュメントコメントで補足します。

public static class UploadLimits{/// <summary>/// 画像ファイル1件あたりの最大サイズ。/// 単位はバイト。/// </summary>public const long MaximumImageFileSizeBytes =10 * 1024 * 1024;}

ただし、名前だけで説明できる内容までコメントへ重複して書く必要はありません。コメントは理由、制約、例外条件を補足するために使います。

7-8. 定期的に不要な定数や重複定義を見直す

開発が進むと、使われなくなった定数や、別の場所に重複して定義された値が増えることがあります。

定期的に次の点を確認しましょう。

  • 参照されていない定数がないか

  • 同じ意味の値が複数箇所にないか

  • 設定ファイルへ移すべき値がないか

  • enumや専用型へ変更すべき値がないか

  • クラスの責務が広がりすぎていないか

  • 公開範囲を狭められないか

定数クラスも通常のコードと同様に、継続的なリファクタリングが必要です。

8. 定数クラスの実装例と改善例

8-1. constだけを定義する基本的な定数クラス

次は、HTTP関連の固定文字列をまとめる例です。

public static class HttpMediaTypes{public const string Json = "application/json";public const string Xml = "application/xml";public const string PlainText = "text/plain";}

利用側では、次のように参照できます。

httpRequest.Headers.Accept.ParseAdd(HttpMediaTypes.Json);

すべての値が文字列リテラルであり、実行時の初期化が不要なので、constを利用できます。

8-2. static readonlyを使って実行時に値を初期化する例

DateTimeRegexなど、コンパイル時に確定できない値にはstatic readonlyを使います。

using System.Text.RegularExpressions;

public static class UserValidation{public static readonly Regex UserNamePattern =new(@"^[a-zA-Z0-9_]+$",RegexOptions.Compiled |RegexOptions.CultureInvariant);

public static readonly DateTime RuleEffectiveFrom =new(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc);

}

ただし、Regexにはタイムアウトを指定し、過度に時間がかかる入力への対策を行うことも検討します。

public static readonly Regex UserNamePattern =new(@"^[a-zA-Z0-9_]+$",RegexOptions.Compiled |RegexOptions.CultureInvariant,TimeSpan.FromSeconds(1));

8-3. 用途別のネストクラスで定数を整理する例

関連性の高い値をネストクラスで分類する方法もあります。

public static class ApiRoutes{public static class Users{public const string List = "/api/users";public const string Detail = "/api/users/{id}";}

public static class Products{public const string List = "/api/products";public const string Detail = "/api/products/{id}";}

}

利用側では、階層から用途を判断できます。

string route = ApiRoutes.Users.Detail;

ただし、階層が深くなりすぎると記述が長くなります。機能が独立している場合は、UserApiRoutesProductApiRoutesのように別クラスへ分ける方法も検討しましょう。

8-4. 変更可能な配列を公開してしまう悪い実装例

次の実装では、static readonlyを使っているため安全に見えます。

public static class SupportedFileTypes{public static readonly string[] Extensions ={".jpg",".png",".gif"};}

しかし、readonlyが禁止するのはフィールドへの再代入だけです。配列の要素は変更できます。

SupportedFileTypes.Extensions[0] = ".exe";

アプリケーション全体で共有している配列が変更されると、予期しない不具合やセキュリティ上の問題につながる可能性があります。

8-5. IReadOnlyListを使ってコレクションを安全に公開する例

読み取り専用コレクションとして公開する場合は、元の可変配列を外部へ渡さないようにします。

using System.Collections.ObjectModel;

public static class SupportedFileTypes{private static readonly string[] ExtensionValues ={".jpg",".png",".gif"};

public static IReadOnlyList&lt;string&gt; Extensions { get; } =Array.AsReadOnly(ExtensionValues);

}

Array.AsReadOnlyによってラップしているため、利用側から要素を変更できません。

より強い不変性を求める場合は、ImmutableArray<T>も有力です。

using System.Collections.Immutable;

public static class SupportedFileTypes{public static ImmutableArray<string> Extensions { get; } =ImmutableArray.Create(".jpg", ".png", ".gif");}

単に配列をIReadOnlyList<T>型として公開するだけでは、実体が配列である場合にキャストされる可能性があります。読み取り専用ラッパーや不変コレクションを利用するほうが安全です。

8-6. 巨大なConstantsクラスを責務ごとに分割する改善例

改善前のコードを確認します。

public static class Constants{public const string AdminRole = "Admin";public const int DefaultPageSize = 20;public const string DateFormat = "yyyy-MM-dd";public const int MaximumRetryCount = 3;public const string ProductCacheKey = "product-list";}

値同士の関連性がなく、クラス名から用途も判断できません。

責務ごとに分割すると、次のようになります。

public static class RoleNames{public const string Administrator = "Admin";}

public static class PaginationDefaults{public const int PageSize = 20;}

public static class DateFormats{public const string Standard = "yyyy-MM-dd";}

public static class RetryPolicy{public const int MaximumAttempts = 3;}

public static class ProductCacheKeys{public const string List = "product-list";}

さらに、特定のクラスだけで使う値は、そのクラス内のprivate constへ移動します。環境ごとに変わる値は設定へ、状態を表す値はenumへ移すことで、定数クラスへの依存を減らせます。

9. C#の定数クラスに関するよくある質問

9-1. 定数クラスはstaticにするべき?

定数やクラス共通の読み取り専用値だけを管理するなら、基本的にはstatic classにします。

public static class DateFormats{public const string Standard = "yyyy-MM-dd";}

static classにすることで、不要なインスタンス化と継承を防げます。

ただし、値を依存性注入で差し替えたい場合や、環境ごとに値を変えたい場合は、定数クラスではなく設定クラスやサービスとして設計しましょう。

9-2. constとstatic readonlyはどちらを優先するべき?

コンパイル時に確定し、将来も変更されない値にはconstを使えます。

public const string JsonMediaType = "application/json";

実行時に生成する値、オブジェクト、変更される可能性がある公開値にはstatic readonlyが適しています。

public static readonly DateTime ReleaseDate =new(2026, 4, 1);

特に公開ライブラリでは、値が参照側へ埋め込まれるpublic constの性質に注意が必要です。将来変更する可能性が少しでもあるなら、static readonlyや静的プロパティを検討しましょう。

9-3. public static readonlyの値は変更されない?

フィールド自体を別の値へ再代入することはできません。

public static readonly List<string> Names = new();

しかし、参照先のオブジェクトが可変であれば、その内容は変更できます。

Names.Add("Alice");Names.Clear();

static readonlyは、オブジェクトの不変性を保証するものではありません。外部へ公開する場合は、不変型、読み取り専用ラッパー、ImmutableArray<T>などを利用します。

9-4. constでDateTimeや配列を定義できる?

DateTimeや配列はconstとして定義できません。

// コンパイルエラー// public const DateTime StartDate = new DateTime(2026, 1, 1);// public const int[] Numbers = { 1, 2, 3 };

DateTimeにはstatic readonlyを使用します。

public static readonly DateTime StartDate =new(2026, 1, 1);

配列を共有する場合は、要素が変更されないように、読み取り専用コレクションや不変コレクションを検討しましょう。

9-5. 定数クラスとenumはどう使い分ける?

複数の選択肢や状態を表す場合は、原則としてenumを使います。

public enum AccountStatus{Active,Suspended,Closed}

日付形式や最大文字数など、選択肢ではない単独の固定値は、定数として扱えます。

public static class AccountValidation{public const int MaximumDisplayNameLength = 100;}

外部システムの文字列コードを扱う場合や、各値に追加情報や振る舞いを持たせたい場合は、専用型やスマートEnumも検討します。

9-6. 定数クラスの名前はConstantsでよい?

小規模なサンプルでは問題にならないこともありますが、実際のアプリケーションでは避けるのが無難です。

Constantsという名前では、何に関する値なのかが分かりません。

次のように、用途を表す名前を付けましょう。

DateFormatsMediaTypesRoleNamesUploadLimitsValidationRulesCacheKeys

クラス名は「定数であること」ではなく、「何を表しているか」を伝えるために使います。

9-7. エラーメッセージやURLを定数クラスにまとめてもよい?

変更されない内部的なエラーコードや、プロトコル上の固定パスであれば、用途別の定数クラスで管理できる場合があります。

public static class ErrorCodes{public const string UserNotFound = "USER_NOT_FOUND";}

一方、ユーザーへ表示するエラーメッセージは、多言語対応や文言変更を考慮し、リソースファイルなどで管理するほうが適しています。

APIのベースURLは環境ごとに異なる可能性があるため、通常はappsettings.jsonなどの設定へ置きます。

{"ExternalApi": {"BaseUrl": "https://api.example.com"}}

URLのうち、環境に依存しない相対パスだけを定数として管理する方法もあります。

public static class ExternalApiPaths{public const string Users = "/v1/users";public const string Products = "/v1/products";}

値が変更される理由とタイミングを考え、定数、設定、リソースを使い分けることが重要です。

まとめ

C#の定数クラスは、関連する固定値を整理し、マジックナンバーや重複文字列を減らすために役立ちます。ただし、無関係な値を一つのConstantsクラスへ集めると、責務が曖昧になり、グローバル変数のような依存を増やしてしまいます。

constreadonlystatic readonlyは、次のように使い分けます。

  • コンパイル時に確定し、将来も変わらない値にはconst

  • インスタンスごとに異なり、生成後は変更しない値にはreadonly

  • 実行時に初期化するクラス共通の値にはstatic readonly

また、値の性質に応じて、次の設計も検討する必要があります。

  • 選択肢や状態にはenum

  • 環境ごとに変わる値にはappsettings.json

  • 型安全な設定管理にはOptionsパターン

  • ドメイン固有の値には値オブジェクトや専用型

  • 表示メッセージにはリソースファイル

  • クラス固有の値にはprivate const

定数クラスを作ること自体を目的にせず、「その値は何を表し、どの範囲で使われ、将来どのような理由で変更されるか」を考えることが、保守しやすいC#コードにつながります。