C# 列挙型(enum)とは?基本の使い方から便利な活用例・注意点まで初心者向けに解説

C#でプログラムを作っていると、「処理中」「完了」「キャンセル」といった決められた選択肢を扱う場面がよくあります。このような値を数値や文字列で管理すると、入力ミスや値の意味が分かりにくいといった問題が起こりがちです。

そこで役立つのが、C#の列挙型であるenumです。列挙型を使うと、関連する複数の定数に名前を付け、ひとまとまりの型として安全に管理できます。

本記事では、C#の列挙型の基本構文から、条件分岐、文字列・数値との相互変換、Enumクラス、Flags属性を使ったビットフラグまで解説します。実践例や設計上の注意点も紹介するので、列挙型を初めて使う方はもちろん、使い方を整理したい方も参考にしてください。

1. C#の列挙型(enum)とは

1-1. 列挙型の基本的な意味と役割

C#の列挙型とは、関連する複数の名前付き定数を、1つの型としてまとめて定義する仕組みです。enumキーワードを使って定義します。

たとえば、注文の状態を次のように表現できます。

public enum OrderStatus{Pending,Processing,Shipped,Completed,Canceled}

この列挙型には、次の5つの値が定義されています。

  • Pending

  • Processing

  • Shipped

  • Completed

  • Canceled

変数を宣言するときは、型名としてOrderStatusを使用します。

OrderStatus status = OrderStatus.Pending;

statusには、原則としてOrderStatusに関連する値を代入します。そのため、単なる整数や文字列で状態を管理する場合よりも、コードの意味が明確になります。

列挙型の実体は整数値です。値を指定しなかった場合、先頭のメンバーには0、以降のメンバーには1ずつ増加した値が割り当てられます。

public enum OrderStatus{Pending,     // 0Processing,  // 1Shipped,     // 2Completed,   // 3Canceled     // 4}

ただし、通常の処理では整数値を直接意識するのではなく、OrderStatus.Pendingのような名前を使って操作します。

1-2. 定数や文字列で管理する場合との違い

列挙型を使わず、注文状態を整数で管理すると、次のようなコードになります。

int status = 2;

if (status == 2){Console.WriteLine("発送済みです");}

このコードだけを見ても、2が何を意味するのか分かりません。別の開発者が誤って100-1を代入する可能性もあります。

定数を使えば、数値の意味は多少分かりやすくなります。

public static class OrderStatusConstants{public const int Pending = 0;public const int Processing = 1;public const int Shipped = 2;}

int status = OrderStatusConstants.Shipped;

しかし、変数の型はあくまでintです。そのため、注文状態とは関係のない整数も代入できます。

int status = 999;

文字列で管理する場合も、入力ミスが問題になります。

string status = "Shippde";

本来は"Shipped"と書くべきところを"Shippde"と記述しても、コンパイルエラーにはなりません。

列挙型なら、存在しないメンバー名を記述した時点でコンパイルエラーになります。

OrderStatus status = OrderStatus.Shippde; // コンパイルエラー

このように列挙型には、関連する値を専用の型としてまとめ、コンパイル時に誤りを検出しやすくする特徴があります。

1-3. 列挙型を使うメリット

C#で列挙型を使う主なメリットは、コードの可読性と型安全性を高められることです。

次の2つの条件式を比較してみましょう。

if (status == 3){// 処理}
if (status == OrderStatus.Completed){// 処理}

後者であれば、「注文状態が完了の場合」という意味をすぐに理解できます。数値の意味をコメントや仕様書で確認する必要がありません。

また、Visual Studioなどの開発環境では、列挙型のメンバーが入力候補として表示されます。

OrderStatus status = OrderStatus.

ここまで入力すると、PendingCompletedなどの候補が表示されるため、入力ミスを防ぎやすくなります。

列挙型には、ほかにも次のメリットがあります。

  • 使用できる選択肢をコード上で明示できる

  • メソッドが受け取る値の意味を分かりやすくできる

  • switch文との相性がよい

  • 一覧取得や文字列変換などの共通操作を利用できる

  • 値の変更箇所を列挙型の定義に集約できる

1-4. 列挙型が適しているケース

列挙型は、選択肢があらかじめ限定されており、頻繁には変化しない値を扱う場合に適しています。

代表的な例は次のとおりです。

public enum PaymentMethod{CreditCard,BankTransfer,CashOnDelivery}
public enum LogLevel{Debug,Information,Warning,Error,Critical}
public enum Direction{Up,Down,Left,Right}

具体的には、次のような用途で利用できます。

  • 注文や申請のステータス

  • 曜日や方向

  • ログレベル

  • 支払い方法

  • ユーザー種別

  • 画面モード

  • 処理結果

  • 権限の組み合わせ

一方、データベースで利用者が自由に追加できるカテゴリや、頻繁に種類が増減する商品区分などには、列挙型が向かないことがあります。そのような値は、テーブルや設定ファイル、クラスなどで管理する方法を検討しましょう。

2. C#で列挙型を定義する基本構文

2-1. enumキーワードを使った定義方法

列挙型は、enumキーワードの後ろに型名を記述して定義します。

アクセス修飾子 enum 列挙型名{メンバー名1,メンバー名2,メンバー名3}

たとえば、天気を表す列挙型は次のように定義できます。

public enum Weather{Sunny,Cloudy,Rainy,Snowy}

列挙型名やメンバー名には、通常、パスカルケースを使用します。

public enum FileStatus{NotFound,Reading,Completed}

C#では、列挙型のメンバーにアクセスするときに「列挙型名.メンバー名」と記述します。

Weather today = Weather.Sunny;

列挙型自体にpublicinternalなどのアクセス修飾子を指定することも可能です。名前空間の直下に定義した列挙型でアクセス修飾子を省略すると、既定ではinternalになります。

2-2. 列挙型の値を変数に代入する方法

列挙型の変数は、通常の変数と同じように宣言できます。

Weather weather = Weather.Cloudy;

宣言と代入を分けることも可能です。

Weather weather;weather = Weather.Rainy;

値を後から変更することもできます。

Weather weather = Weather.Sunny;weather = Weather.Snowy;

型推論を利用する場合は、varも使用できます。

var weather = Weather.Sunny;

この場合、weatherの型はコンパイラによってWeatherと推論されます。

メソッドの戻り値として受け取ることも可能です。

Weather weather = GetWeather();

static Weather GetWeather(){return Weather.Cloudy;}

なお、値を代入していないローカル変数は、そのまま使用できません。

Weather weather;Console.WriteLine(weather); // コンパイルエラー

一方、クラスのフィールドや配列要素などは、列挙型の既定値である0で初期化されます。

2-3. 列挙型のメンバーに数値を指定する方法

列挙型の各メンバーには、整数値を明示的に割り当てられます。

public enum OrderStatus{Pending = 0,Processing = 10,Shipped = 20,Completed = 30,Canceled = 40}

値を指定したメンバーの次に数値指定のないメンバーがある場合、直前の値に1を加えた値が割り当てられます。

public enum Sample{First = 10,Second,      // 11Third = 20,Fourth       // 21}

同じ数値を複数のメンバーに割り当てることも、言語仕様上は可能です。

public enum ResultCode{Success = 0,Ok = 0,Error = 1}

ただし、同じ数値に複数の名前を割り当てると、文字列変換時にどの名前が取得されるか分かりにくくなります。別名が必要な明確な理由がない限り、値の重複は避けたほうが安全です。

2-4. 列挙型の基になる整数型を変更する方法

列挙型の基になる型は、既定ではintです。

public enum Status{None,Active,Inactive}

基になる型を変更するときは、列挙型名の後ろにコロンと整数型を指定します。

public enum Status : byte{None = 0,Active = 1,Inactive = 2}

列挙型の基になる型として使用できるのは、次の整数型です。

  • byte

  • sbyte

  • short

  • ushort

  • int

  • uint

  • long

  • ulong

たとえば、非常に大きな値を扱う場合はlongulongを指定できます。

public enum LargeCode : long{First = 1L,Second = 10_000_000_000L}

小さな値しか扱わない場合にbyteを指定すると、保存時のデータサイズを小さくできる場合があります。ただし、一般的なアプリケーションでは、特別な理由がなければ既定のintを使用して問題ありません。

2-5. 別ファイルやクラス内に定義する際の考え方

列挙型は、名前空間の直下やクラス、構造体の内部に定義できます。

複数のクラスから使用する列挙型は、独立したファイルに定義すると管理しやすくなります。

namespace SampleApp.Models;

public enum OrderStatus{Pending,Processing,Shipped,Completed,Canceled}

ファイル名は、通常、列挙型名に合わせてOrderStatus.csとします。

特定のクラスでしか使用しない列挙型は、クラス内に定義する方法もあります。

public class Document{public enum DocumentStatus{Draft,Published,Archived}

public DocumentStatus Status { get; set; }

}

クラス外から参照するときは、次のように記述します。

Document.DocumentStatus status = Document.DocumentStatus.Draft;

ただし、入れ子にすると型名が長くなります。複数の場所で使用する可能性がある列挙型は、独立した型として定義したほうが再利用しやすくなります。

判断の目安は次のとおりです。

  • 複数のクラスで使う場合は独立したファイルに定義する

  • 特定のクラスの実装詳細である場合はクラス内への定義を検討する

  • ドメイン上の重要な概念である場合は独立した公開型にする

  • 外部に公開する必要がない場合はinternalprivateを使う

3. C#の列挙型を条件分岐で使う方法

3-1. if文で列挙型を比較する方法

列挙型の値は、==演算子や!=演算子を使って比較できます。

OrderStatus status = OrderStatus.Completed;

if (status == OrderStatus.Completed){Console.WriteLine("注文は完了しています");}

一致しないことを確認する場合は、!=を使用します。

if (status != OrderStatus.Canceled){Console.WriteLine("キャンセルされていません");}

複数の値を条件に含めることも可能です。

if (status == OrderStatus.Shipped ||status == OrderStatus.Completed){Console.WriteLine("発送処理は終了しています");}

値の候補が少ない場合はif文でも読みやすく書けますが、分岐が増える場合はswitch文やswitch式を使うと整理しやすくなります。

3-2. switch文で列挙型を分岐する方法

列挙型は、switch文との相性がよい型です。

OrderStatus status = OrderStatus.Shipped;

switch (status){case OrderStatus.Pending:Console.WriteLine("注文を受け付けました");break;

case OrderStatus.Processing:Console.WriteLine("注文を処理しています");break;case OrderStatus.Shipped:Console.WriteLine("商品を発送しました");break;case OrderStatus.Completed:Console.WriteLine("注文が完了しました");break;case OrderStatus.Canceled:Console.WriteLine("注文はキャンセルされました");break;default:Console.WriteLine("不明な状態です");break;

}

caseにメンバー名を記述するため、数値で分岐する場合よりも処理内容を理解しやすくなります。

列挙型には未定義の数値が入る可能性があるため、外部入力やデータベースから取得した値を扱う場合は、defaultも用意しておくと安全です。

同じ処理を行う値は、複数のcaseをまとめられます。

switch (status){case OrderStatus.Pending:case OrderStatus.Processing:Console.WriteLine("注文処理中です");break;

case OrderStatus.Shipped:case OrderStatus.Completed:Console.WriteLine("発送後の状態です");break;default:Console.WriteLine("その他の状態です");break;

}

3-3. switch式で列挙型を扱う方法

C#のswitch式を使うと、列挙型に応じた値を簡潔に返せます。

OrderStatus status = OrderStatus.Processing;

string message = status switch{OrderStatus.Pending => "受付待ち",OrderStatus.Processing => "処理中",OrderStatus.Shipped => "発送済み",OrderStatus.Completed => "完了",OrderStatus.Canceled => "キャンセル",_ => "不明"};

Console.WriteLine(message);

switch式は、列挙型から表示文字列や数値、別のオブジェクトへ変換するときに便利です。

メソッドと組み合わせると、次のように記述できます。

static string GetStatusText(OrderStatus status){return status switch{OrderStatus.Pending => "受付待ち",OrderStatus.Processing => "処理中",OrderStatus.Shipped => "発送済み",OrderStatus.Completed => "完了",OrderStatus.Canceled => "キャンセル",_ => throw new ArgumentOutOfRangeException(nameof(status),status,"未定義の注文状態です")};}

不正な値を許容したくない場合は、ワイルドカードので例外を発生させる方法があります。

列挙型の全メンバーを列挙していても、未定義の整数値が入る可能性は残ります。そのため、外部から値を受け取る処理では、によるフォールバックを用意しておくと堅牢です。

3-4. 列挙型をメソッドの引数や戻り値に使う方法

列挙型は、メソッドの引数として使用できます。

static void UpdateStatus(OrderStatus status){Console.WriteLine($"状態を{status}に変更します");}

呼び出し側は、次のようにメンバーを指定します。

UpdateStatus(OrderStatus.Shipped);

引数をstringintにするより、メソッドが受け付ける値の種類を明確にできます。

戻り値として使用することも可能です。

static OrderStatus GetInitialStatus(){return OrderStatus.Pending;}

次のように、処理結果を列挙型で表す設計もよく使われます。

public enum SaveResult{Success,ValidationError,NotFound,DatabaseError}

static SaveResult SaveData(string value){if (string.IsNullOrWhiteSpace(value)){return SaveResult.ValidationError;}

// 保存処理return SaveResult.Success;

}

呼び出し側では、結果に応じて処理を分岐できます。

SaveResult result = SaveData("サンプル");

switch (result){case SaveResult.Success:Console.WriteLine("保存しました");break;

case SaveResult.ValidationError:Console.WriteLine("入力内容を確認してください");break;case SaveResult.NotFound:Console.WriteLine("対象データが見つかりません");break;case SaveResult.DatabaseError:Console.WriteLine("データベースエラーが発生しました");break;

}

ただし、エラーの詳細情報や複数のデータを返したい場合は、列挙型だけでなく、専用の結果クラスやレコードを使う方法も検討しましょう。

4. 列挙型と文字列・数値を相互変換する方法

4-1. 列挙型を文字列に変換するToStringメソッド

列挙型の値を文字列に変換するには、ToStringメソッドを使用します。

OrderStatus status = OrderStatus.Shipped;

string text = status.ToString();

Console.WriteLine(text); // Shipped

ToStringで得られるのは、通常、コード上のメンバー名です。日本語の表示名が自動的に返されるわけではありません。

値に対応するメンバー名が定義されていない場合は、数値が文字列として返されます。

OrderStatus status = (OrderStatus)999;

Console.WriteLine(status.ToString()); // 999

書式指定文字列を使うこともできます。

Console.WriteLine(status.ToString("G"));Console.WriteLine(status.ToString("D"));Console.WriteLine(status.ToString("X"));

代表的な書式は次のとおりです。

  • G:メンバー名。名前がなければ数値

  • D:10進数

  • X:16進数

  • F:フラグとして可能な限り名前を組み合わせて表示

通常の用途では、引数なしのToString()で十分です。

4-2. 文字列を列挙型に変換するEnum.Parseメソッド

文字列を列挙型へ変換するには、Enum.Parseを使用できます。

string text = "Shipped";

OrderStatus status = Enum.Parse<OrderStatus>(text);

Console.WriteLine(status); // Shipped

型を引数で指定する非ジェネリック版もあります。

OrderStatus status =(OrderStatus)Enum.Parse(typeof(OrderStatus), "Shipped");

ただし、変換できない文字列を渡すと例外が発生します。

OrderStatus status =Enum.Parse<OrderStatus>("UnknownStatus");

この場合、ArgumentExceptionが発生します。

入力内容が必ず正しいと保証できる場合にはEnum.Parseを使えますが、ユーザー入力や外部データを扱う場合は、例外を使わず判定できるEnum.TryParseが適しています。

4-3. Enum.TryParseで安全に変換する方法

Enum.TryParseを使うと、例外を発生させずに変換の成否を確認できます。

string text = "Completed";

if (Enum.TryParse<OrderStatus>(text, out OrderStatus status)){Console.WriteLine($"変換成功: {status}");}else{Console.WriteLine("変換できませんでした");}

変換に成功するとtrueが返り、out引数に変換後の値が設定されます。失敗した場合はfalseが返ります。

変数の型を省略して、次のように書くことも可能です。

if (Enum.TryParse<OrderStatus>("Shipped", out var status)){Console.WriteLine(status);}

ただし、TryParseが成功したからといって、必ず列挙型に定義済みのメンバーであるとは限りません。数値形式の文字列は、未定義の数値でも変換できる場合があります。

bool success =Enum.TryParse<OrderStatus>("999", out var status);

Console.WriteLine(success); // TrueConsole.WriteLine(status); // 999

定義済みの値だけを受け付けたい場合は、Enum.IsDefinedも組み合わせます。

if (Enum.TryParse<OrderStatus>(text, out var status) &&Enum.IsDefined(status)){Console.WriteLine($"有効な値です: {status}");}else{Console.WriteLine("無効な値です");}

4-4. 列挙型を整数に変換する方法

列挙型を整数に変換するには、キャストを使用します。

OrderStatus status = OrderStatus.Shipped;

int number = (int)status;

Console.WriteLine(number);

基になる型をbyteにしている場合は、byteへキャストします。

public enum Priority : byte{Low = 1,Medium = 2,High = 3}

Priority priority = Priority.High;byte number = (byte)priority;

基になる型を意識せず、汎用的に変換したい場合はConvertクラスも利用できます。

int number = Convert.ToInt32(status);

ただし、基になる型がlongulongであり、値がintの範囲を超える可能性がある場合は、適切な変換メソッドを選ぶ必要があります。

long number = Convert.ToInt64(status);

4-5. 整数を列挙型に変換する方法

整数を列挙型へ変換する場合も、キャストを使用します。

int number = 2;

OrderStatus status = (OrderStatus)number;

Console.WriteLine(status);

ただし、列挙型に定義されていない数値もキャストできます。

OrderStatus status = (OrderStatus)999;

Console.WriteLine(status); // 999

キャスト自体では、値が定義済みかどうかは検証されません。安全に変換するには、Enum.IsDefinedを使います。

int number = 2;

if (Enum.IsDefined(typeof(OrderStatus), number)){OrderStatus status = (OrderStatus)number;Console.WriteLine(status);}else{Console.WriteLine("定義されていない値です");}

ジェネリック版を利用できる環境では、次のようにも記述できます。

OrderStatus status = (OrderStatus)number;

if (Enum.IsDefined(status)){Console.WriteLine(status);}

4-6. 大文字・小文字を区別せずに変換する方法

Enum.ParseEnum.TryParseは、既定では大文字と小文字を区別します。

Enum.TryParse<OrderStatus>("shipped", out var status);

Shippedに対してshippedを渡すと、既定の設定では変換に失敗します。

大文字・小文字を区別しない場合は、ignoreCasetrueを指定します。

if (Enum.TryParse<OrderStatus>("shipped",ignoreCase: true,out var status)){Console.WriteLine(status); // Shipped}

Enum.Parseでも同様です。

OrderStatus status =Enum.Parse<OrderStatus>("completed", ignoreCase: true);

Web APIのクエリ文字列や設定ファイルなど、大文字・小文字の揺れが発生する入力では便利です。

ただし、表記ルールを厳密に管理したい場合は、あえて大文字・小文字を区別する設計も考えられます。

4-7. 不正な値を変換するときの注意点

列挙型の変換では、次の3つを区別する必要があります。

  • 変換できない文字列

  • 変換はできるが未定義の数値

  • Flags列挙型の有効な組み合わせ

たとえば、次の文字列はメンバー名でも数値でもないため、変換できません。

Enum.TryParse<OrderStatus>("abc", out var status);

一方、"999"は数値として解釈できるため、TryParseが成功する可能性があります。

Enum.TryParse<OrderStatus>("999", out var status);

定義済みの通常の列挙値だけを許可する場合は、次のように検証します。

static bool TryParseDefinedStatus(string input,out OrderStatus status){return Enum.TryParse(input,ignoreCase: true,out status)&& Enum.IsDefined(status);}

Flags属性を付けた列挙型では、複数の値を組み合わせた数値が、個別のメンバーとして定義されていないことがあります。そのため、Enum.IsDefinedだけでは、有効な組み合わせを正しく判定できません。

通常の列挙型とFlags列挙型では、検証方法を分けて考える必要があります。

5. Enumクラスを使った便利な操作

5-1. Enum.GetValuesで列挙型の値を一覧取得する方法

Enum.GetValuesを使うと、列挙型に定義されている値を一覧で取得できます。

Array values = Enum.GetValues(typeof(OrderStatus));

foreach (OrderStatus status in values){Console.WriteLine(status);}

実行すると、定義されている各メンバーが順番に出力されます。

PendingProcessingShippedCompletedCanceled

取得結果は、値の数値的な大きさを基準に並びます。ソースコード上の記述順と常に同じになるとは限らないため、表示順を厳密に管理したい場合は、数値を明示的に割り当てるか、別途並び順を定義しましょう。

同じ数値を持つメンバーが複数ある場合、取得結果にも重複した値が含まれます。

5-2. Enum.GetNamesでメンバー名を一覧取得する方法

Enum.GetNamesを使うと、メンバー名を文字列配列として取得できます。

string[] names = Enum.GetNames(typeof(OrderStatus));

foreach (string name in names){Console.WriteLine(name);}

入力候補やログ出力、簡易的な選択肢の生成などに利用できます。

string[] statusNames = Enum.GetNames(typeof(OrderStatus));

Console.WriteLine(string.Join(", ", statusNames));

ただし、取得できるのはコード上のメンバー名です。利用者向けの日本語表示が必要な場合は、Dictionaryや属性を使って表示名を管理します。

5-3. Enum.IsDefinedで有効な値か確認する方法

Enum.IsDefinedを使うと、指定した名前または値が列挙型に定義されているか確認できます。

bool result =Enum.IsDefined(typeof(OrderStatus), OrderStatus.Shipped);

Console.WriteLine(result); // True

整数値も確認できます。

bool result =Enum.IsDefined(typeof(OrderStatus), 999);

Console.WriteLine(result); // False

文字列を指定した場合は、メンバー名が完全一致するか確認されます。

bool result =Enum.IsDefined(typeof(OrderStatus), "Shipped");

Console.WriteLine(result); // True

大文字・小文字は区別されます。

bool result =Enum.IsDefined(typeof(OrderStatus), "shipped");

Console.WriteLine(result); // False

Flags列挙型では、個別に名前が定義されていない組み合わせに対してfalseになる点に注意してください。

5-4. foreachで列挙型の全要素を処理する方法

Enum.GetValuesforeachを組み合わせると、列挙型の全要素を順番に処理できます。

foreach (OrderStatus statusin Enum.GetValues(typeof(OrderStatus))){Console.WriteLine($"名前: {status}, 数値: {(int)status}");}

選択肢を作る例は次のとおりです。

foreach (OrderStatus statusin Enum.GetValues(typeof(OrderStatus))){string label = status switch{OrderStatus.Pending => "受付待ち",OrderStatus.Processing => "処理中",OrderStatus.Shipped => "発送済み",OrderStatus.Completed => "完了",OrderStatus.Canceled => "キャンセル",_ => "不明"};

Console.WriteLine($"{(int)status}: {label}");

}

列挙型のメンバーを追加すると、一覧処理にも自動的に含まれる点が便利です。

ただし、画面への表示順や表示対象を厳密に管理する場合は、すべての値を自動表示するより、表示用の一覧を明示的に定義したほうが安全なこともあります。

5-5. ジェネリック版Enum.GetValuesを使う方法

近年の.NETでは、ジェネリック版のEnum.GetValues<TEnum>()を使用できます。

OrderStatus[] values =Enum.GetValues<OrderStatus>();

foreach (OrderStatus status in values){Console.WriteLine(status);}

従来の書き方と比較すると、キャストが不要で、取得結果の型も明確です。

foreach (OrderStatus statusin Enum.GetValues(typeof(OrderStatus))){Console.WriteLine(status);}
foreach (OrderStatus statusin Enum.GetValues<OrderStatus>()){Console.WriteLine(status);}

メンバー名は、Enum.GetNames<TEnum>()で取得できます。

string[] names = Enum.GetNames<OrderStatus>();

利用している.NETのバージョンでジェネリック版を使用できる場合は、型安全で簡潔な書き方として優先するとよいでしょう。

6. Flags属性で複数の値を組み合わせる方法

6-1. Flags属性とは

通常の列挙型は、複数の選択肢から1つの値を表すために使います。

一方、複数の値を同時に保持したい場合は、Flags属性を付けた列挙型を使用できます。

たとえば、ユーザーが次の権限を複数持つケースを考えます。

  • 閲覧権限

  • 作成権限

  • 編集権限

  • 削除権限

Flags列挙型を使えば、「閲覧と編集の両方を許可する」といった組み合わせを1つの値として表現できます。

[Flags]public enum Permission{None = 0,Read = 1,Create = 2,Update = 4,Delete = 8}

Flags属性は、列挙型がビットフラグとして設計されていることを示します。

6-2. ビットフラグ用の列挙型を定義する方法

ビットフラグ用の列挙型では、各メンバーに2の累乗を割り当てます。

[Flags]public enum Permission{None = 0,      // 0000Read = 1,      // 0001Create = 2,    // 0010Update = 4,    // 0100Delete = 8     // 1000}

2の累乗を使うことで、各値が異なるビットを表します。

2進数で見ると、次のようになります。

Read   = 0001Create = 0010Update = 0100Delete = 1000

組み合わせを表すメンバーも定義できます。

[Flags]public enum Permission{None = 0,Read = 1 << 0,Create = 1 << 1,Update = 1 << 2,Delete = 1 << 3,

Write = Create | Update,All = Read | Create | Update | Delete

}

ビットシフト演算子を使うと、どのビットを利用しているか分かりやすくなります。

6-3. ビットOR演算子で複数の値を設定する方法

複数のフラグを組み合わせるには、ビットOR演算子の|を使用します。

Permission permission =Permission.Read | Permission.Update;

この値には、ReadUpdateの両方が含まれています。

Console.WriteLine(permission);

Flags属性が付いていれば、通常は次のように名前の組み合わせが表示されます。

Read, Update

3つ以上の値も組み合わせられます。

Permission permission =Permission.Read |Permission.Create |Permission.Update;

事前に定義した組み合わせも使用できます。

Permission permission = Permission.All;

6-4. HasFlagメソッドで値を確認する方法

指定したフラグが含まれているか確認するには、HasFlagメソッドを使用できます。

Permission permission =Permission.Read | Permission.Update;

if (permission.HasFlag(Permission.Read)){Console.WriteLine("閲覧できます");}

含まれていないフラグを確認すると、falseになります。

bool canDelete =permission.HasFlag(Permission.Delete);

Console.WriteLine(canDelete); // False

複数のフラグをまとめて確認することも可能です。

Permission required =Permission.Read | Permission.Update;

if (permission.HasFlag(required)){Console.WriteLine("閲覧権限と更新権限があります");}

ただし、HasFlag(Permission.None)は常にtrueになります。Noneを特別な状態として判定したい場合は、等価比較を使用します。

if (permission == Permission.None){Console.WriteLine("権限がありません");}

6-5. ビットAND演算子で値を判定する方法

ビットAND演算子の&を使って、フラグが含まれているか判定することもできます。

Permission permission =Permission.Read | Permission.Update;

if ((permission & Permission.Read) == Permission.Read){Console.WriteLine("閲覧できます");}

複数のフラグがすべて含まれることを確認する場合は、次のように記述します。

Permission required =Permission.Read | Permission.Update;

if ((permission & required) == required){Console.WriteLine("必要な権限をすべて持っています");}

いずれか1つでも含まれているか確認する場合は、結果がNone以外かを判定します。

Permission target =Permission.Update | Permission.Delete;

if ((permission & target) != Permission.None){Console.WriteLine("更新または削除の権限があります");}

HasFlagは意図が分かりやすく、ビット演算は細かな条件を表現しやすいという特徴があります。

6-6. フラグを追加・削除・反転する方法

フラグを追加するには、|を使用します。

Permission permission = Permission.Read;

permission |= Permission.Update;

この結果、permissionにはReadUpdateが含まれます。

フラグを削除するには、ビットAND演算子&とビット反転演算子~を組み合わせます。

permission &= ~Permission.Update;

指定したフラグの有無を切り替えるには、排他的OR演算子の^を使用します。

permission ^= Permission.Read;

Readが含まれていれば削除され、含まれていなければ追加されます。

すべてのフラグを解除する場合は、Noneを代入します。

permission = Permission.None;

複数のフラグを一度に削除することも可能です。

permission &=~(Permission.Create | Permission.Update);

6-7. Flags属性を使う際の値の決め方と注意点

Flags列挙型では、各基本メンバーに2の累乗を割り当てます。

[Flags]public enum Feature{None = 0,FeatureA = 1,FeatureB = 2,FeatureC = 4,FeatureD = 8}

次のように連番を割り当ててはいけません。

[Flags]public enum InvalidFeature{None = 0,FeatureA = 1,FeatureB = 2,FeatureC = 3, // 1と2の組み合わせになるFeatureD = 4}

3は2進数で0011なので、FeatureAFeatureBを組み合わせた値と同じです。独立したフラグとして扱えません。

また、次の点にも注意しましょう。

  • None = 0を定義する

  • 基本フラグには2の累乗を割り当てる

  • 組み合わせメンバーは基本フラグのORで定義する

  • 将来の追加に備えて基になる整数型の範囲を確認する

  • ~で反転した値には未使用ビットが含まれる可能性がある

  • 外部入力は有効なビットだけで構成されているか検証する

有効なビット以外が含まれていないか確認する例は次のとおりです。

const Permission allDefined =Permission.Read |Permission.Create |Permission.Update |Permission.Delete;

Permission value = (Permission)31;

bool isValid = (value & ~allDefined) == 0;

7. C#の列挙型を活用する実践例

7-1. 曜日や状態を列挙型で管理する例

アプリケーション独自の曜日区分を管理する例です。

public enum BusinessDayType{Weekday,Saturday,Sunday,Holiday}

判定結果を列挙型で返します。

static BusinessDayType GetDayType(DayOfWeek dayOfWeek,bool isHoliday){if (isHoliday){return BusinessDayType.Holiday;}

return dayOfWeek switch{DayOfWeek.Saturday =&gt; BusinessDayType.Saturday,DayOfWeek.Sunday =&gt; BusinessDayType.Sunday,_ =&gt; BusinessDayType.Weekday};

}

呼び出し側は、戻り値に応じて営業時間などを切り替えられます。

BusinessDayType dayType =GetDayType(DateTime.Today.DayOfWeek, isHoliday: false);

string openingHours = dayType switch{BusinessDayType.Weekday => "9:00~18:00",BusinessDayType.Saturday => "10:00~15:00",BusinessDayType.Sunday => "休業",BusinessDayType.Holiday => "休業",_ => "不明"};

C#には標準のDayOfWeek列挙型も用意されています。標準型で要件を満たせる場合は、独自の列挙型を作る前に既存の型を確認しましょう。

7-2. 注文・申請ステータスを管理する例

注文状態を管理する列挙型を定義します。

public enum OrderStatus{Pending = 0,Confirmed = 10,Preparing = 20,Shipped = 30,Completed = 40,Canceled = 90}

注文クラスのプロパティとして使用できます。

public class Order{public int Id { get; init; }

public OrderStatus Status { get; private set; }= OrderStatus.Pending;public void Confirm(){if (Status != OrderStatus.Pending){throw new InvalidOperationException("受付待ちの注文だけを確定できます。");}Status = OrderStatus.Confirmed;}public void Cancel(){if (Status is OrderStatus.Shippedor OrderStatus.Completed){throw new InvalidOperationException("発送後の注文はキャンセルできません。");}Status = OrderStatus.Canceled;}

}

列挙型で状態を表すだけでなく、状態変更のルールをクラス内に実装すると、不正な遷移を防ぎやすくなります。

申請処理にも同じ考え方を利用できます。

public enum ApplicationStatus{Draft,Submitted,Reviewing,Approved,Rejected,Withdrawn}

ただし、状態遷移が非常に複雑な場合は、列挙型と条件分岐だけで管理するとコードが肥大化します。その場合は、ステートパターンや専用の状態管理クラスも検討しましょう。

7-3. ユーザー権限をFlags属性で管理する例

ユーザー権限をFlags列挙型で管理します。

[Flags]public enum UserPermission{None = 0,View = 1 << 0,Create = 1 << 1,Edit = 1 << 2,Delete = 1 << 3,ManageUsers = 1 << 4,

Editor = View | Create | Edit,Administrator =View | Create | Edit | Delete | ManageUsers

}

ユーザークラスに権限を保持します。

public class User{public string Name { get; init; } = string.Empty;

public UserPermission Permissions { get; set; }

}

権限を設定します。

var user = new User{Name = "山田",Permissions =UserPermission.View |UserPermission.Edit};

権限を確認します。

if (user.Permissions.HasFlag(UserPermission.Edit)){Console.WriteLine("編集できます");}

必要な権限がすべてそろっているか確認するメソッドを作ることもできます。

static bool HasAllPermissions(User user,UserPermission required){return (user.Permissions & required) == required;}
bool allowed = HasAllPermissions(user,UserPermission.View | UserPermission.Edit);

なお、権限管理では、列挙型の判定だけに頼らず、サーバー側で必ず認可処理を実行する必要があります。画面上のボタンを非表示にするだけでは、セキュリティ対策として不十分です。

7-4. UIの選択肢に列挙型を表示する例

列挙型の値を、画面のドロップダウンやラジオボタンの選択肢として利用できます。

コンソールに一覧表示する単純な例は次のとおりです。

foreach (OrderStatus statusin Enum.GetValues<OrderStatus>()){Console.WriteLine($"{(int)status}: {status}");}

ASP.NET Core MVCでは、列挙型から選択肢を生成する機能も利用できます。

モデルに列挙型のプロパティを定義します。

public class OrderSearchViewModel{public OrderStatus? Status { get; set; }}

Razorビューでは、列挙型から選択肢を生成できます。

<select asp-for="Status"asp-items="Html.GetEnumSelectList<OrderStatus>()"><option value="">すべて</option></select>

ただし、メンバー名をそのまま画面に表示すると、英語の識別子が利用者に見えることがあります。日本語の表示名が必要な場合は、Display属性などを組み合わせます。

7-5. Dictionaryと組み合わせて表示名を管理する例

列挙型のメンバー名と画面上の表示名を分ける方法として、Dictionaryを利用できます。

public enum OrderStatus{Pending,Processing,Shipped,Completed,Canceled}

表示名を辞書に定義します。

private static readonly IReadOnlyDictionary<OrderStatus,string> StatusLabels =new Dictionary<OrderStatus, string>{[OrderStatus.Pending] = "受付待ち",[OrderStatus.Processing] = "処理中",[OrderStatus.Shipped] = "発送済み",[OrderStatus.Completed] = "完了",[OrderStatus.Canceled] = "キャンセル"};

表示名を取得します。

OrderStatus status = OrderStatus.Shipped;

string label = StatusLabels[status];

Console.WriteLine(label); // 発送済み

未定義値も考慮する場合は、TryGetValueを使います。

static string GetStatusLabel(OrderStatus status){return StatusLabels.TryGetValue(status, out string? label)? label: "不明";}

Dictionaryを使う方法には、処理が分かりやすく、リフレクションが不要という利点があります。

一方、列挙型にメンバーを追加した際、辞書への追加を忘れる可能性があります。単体テストで、すべてのメンバーに表示名が登録されていることを確認すると安全です。

7-6. 属性を使って日本語の表示名を設定する例

System.ComponentModel.DataAnnotationsDisplay属性を使うと、各メンバーに表示名を設定できます。

using System.ComponentModel.DataAnnotations;

public enum OrderStatus{[Display(Name = "受付待ち")]Pending,

<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[Display(Name = "処理中")]</span>Processing,<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[Display(Name = "発送済み")]</span>Shipped,<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[Display(Name = "完了")]</span>Completed,<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[Display(Name = "キャンセル")]</span>Canceled

}

属性から表示名を取得する拡張メソッドを作成します。

using System.ComponentModel.DataAnnotations;using System.Reflection;

public static class EnumExtensions{public static string GetDisplayName<TEnum>(this TEnum value)where TEnum : struct, Enum{string memberName = value.ToString();

    MemberInfo? member = typeof(TEnum).GetMember(memberName).FirstOrDefault();DisplayAttribute? attribute = member?.GetCustomAttribute&lt;DisplayAttribute&gt;();return attribute?.GetName() ?? memberName;}

}

次のように利用できます。

OrderStatus status = OrderStatus.Shipped;

Console.WriteLine(status.GetDisplayName());// 発送済み

属性を使うと、表示名を列挙型の定義にまとめられます。ASP.NET Core MVCなどのフレームワーク機能と連携しやすい点も利点です。

ただし、属性の取得にはリフレクションを使用します。大量に繰り返し呼び出す場合は、取得結果をキャッシュする設計を検討しましょう。

また、多言語対応が必要な場合は、Display属性のリソース機能や、別のローカライズ基盤を利用します。

8. 列挙型を使うときの注意点

8-1. 定義されていない数値も代入できる

C#の列挙型には、キャストによって未定義の数値を代入できます。

OrderStatus status = (OrderStatus)999;

このコードはコンパイルでき、実行時にもキャストだけでは例外になりません。

メソッドの引数が列挙型であっても、呼び出し側がキャストすれば未定義値を渡せます。

UpdateStatus((OrderStatus)999);

そのため、外部入力やデータベースから値を受け取る境界では、値を検証する必要があります。

static void UpdateStatus(OrderStatus status){if (!Enum.IsDefined(status)){throw new ArgumentOutOfRangeException(nameof(status),status,"未定義の状態です。");}

// 更新処理

}

Flags列挙型では、Enum.IsDefinedだけで組み合わせの妥当性を判断できないため、有効なビットの範囲を検証します。

8-2. 先頭メンバーを0にするべき理由

列挙型の既定値は0です。

たとえば、クラスの列挙型プロパティを明示的に初期化しなかった場合、値は0になります。

public class Order{public OrderStatus Status { get; set; }}
var order = new Order();

Console.WriteLine((int)order.Status); // 0

そのため、先頭メンバーやNoneUnknownなどに0を割り当てる設計が一般的です。

public enum OrderStatus{Unknown = 0,Pending = 1,Processing = 2,Completed = 3}

有効な初期状態が明確な場合は、その状態を0にしても構いません。

public enum OrderStatus{Pending = 0,Processing = 1,Completed = 2}

一方、0に対応するメンバーが存在しないと、初期化直後から未定義値になります。

public enum OrderStatus{Pending = 1,Processing = 2,Completed = 3}

どの状態を0にするかは、「未設定を表したいか」「自然な初期状態があるか」を基準に決めましょう。

8-3. メンバーの追加・削除・並び替えによる影響

数値を明示せずに列挙型を定義すると、記述順に応じて値が割り当てられます。

public enum Status{Pending,    // 0Processing, // 1Completed   // 2}

途中にメンバーを追加すると、後ろの数値が変わります。

public enum Status{Pending,    // 0Confirmed,  // 1Processing, // 2Completed   // 3}

以前はProcessing1Completed2でしたが、追加後は値が変わっています。

列挙値をデータベースやファイルに数値として保存していた場合、既存データの意味が変わってしまいます。

外部に数値を保存する列挙型では、値を明示的に固定しましょう。

public enum Status{Pending = 0,Processing = 10,Completed = 20,Confirmed = 30}

削除した値を別の用途で再利用することも避けるべきです。過去データに古い数値が残っていると、新しい意味として誤って解釈される可能性があります。

8-4. データベースやファイルに数値を保存する際の注意点

列挙型を数値として永続化すると、データ量を抑えやすく、比較も効率的です。一方、データだけを見ても意味が分かりにくく、数値変更の影響を受けます。

数値で保存する場合は、次の対策が重要です。

  • 各メンバーの数値を明示する

  • 既存メンバーの数値を変更しない

  • 削除した数値を別の意味で再利用しない

  • 未定義値を読み込んだ場合の処理を決める

  • データベース制約も必要に応じて設定する

文字列で保存する方法もあります。

PendingProcessingCompleted

文字列は意味を確認しやすい反面、メンバー名の変更が保存データに影響します。また、数値よりも保存サイズが大きくなる傾向があります。

数値と文字列のどちらを選ぶ場合も、「一度保存した識別値を安易に変更しない」ことが重要です。

8-5. JSONでシリアライズ・デシリアライズする際の注意点

System.Text.Jsonで列挙型をそのままシリアライズすると、既定では数値として出力されます。

using System.Text.Json;

var data = new{Status = OrderStatus.Shipped};

string json = JsonSerializer.Serialize(data);

Console.WriteLine(json);

出力例は次のようになります。

{"Status":2}

文字列として出力したい場合は、JsonStringEnumConverterを追加します。

using System.Text.Json;using System.Text.Json.Serialization;

var options = new JsonSerializerOptions();options.Converters.Add(new JsonStringEnumConverter());

string json = JsonSerializer.Serialize(data, options);

出力例は次のとおりです。

{"Status":"Shipped"}

プロパティや列挙型に属性を付ける方法もあります。

[JsonConverter(typeof(JsonStringEnumConverter))]public enum OrderStatus{Pending,Processing,Shipped,Completed,Canceled}

文字列形式には可読性が高い利点がありますが、メンバー名を変更するとAPIの互換性に影響します。外部公開APIでは、コード上のリファクタリングが通信仕様の変更につながらないよう注意しましょう。

また、既定設定では数値の列挙値を受け付ける構成になる場合があります。数値を許可したくない場合は、コンバーターの設定を確認します。

var options = new JsonSerializerOptions();options.Converters.Add(new JsonStringEnumConverter(namingPolicy: null,allowIntegerValues: false));

外部から受け取った値については、デシリアライズできたことだけでなく、業務上有効な状態かどうかも検証しましょう。

8-6. 列挙型に処理や複雑なデータを持たせられない

C#の列挙型は、名前付きの整数定数をまとめる型です。クラスのように、インスタンスフィールド、コンストラクター、通常のインスタンスメソッドを定義することはできません。

次のような情報を各選択肢に持たせたい場合、列挙型だけでは表現しにくくなります。

  • 日本語名

  • 説明文

  • 表示順

  • 手数料

  • 有効期間

  • 状態ごとに異なる処理

簡単な表示名であれば、switch式、Dictionary、属性、拡張メソッドを組み合わせられます。

public static string GetLabel(this OrderStatus status){return status switch{OrderStatus.Pending => "受付待ち",OrderStatus.Processing => "処理中",OrderStatus.Shipped => "発送済み",OrderStatus.Completed => "完了",OrderStatus.Canceled => "キャンセル",_ => "不明"};}

しかし、データや処理が複雑になる場合は、クラスやレコードを使ったほうが自然です。

8-7. 巨大な列挙型や頻繁に変更される値には向かない

数百件、数千件の値を列挙型に定義すると、コードが巨大になり、変更やレビューが難しくなります。

また、次のような値は列挙型に適さない可能性があります。

  • 管理画面から追加・削除されるカテゴリ

  • 外部サービスから定期的に更新されるコード

  • 国や地域ごとに変化するマスターデータ

  • 有効期限や追加情報を持つ選択肢

  • デプロイせずに変更したい設定値

列挙型を変更するには、通常、ソースコードの修正、ビルド、デプロイが必要です。

頻繁に変更される値は、データベース、設定ファイル、外部マスターなどで管理するほうが柔軟です。列挙型は、アプリケーションのコードと強く結び付き、種類が比較的安定している概念に使用しましょう。

9. 読みやすく保守しやすい列挙型の設計方法

9-1. 列挙型とメンバーの命名規則

C#では、列挙型名とメンバー名にパスカルケースを使うのが一般的です。

public enum OrderStatus{Pending,Processing,Shipped,Completed}

略語を含む場合も、プロジェクトの命名規則に合わせます。

public enum HttpMethodType{Get,Post,Put,Delete}

列挙型名は、通常、単数形にします。

public enum Color{Red,Green,Blue}

Flags属性を付けた列挙型は、複数の値を表すため、複数形が適する場合があります。

[Flags]public enum FilePermissions{None = 0,Read = 1,Write = 2,Execute = 4}

EnumFlagといった接尾辞を常に付ける必要はありません。

// 分かりやすいpublic enum OrderStatus{}

// 型であることを名前に重複して表しているpublic enum OrderStatusEnum{}

メンバー名には、意味が明確な名前を使いましょう。

public enum PaymentStatus{Pending,Authorized,Paid,Refunded,Failed}

Status1TypeAのような抽象的な名前は、意味を理解しにくいため避けます。

9-2. NoneやUnknownを定義する判断基準

NoneUnknownを定義するかどうかは、0がどのような状態を表すべきかで決めます。

値が何も選択されていない状態を表したい場合は、Noneが適しています。

public enum Selection{None = 0,First = 1,Second = 2}

外部から受け取った値を識別できない状態や、未判定の状態を表したい場合は、Unknownが適しています。

public enum DeviceStatus{Unknown = 0,Online = 1,Offline = 2}

自然な初期状態がある場合は、その値を0にする選択肢もあります。

public enum OrderStatus{Pending = 0,Processing = 1,Completed = 2}

一方、業務上「不明」という状態を許容してはいけない場合、Unknownを定義すると不正な状態が長期間残る原因になることがあります。

判断基準は次のとおりです。

  • 未設定状態が業務上存在するならNone

  • 不明な外部値を保持する必要があるならUnknown

  • 明確な初期状態があるなら、その状態を0

  • 不正な状態を早期に検出したいなら、例外や入力検証も利用する

9-3. 数値を明示的に割り当てるべきケース

列挙型をアプリケーション内部だけで一時的に使う場合は、必ずしも各数値を指定する必要はありません。

public enum SortDirection{Ascending,Descending}

一方、次の用途では数値を明示的に割り当てるべきです。

  • データベースに保存する

  • ファイルに保存する

  • APIで数値として送受信する

  • 外部システムのコード値に対応させる

  • 長期間の互換性を維持する必要がある

public enum PaymentStatus{Unknown = 0,Pending = 10,Paid = 20,Refunded = 30,Failed = 90}

値の間隔を空けると、既存値を変えずに中間状態を追加しやすくなる場合があります。

ただし、空き番号を作れば必ず安全というわけではありません。一度公開または保存した数値は、その意味を固定することが重要です。

9-4. Flags属性では2の累乗を割り当てる

Flags列挙型の基本メンバーには、必ず異なるビットを割り当てます。

[Flags]public enum AccessRights{None = 0,Read = 1 << 0,Write = 1 << 1,Execute = 1 << 2,Delete = 1 << 3}

数値で書くと、1248です。

複合値は、基本メンバーを組み合わせて定義します。

[Flags]public enum AccessRights{None = 0,Read = 1 << 0,Write = 1 << 1,Execute = 1 << 2,Delete = 1 << 3,

ReadWrite = Read | Write,All = Read | Write | Execute | Delete

}

基本メンバーに複合値を直接割り当てると、各フラグを独立して管理できません。

利用可能なビット数にも注意が必要です。多数のフラグを定義する場合は、基になる型としてlongulongを使用することを検討します。

[Flags]public enum LargeOptions : ulong{None = 0,Option1 = 1UL << 0,Option2 = 1UL << 1,Option3 = 1UL << 2}

9-5. 列挙型と定数クラスを使い分ける基準

列挙型と定数クラスは、どちらも名前付きの値を定義できますが、用途が異なります。

列挙型は、限定された選択肢を1つの型として表したい場合に適しています。

public enum LogLevel{Debug,Information,Warning,Error}

定数クラスは、型として1つにまとめる必要のない値や、整数以外の定数を管理する場合に適しています。

public static class ApiConstants{public const string Version = "v1";public const string HeaderName = "X-Api-Key";public const int DefaultTimeoutSeconds = 30;}

列挙型の基になる値は整数型に限られます。文字列を値として直接持たせたい場合は、定数を使う方法があります。

public static class ContentTypes{public const string Json = "application/json";public const string Xml = "application/xml";}

使い分けの基準は次のとおりです。

  • 限定された選択肢を型として扱うなら列挙型

  • switchや型付き引数で使うなら列挙型

  • 文字列や小数などの定数なら定数クラス

  • 値同士が同じ型の選択肢ではないなら定数クラス

  • 実行時に決まる値ならstatic readonlyや設定を検討する

9-6. 列挙型とクラス・レコードを使い分ける基準

列挙型は、各値が名前と整数値だけで十分な場合に適しています。

public enum MembershipType{Free,Standard,Premium}

しかし、会員種別ごとに料金、表示名、割引率などを持たせる場合は、クラスやレコードが適しています。

public sealed record MembershipPlan(string Code,string DisplayName,decimal MonthlyPrice,decimal DiscountRate);
public static class MembershipPlans{public static readonly MembershipPlan Free =new(Code: "free",DisplayName: "無料プラン",MonthlyPrice: 0,DiscountRate: 0);

public static readonly MembershipPlan Premium =new(Code: "premium",DisplayName: "プレミアムプラン",MonthlyPrice: 3000,DiscountRate: 0.1m);

}

クラスやレコードが適しているのは、次のようなケースです。

  • 値ごとに複数の情報を持つ

  • 値ごとに処理が異なる

  • 継承やポリモーフィズムを利用したい

  • 実行時に値を追加したい

  • 値の関係や制約が複雑

  • データベースのマスターと連携する

単純な選択肢は列挙型、豊富な情報や振る舞いを持つ概念はクラスやレコード、と考えると判断しやすくなります。

10. C#の列挙型に関するよくある質問

10-1. enumの初期値は何になる?

列挙型の既定値は、数値の0です。

OrderStatus status = default;

Console.WriteLine((int)status); // 0

クラスのフィールドやプロパティ、配列要素も、明示的に初期化しなければ0になります。

OrderStatus[] statuses = new OrderStatus[3];

Console.WriteLine((int)statuses[0]); // 0

ただし、0に対応するメンバーが定義されているとは限りません。

public enum OrderStatus{Pending = 1,Completed = 2}

この場合、既定値の0は未定義値です。意図しない状態を避けるため、0に対応するメンバーを定義するのが一般的です。

10-2. enumに文字列を直接設定できる?

C#の列挙型に、文字列を基になる値として直接設定することはできません。

次のような定義はできません。

public enum Status{Pending = "pending" // コンパイルエラー}

列挙型の基になる型として使用できるのは整数型です。

文字列が必要な場合は、次の方法を使います。

  • ToStringでメンバー名を取得する

  • Display属性などで表示名を設定する

  • Dictionaryで対応付ける

  • switch式や拡張メソッドで変換する

  • 文字列定数を使う

  • クラスやレコードを使う

APIに送る文字列を取得する例は次のとおりです。

static string ToApiValue(OrderStatus status){return status switch{OrderStatus.Pending => "pending",OrderStatus.Processing => "processing",OrderStatus.Shipped => "shipped",OrderStatus.Completed => "completed",OrderStatus.Canceled => "canceled",_ => throw new ArgumentOutOfRangeException(nameof(status))};}

10-3. enumをnullにすることはできる?

列挙型は値型なので、そのままではnullを代入できません。

OrderStatus status = null; // コンパイルエラー

nullを許容したい場合は、Nullable型を使います。

OrderStatus? status = null;

Nullable<T>を明示しても同じ意味です。

Nullable<OrderStatus> status = null;

値があるか確認するには、HasValueを使用できます。

if (status.HasValue){Console.WriteLine(status.Value);}

パターンマッチングを使うと簡潔です。

if (status is OrderStatus actualStatus){Console.WriteLine(actualStatus);}

検索条件の「未指定」や、入力フォームの「選択されていない状態」を表す場合に、Nullable列挙型が便利です。

10-4. enumの値が定義済みか安全に確認するには?

列挙型の値が定義済みか確認するには、Enum.IsDefinedを使用します。

OrderStatus status = (OrderStatus)999;

if (Enum.IsDefined(status)){Console.WriteLine("定義済みです");}else{Console.WriteLine("未定義です");}

文字列入力を変換する場合は、TryParseと組み合わせます。

static bool TryGetOrderStatus(string input,out OrderStatus status){return Enum.TryParse(input,ignoreCase: true,out status)&& Enum.IsDefined(status);}

ただし、Flags列挙型では複数のフラグを組み合わせた値が、名前付きメンバーとして定義されていないことがあります。

その場合は、定義済みの全ビット以外が含まれていないことを確認します。

static bool IsValidPermission(Permission value){const Permission all =Permission.Read |Permission.Create |Permission.Update |Permission.Delete;

return (value &amp; ~all) == 0;

}

10-5. enumの日本語表示名を取得するには?

代表的な方法は、次の3つです。

  • switch式で変換する

  • Dictionaryで対応付ける

  • Display属性などを使う

選択肢が少ない場合は、switch式が簡単です。

static string GetDisplayName(OrderStatus status){return status switch{OrderStatus.Pending => "受付待ち",OrderStatus.Processing => "処理中",OrderStatus.Shipped => "発送済み",OrderStatus.Completed => "完了",OrderStatus.Canceled => "キャンセル",_ => "不明"};}

ASP.NET Coreなどで画面表示に使う場合は、Display属性が便利です。

[Display(Name = "発送済み")]Shipped

多言語対応が必要な場合は、リソースファイルを使ったローカライズも検討しましょう。

10-6. enumとconst・static readonlyの違いは?

enumは、関連する整数値の選択肢を専用の型としてまとめる機能です。

public enum OrderStatus{Pending,Shipped,Completed}

constは、コンパイル時に値が確定する定数です。

public const int MaxRetryCount = 3;public const string ApiVersion = "v1";

static readonlyは、実行時に一度だけ設定できる静的な読み取り専用フィールドです。

public static readonly DateTime ServiceStartDate =new DateTime(2025, 1, 1);

主な違いは次のとおりです。

種類主な用途値の型値が決まるタイミング
enum限定された選択肢整数を基にした専用型コンパイル時
const単純な定数コンパイル時定数にできる型コンパイル時
static readonly実行時に決まる固定値任意の型型の初期化時

複数の選択肢を型安全に扱いたいならenum、単独の固定値ならconst、オブジェクトや実行時に生成する値ならstatic readonlyが基本的な使い分けです。

10-7. enumの値を後から変更しても問題ない?

アプリケーション内部だけで使用し、値を保存・公開していない列挙型であれば、変更の影響が限定的な場合もあります。

しかし、次の用途で使用している場合は、数値の変更に注意が必要です。

  • データベースへの保存

  • ファイルへの保存

  • JSONなどによる通信

  • 外部APIへの公開

  • ログや監査データへの記録

  • 他のアプリケーションとの共有

たとえば、Completed = 2として保存していたのに、後からCompleted = 3へ変更すると、既存の2を正しく解釈できなくなります。

// 変更前public enum OrderStatus{Pending = 0,Processing = 1,Completed = 2}
// 危険な変更例public enum OrderStatus{Pending = 0,Processing = 1,Confirmed = 2,Completed = 3}

外部に保存する列挙型では、既存値を変更せず、新しいメンバーには未使用の数値を割り当てます。

public enum OrderStatus{Pending = 0,Processing = 1,Completed = 2,Confirmed = 3}

メンバー名を変更する場合も、文字列として保存または送信していると互換性が失われます。列挙型の名前と数値は、外部仕様の一部になる可能性があると考えて設計しましょう。

まとめ

C#の列挙型は、関連する複数の選択肢を名前付きの定数としてまとめ、専用の型として扱うための機能です。

public enum OrderStatus{Pending,Processing,Shipped,Completed,Canceled}

数値や文字列を直接使う場合と比べて、値の意味が分かりやすくなり、入力ミスや不正な組み合わせを減らせます。if文、switch文、switch式、メソッドの引数や戻り値など、さまざまな場所で活用できます。

文字列との変換にはEnum.ParseEnum.TryParse、一覧取得にはEnum.GetValuesEnum.GetNames、定義済みかの確認にはEnum.IsDefinedが利用できます。

複数の値を組み合わせる場合は、Flags属性とビット演算を使用します。

[Flags]public enum Permission{None = 0,Read = 1,Create = 2,Update = 4,Delete = 8}

一方で、列挙型には未定義の数値を代入できるため、外部入力を扱う場合は検証が必要です。データベースやJSONに保存する場合は、数値やメンバー名の変更が互換性に影響する点にも注意しなければなりません。

列挙型は、種類が限定され、比較的安定している値に向いています。各選択肢が複雑なデータや処理を持つ場合や、値が頻繁に追加される場合は、定数クラス、データベース、クラス、レコードなどとの使い分けが重要です。

用途に合った設計と適切な入力検証を行えば、C#の列挙型は、読みやすく保守しやすいコードを作るための強力な仕組みになります。