C#のenum(列挙型)は、決まった選択肢に名前を付けて1つの型にまとめる仕組みです。文字列にするなら ToString、文字列から戻すなら Enum.TryParse、数値とはキャストで行き来します。

この記事では、enum ⇔ 文字列 ⇔ 数値の変換方法を「例外の有無」「大文字小文字の無視」で整理した早見表を先に示し、すべてのコードに .NET 8(C# 12)での実行結果を付けました。さらに、"10" のような数値文字列や未定義の数値がそのまま通ってしまう落とし穴を再現し、コピペで使える安全な変換メソッド、Flags のビット演算(追加・削除・判定)、Enum.GetValues<T> による一覧取得、表示名を付ける方法までまとめています。

enumの変換早見表

やりたいこと 書き方 変換できないとき 大文字小文字 数値文字列 "10" 未定義の数値
enum → 文字列 s.ToString() ― ― ― 数値の文字列("99")になる
enum → 名前(定義済みのみ) Enum.GetName(s) null を返す ― ― null
enum → 数値 (int)s ― ― ― そのままの数値
文字列 → enum Enum.Parse<T>(text) ArgumentException 区別する 通る ―
文字列 → enum(無視) Enum.Parse<T>(text, ignoreCase: true) ArgumentException 無視する 通る ―
文字列 → enum Enum.TryParse<T>(text, out var v) false を返す(例外なし) 区別する 通る ―
文字列 → enum(無視) Enum.TryParse<T>(text, true, out var v) false を返す(例外なし) 無視する 通る ―
数値 → enum (T)number 例外なし ― ― 通る
定義済みか確認 Enum.IsDefined(v) false を返す 名前で渡す場合は区別する ― false

Flags と一覧取得の早見表です。

やりたいこと 書き方
フラグを追加 p |= Permission.Write;
フラグを削除 p &= ~Permission.Write;
フラグを切り替え p ^= Permission.Share;
含むか判定 p.HasFlag(Permission.Read)
どれか1つでも含むか (p & (A | B)) != 0
何も無いか p == Permission.None
すべての値を取得 Enum.GetValues<T>()
すべての名前を取得 Enum.GetNames<T>()
メンバーの数 Enum.GetNames<T>().Length

外から来た文字列は TryParse、そのあと IsDefined で確認、という2段がこの記事の結論です。理由は「数値文字列と未定義の数値が通る」の章で再現します。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「C# enumの使い方|文字列変換・Parse・Flags・一覧取得まとめ」

この記事の確認環境

項目 内容
.NET SDK 8.0.131
C# 12(.NET 8 の既定)
実行方法 dotnet new console で作ったプロジェクトの Program.cs を置き換えて dotnet run

Enum.GetValues<T>() や Enum.GetNames<T>()、Enum.IsDefined(v) のように型引数で書けるジェネリック版は、.NET 5 以降で使える書き方として一般に知られています(導入バージョンの公式ページは今回開いていません)。この記事では .NET 8 で動かして確認しました。.NET 9・.NET 10 での動作は未実行です。

例外のメッセージは実行環境の言語設定によって変わります。この記事の出力は英語のメッセージです。

enumの定義と基本の使い方

// enum の定義と基本
TaskStatus status = TaskStatus.InProgress;

Console.WriteLine(status);                 // 名前が表示される
Console.WriteLine((int)status);            // 数値にするにはキャスト
Console.WriteLine(status == TaskStatus.InProgress);
Console.WriteLine(default(TaskStatus));    // 既定値は 0 に対応するメンバー
Console.WriteLine((int)Priority.High);     // 値を指定した enum
Console.WriteLine(Enum.GetUnderlyingType(typeof(TaskStatus)));
Console.WriteLine(Enum.GetUnderlyingType(typeof(Size)));

string label = status switch
{
    TaskStatus.Todo => "未着手",
    TaskStatus.InProgress => "作業中",
    TaskStatus.Done => "完了",
    _ => "不明",
};
Console.WriteLine(label);

public enum TaskStatus      // 値を書かなければ 0, 1, 2 ... と順に振られる
{
    Todo,
    InProgress,
    Done,
}

public enum Priority        // 値を自分で決めることもできる
{
    Low = 10,
    Middle = 20,
    High = 30,
}

public enum Size : byte     // 元になる整数型を指定できる(既定は int)
{
    S, M, L,
}

実行結果(.NET 8):

InProgress
1
True
Todo
30
System.Int32
System.Byte
作業中
ポイント 内容
値の振られ方 書かなければ 0 から順に 1 ずつ増える
値の指定 Low = 10 のように自分で決められる
元になる型 既定は int。: byte のように指定できる
既定値 default(T) は 0。0 に対応するメンバーがあればその名前が出る
表示 Console.WriteLine(status) は名前(InProgress)を出力する
分岐 switch 式と相性がよい

0 に対応するメンバーを必ず1つ用意しておくと、初期化し忘れた変数やフィールドが「意味のある既定値」になります。None や Unknown、あるいは最初の状態(Todo)を 0 にするのが定番です。

enumと文字列・数値を変換する

変換方法を一通り実行して、早見表の内容を確かめます。

// enum ⇔ 文字列 ⇔ 数値 の変換を一通り試す
var s = TaskStatus.InProgress;

Console.WriteLine("--- enum → 文字列・数値 ---");
Console.WriteLine(s.ToString());            // InProgress
Console.WriteLine(nameof(TaskStatus.Done)); // Done(コンパイル時に決まる)
Console.WriteLine(s.ToString("D"));         // 1(数値を文字列で)
Console.WriteLine((int)s);                  // 1
Console.WriteLine(Enum.GetName(s));         // InProgress
Console.WriteLine(Enum.GetName((TaskStatus)99) is null);  // 未定義なら null

Console.WriteLine("--- 文字列 → enum(Parse) ---");
Console.WriteLine(Enum.Parse<TaskStatus>("Done"));
Console.WriteLine(Enum.Parse<TaskStatus>("done", ignoreCase: true));
Try(() => Enum.Parse<TaskStatus>("done"));
Try(() => Enum.Parse<TaskStatus>("Finished"));
Try(() => Enum.Parse<TaskStatus>(""));
Console.WriteLine(Enum.Parse<TaskStatus>("  Done  "));   // 前後の空白は無視される

Console.WriteLine("--- 文字列 → enum(TryParse) ---");
Console.WriteLine(Enum.TryParse<TaskStatus>("Done", out var r1) + " / " + r1);
Console.WriteLine(Enum.TryParse<TaskStatus>("done", out var r2) + " / " + r2);
Console.WriteLine(Enum.TryParse<TaskStatus>("done", true, out var r3) + " / " + r3);
Console.WriteLine(Enum.TryParse<TaskStatus>("Finished", out var r4) + " / " + r4);
Console.WriteLine(Enum.TryParse<TaskStatus>(null, out var r5) + " / " + r5);

Console.WriteLine("--- 数値 → enum ---");
Console.WriteLine((TaskStatus)2);
Console.WriteLine(Enum.IsDefined((TaskStatus)2));
Console.WriteLine(Enum.IsDefined(typeof(TaskStatus), 2));
Console.WriteLine(Enum.IsDefined(typeof(TaskStatus), "Done"));
Console.WriteLine(Enum.IsDefined(typeof(TaskStatus), "done"));   // 大文字小文字は区別される

static void Try(Func<TaskStatus> f)
{
    try { Console.WriteLine(f()); }
    catch (Exception e) { Console.WriteLine($"{e.GetType().Name}: {e.Message}"); }
}

public enum TaskStatus { Todo, InProgress, Done }

実行結果(.NET 8):

--- enum → 文字列・数値 ---
InProgress
Done
1
1
InProgress
True
--- 文字列 → enum(Parse) ---
Done
Done
ArgumentException: Requested value 'done' was not found.
ArgumentException: Requested value 'Finished' was not found.
ArgumentException: Must specify valid information for parsing in the string. (Parameter 'value')
Done
--- 文字列 → enum(TryParse) ---
True / Done
False / Todo
True / Done
False / Todo
False / Todo
--- 数値 → enum ---
Done
True
True
True
False

enumから文字列・数値にする

書き方 結果 使いどころ
s.ToString() InProgress 実行時の値を名前にする
nameof(TaskStatus.Done) Done 名前が固定のとき。コンパイル時に文字列になる
s.ToString("D") 1 数値を文字列でほしいとき
(int)s 1 数値として使うとき
Enum.GetName(s) InProgress(未定義なら null) 定義済みかどうかも同時に知りたいとき

文字列からenumにする(ParseとTryParse)

Parse は変換できないと ArgumentException、TryParse は false を返すという違いです。出力から次のことがわかります。

  • Enum.Parse<TaskStatus>("done") は Requested value 'done' was not found. で例外になる(大文字小文字を区別する)
  • ignoreCase: true を付けると "done" でも Done になる
  • 空文字は Must specify valid information for parsing in the string. で例外になる
  • 前後の空白は無視される
  • TryParse で変換できなかったとき、out 変数には既定値(0 に対応する Todo)が入る

最後の点は見落としやすいところです。TryParse の戻り値を見ずに out 変数だけを使うと、変換できなかったのに Todo として処理が進みます。 必ず戻り値の bool で分岐します。

if (Enum.TryParse<TaskStatus>(text, ignoreCase: true, out var status))
{
    // 変換できたときだけ status を使う
}

この書き方は、上の実行コードの TryParse と同じ呼び出しを if に入れた形です。

ParseとTryParseの選び方

入力の出どころ 選ぶもの 理由
画面の入力、クエリ文字列、CSV、外部からのデータ TryParse + IsDefined 想定外の値が来る前提で、例外なしで分岐できる
自分で書いた設定値など、合わないなら止めたいもの Parse 例外で早く気づける
コード中の固定の名前 nameof や enum を直接書く 変換そのものが不要

数値からenumにする

(TaskStatus)2 のようにキャストします。キャストはどんな数値でも例外にならないため、範囲の確認には Enum.IsDefined を組み合わせます。Enum.IsDefined(typeof(TaskStatus), "done") が False になっているとおり、名前を渡す場合は大文字小文字が区別されます。

数値文字列と未定義の数値が通る落とし穴

ここがこの記事でいちばん伝えたい部分です。Parse と TryParse は「名前」だけでなく「数値の文字列」も受け付けます。しかも、その数値が enum に定義されていなくても変換できたことになります。

// 落とし穴:数値文字列と未定義の数値がそのまま通る → 安全な変換メソッド
Console.WriteLine("--- 落とし穴の再現 ---");
var a = Enum.Parse<TaskStatus>("10");
Console.WriteLine($"Parse(\"10\") = {a} / IsDefined = {Enum.IsDefined(a)}");

bool ok = Enum.TryParse<TaskStatus>("10", out var b);
Console.WriteLine($"TryParse(\"10\") = {ok} / {b}");

var c = (TaskStatus)99;
Console.WriteLine($"(TaskStatus)99 = {c} / IsDefined = {Enum.IsDefined(c)}");

Console.WriteLine($"TryParse(\"-1\") = {Enum.TryParse<TaskStatus>("-1", out var d)} / {d}");
Console.WriteLine($"TryParse(\"Todo, Done\") = {Enum.TryParse<TaskStatus>("Todo, Done", out var e)} / {e}");
Console.WriteLine($"TryParse(\"1\") = {Enum.TryParse<TaskStatus>("1", out var f)} / {f}");

string label = c switch
{
    TaskStatus.Todo => "未着手",
    TaskStatus.InProgress => "作業中",
    TaskStatus.Done => "完了",
    _ => "(どれにも当てはまらない)",
};
Console.WriteLine(label);

Console.WriteLine("--- 安全な変換 ---");
foreach (var text in new[] { "Done", "done", "10", "1", "Finished", "", " Done ", "Todo, Done" })
{
    bool r = EnumUtil.TryParseName<TaskStatus>(text, out var v);
    Console.WriteLine($"TryParseName(\"{text}\") = {r} / {v}");
}
foreach (var n in new[] { 0, 2, 3, 99, -1 })
{
    bool r = EnumUtil.TryFromInt<TaskStatus>(n, out var v);
    Console.WriteLine($"TryFromInt({n}) = {r} / {v}");
}
Console.WriteLine(EnumUtil.ParseNameOrDefault("Finished", TaskStatus.Todo));

public enum TaskStatus { Todo, InProgress, Done }

public static class EnumUtil
{
    /// <summary>名前だけを受け付ける(数値文字列・未定義値・カンマ区切りは false)。</summary>
    public static bool TryParseName<T>(string? text, out T value, bool ignoreCase = true)
        where T : struct, Enum
    {
        value = default;
        if (string.IsNullOrWhiteSpace(text)) return false;

        var t = text.Trim();
        if (char.IsDigit(t[0]) || t[0] == '-' || t[0] == '+') return false;   // 数値文字列は受け付けない
        if (t.Contains(',')) return false;                                    // カンマ区切りも受け付けない
        if (!Enum.TryParse<T>(t, ignoreCase, out var parsed)) return false;
        if (!Enum.IsDefined(parsed)) return false;                            // 念のため定義済みか確認

        value = parsed;
        return true;
    }

    /// <summary>定義済みの数値だけを enum にする。</summary>
    public static bool TryFromInt<T>(int number, out T value)
        where T : struct, Enum
    {
        value = default;
        if (!Enum.IsDefined(typeof(T), number)) return false;
        value = (T)Enum.ToObject(typeof(T), number);
        return true;
    }

    /// <summary>変換できなければ既定値を返す。</summary>
    public static T ParseNameOrDefault<T>(string? text, T defaultValue)
        where T : struct, Enum
        => TryParseName<T>(text, out var v) ? v : defaultValue;
}

実行結果(.NET 8):

--- 落とし穴の再現 ---
Parse("10") = 10 / IsDefined = False
TryParse("10") = True / 10
(TaskStatus)99 = 99 / IsDefined = False
TryParse("-1") = True / -1
TryParse("Todo, Done") = True / Done
TryParse("1") = True / InProgress
(どれにも当てはまらない)
--- 安全な変換 ---
TryParseName("Done") = True / Done
TryParseName("done") = True / Done
TryParseName("10") = False / Todo
TryParseName("1") = False / Todo
TryParseName("Finished") = False / Todo
TryParseName("") = False / Todo
TryParseName(" Done ") = True / Done
TryParseName("Todo, Done") = False / Todo
TryFromInt(0) = True / Todo
TryFromInt(2) = True / Done
TryFromInt(3) = False / Todo
TryFromInt(99) = False / Todo
TryFromInt(-1) = False / Todo
Todo

前半の「落とし穴の再現」を表にします。TaskStatus は 0〜2 しか定義していません。

入力 結果 起きていること
Enum.Parse<TaskStatus>("10") 10(IsDefined は False) 数値文字列として解釈され、未定義の値のまま返る
Enum.TryParse<TaskStatus>("10", ...) True / 10 TryParse でも同じく通る
(TaskStatus)99 99(IsDefined は False) キャストは範囲を確認しない
Enum.TryParse<TaskStatus>("-1", ...) True / -1 負の数値文字列も通る
Enum.TryParse<TaskStatus>("Todo, Done", ...) True / Done カンマ区切りはビット OR される(0 と 2 で 2)
Enum.TryParse<TaskStatus>("1", ...) True / InProgress 定義済みの数値は名前に対応する

Microsoft Learn の Enum.TryParse のページ(今回開いて確認しました)にも、定義されていない整数の文字列でも変換は成功すること、定義済みかどうかは IsDefined で確かめることが、サンプルコードつきで説明されています。

未定義の値が入った変数を switch 式に渡すと、どの分岐にも当てはまらず _ の行に進みます。出力の「(どれにも当てはまらない)」がそれです。_ の分岐を書いていない場合の挙動はこの記事では未実行ですが、未定義の値は入口で止めておくのがいちばん確実です。

コピペ用:安全な変換メソッド

上のコードの EnumUtil クラスが、そのまま使える安全な変換メソッドです。後半の実行結果で、次のように振り分けられていることを確認できます。

入力 TryParseName の結果 入力 TryFromInt の結果
"Done" True / Done 0 True / Todo
"done" True / Done(大文字小文字を無視) 2 True / Done
"10" False 3 False
"1" False(数値文字列は受け付けない) 99 False
"Finished" False -1 False
""(空文字) False
" Done "(前後に空白) True / Done
"Todo, Done" False(カンマ区切りは受け付けない)
メソッド 役割
EnumUtil.TryParseName<T>(text, out value) 名前だけを受け付ける。数値文字列・カンマ区切り・未定義は false
EnumUtil.TryFromInt<T>(number, out value) 定義済みの数値だけを enum にする
EnumUtil.ParseNameOrDefault<T>(text, defaultValue) 変換できなければ既定値を返す

where T : struct, Enum という制約を付けることで、enum 以外の型を渡すとコンパイル時に気づけます。「数値文字列も受け付けたい」場合は、数値かどうかを判定している1行を外し、IsDefined の確認だけを残します。

なお、TryFromInt は元になる型が int の enum を想定しています。: byte などを指定した enum に int を渡したときの動きは未実行です。また、Flags を付けた enum では、次の章で見るとおり組み合わせの値に対して IsDefined が False になるため、この EnumUtil は Flags ではない enum 向けです。

Flagsでビット演算を使う(追加・削除・判定)

[Flags] 属性を付けた enum は、複数の値を1つの変数にまとめて持てます。 各メンバーの値を 1, 2, 4, 8 … と2のべき乗にするのが決まりで、1 << 0、1 << 1 のようにシフト演算で書くと間違いにくくなります。

// Flags:複数の値を1つの変数にまとめる
var p = Permission.Read;

p |= Permission.Write;                       // 追加
Console.WriteLine($"追加: {p} ({(int)p})");

p |= Permission.Delete;
p &= ~Permission.Write;                      // 削除
Console.WriteLine($"削除: {p} ({(int)p})");

p ^= Permission.Share;                       // 切り替え(無ければ付く)
Console.WriteLine($"切替1: {p}");
p ^= Permission.Share;                       // もう一度で外れる
Console.WriteLine($"切替2: {p}");

Console.WriteLine("--- 判定 ---");
Console.WriteLine(p.HasFlag(Permission.Read));                    // 含むか
Console.WriteLine((p & Permission.Read) == Permission.Read);      // 同じ意味のビット演算
Console.WriteLine(p.HasFlag(Permission.Read | Permission.Delete)); // 両方含むか
Console.WriteLine(p.HasFlag(Permission.Write));
Console.WriteLine((p & (Permission.Write | Permission.Delete)) != 0); // どれか1つでも含むか
Console.WriteLine(p.HasFlag(Permission.None));                    // None は常に True
Console.WriteLine(p == Permission.None);                          // 「何も無い」の判定は ==

Console.WriteLine("--- 文字列との変換 ---");
Console.WriteLine(Permission.All);
Console.WriteLine(Permission.Read | Permission.Write);
Console.WriteLine(Enum.Parse<Permission>("Read, Write"));
Console.WriteLine((int)Enum.Parse<Permission>("Read, Share"));
Console.WriteLine((Permission)5);
Console.WriteLine((Permission)16);            // 未定義のビット
Console.WriteLine(Enum.IsDefined(Permission.Read | Permission.Write));  // 組み合わせは False

Console.WriteLine("--- 立っているフラグを列挙 ---");
var q = Permission.Read | Permission.Share;
foreach (var f in Enum.GetValues<Permission>())
{
    if (f != Permission.None && System.Numerics.BitOperations.PopCount((uint)f) == 1 && q.HasFlag(f))
        Console.WriteLine(f);
}

Console.WriteLine("--- [Flags] を付けない場合 ---");
Console.WriteLine(NoFlags.A | NoFlags.B);
Console.WriteLine((NoFlags.A | NoFlags.B).HasFlag(NoFlags.A));

Console.WriteLine("--- 範囲チェック ---");
Console.WriteLine(IsValid((Permission)5));
Console.WriteLine(IsValid((Permission)16));

static bool IsValid(Permission value) => (value & ~Permission.All) == 0;

[Flags]
public enum Permission
{
    None   = 0,
    Read   = 1 << 0,   // 1
    Write  = 1 << 1,   // 2
    Delete = 1 << 2,   // 4
    Share  = 1 << 3,   // 8
    All    = Read | Write | Delete | Share,   // 15
}

public enum NoFlags { A = 1, B = 2 }

実行結果(.NET 8):

追加: Read, Write (3)
削除: Read, Delete (5)
切替1: Read, Delete, Share
切替2: Read, Delete
--- 判定 ---
True
True
True
False
True
True
False
--- 文字列との変換 ---
All
Read, Write
Read, Write
9
Read, Delete
16
False
--- 立っているフラグを列挙 ---
Read
Share
--- [Flags] を付けない場合 ---
3
True
--- 範囲チェック ---
True
False

追加・削除・切り替え・判定の書き方

操作 書き方 意味
追加 p |= Permission.Write; Write のビットを立てる
削除 p &= ~Permission.Write; Write のビットだけ下ろす
切り替え p ^= Permission.Share; 無ければ付け、あれば外す
含むか p.HasFlag(Permission.Read) 指定したビットがすべて立っているか
含むか(ビット演算) (p & Permission.Read) == Permission.Read HasFlag と同じ意味
両方含むか p.HasFlag(Permission.Read | Permission.Delete) 2つとも立っているか
どれか含むか (p & (Permission.Write | Permission.Delete)) != 0 1つでも立っているか
何も無いか p == Permission.None 値が 0 か

Flagsで気を付けたい点

出力から読み取れる注意点です。

動き 出力 対応
p.HasFlag(Permission.None) は常に True True 「何も無い」の判定は p == Permission.None で書く
ToString は名前をカンマ区切りで返す Read, Write 保存・表示にそのまま使える
すべて立っていると、まとめた名前になる All All のような合成メンバーがあるとそちらが優先される
Parse はカンマ区切りの文字列を受け付ける Read, Write ToString の結果をそのまま戻せる
未定義のビット(16)もキャストできる 16 (value & ~Permission.All) == 0 で範囲を確認する
組み合わせの値に IsDefined は使えない False 範囲の確認は上のビット演算で行う
[Flags] を付けないと ToString が数値になる 3 HasFlag 自体は動くが、表示のために [Flags] を付ける

値を 1, 2, 3, 4 と連番にしてしまうと、3 が「1 と 2 の組み合わせ」と区別できなくなります。 必ず 1, 2, 4, 8 と倍々にします。

Flagsを使うかどうかの判断表

状況 選ぶもの
選べるのは常に1つ(状態、種別、優先度) 通常の enum
複数を同時に持てる(権限、オプション、曜日の組み合わせ) [Flags] 付きの enum
選択肢があとから頻繁に増減する、利用者が追加する enum ではなく、テーブルや設定で管理

enumの一覧を取得する(GetValuesとGetNames)

選択肢のドロップダウンを作る、全パターンを検査する、といった場面で使います。

// 一覧取得:GetValues<T> / GetNames<T>
Console.WriteLine("--- GetValues<T> ---");
TaskStatus[] values = Enum.GetValues<TaskStatus>();
foreach (var v in values)
{
    Console.WriteLine($"{(int)v}: {v}");
}

Console.WriteLine("--- GetNames<T> ---");
string[] names = Enum.GetNames<TaskStatus>();
Console.WriteLine(string.Join(", ", names));
Console.WriteLine($"件数: {names.Length}");

Console.WriteLine("--- 以前からある書き方(typeof) ---");
foreach (TaskStatus v in Enum.GetValues(typeof(TaskStatus)))
{
    Console.Write(v + " ");
}
Console.WriteLine();

Console.WriteLine("--- 名前→数値の辞書にする ---");
Dictionary<string, int> map = Enum.GetValues<TaskStatus>()
    .ToDictionary(v => v.ToString(), v => (int)v);
foreach (var (k, n) in map) Console.WriteLine($"{k} = {n}");

Console.WriteLine("--- 最初・最後・次の値 ---");
Console.WriteLine(values.First());
Console.WriteLine(values.Last());
Console.WriteLine(Next(TaskStatus.Todo));
Console.WriteLine(Next(TaskStatus.Done));      // 最後の次は最初に戻す

Console.WriteLine("--- 値の順に並ぶ ---");
Console.WriteLine(string.Join(", ", Enum.GetNames<Priority>()));

Console.WriteLine("--- 同じ値のメンバーがある場合 ---");
Console.WriteLine(string.Join(", ", Enum.GetNames<Level>()));
Console.WriteLine(string.Join(", ", Enum.GetValues<Level>()));
Console.WriteLine(Enum.GetValues<Level>().Distinct().Count());

static TaskStatus Next(TaskStatus s)
{
    var all = Enum.GetValues<TaskStatus>();
    return all[(Array.IndexOf(all, s) + 1) % all.Length];
}

public enum TaskStatus { Todo, InProgress, Done }

public enum Priority { High = 30, Low = 10, Middle = 20 }   // 定義順は High, Low, Middle

public enum Level { Low = 1, Normal = 2, Default = 2, High = 3 }

実行結果(.NET 8):

--- GetValues<T> ---
0: Todo
1: InProgress
2: Done
--- GetNames<T> ---
Todo, InProgress, Done
件数: 3
--- 以前からある書き方(typeof) ---
Todo InProgress Done 
--- 名前→数値の辞書にする ---
Todo = 0
InProgress = 1
Done = 2
--- 最初・最後・次の値 ---
Todo
Done
InProgress
Todo
--- 値の順に並ぶ ---
Low, Middle, High
--- 同じ値のメンバーがある場合 ---
Low, Normal, Default, High
Low, Normal, Normal, High
3
書き方 戻り値 補足
Enum.GetValues<TaskStatus>() TaskStatus[] キャスト不要で foreach や LINQ に渡せる
Enum.GetNames<TaskStatus>() string[] 名前だけがほしいとき
Enum.GetValues(typeof(TaskStatus)) Array 以前からある書き方。foreach で型を書く必要がある
Enum.GetNames<T>().Length int メンバーの数

出力から、次の2点も確認できました。

  • 並び順は定義した順ではなく、値の小さい順になる(Priority は High, Low, Middle の順に定義したが、Low, Middle, High と出力された)
  • 同じ値を持つメンバーがあると、GetNames は全部の名前を返すが、GetValues は同じ値が重複して並ぶ(Level の例で Normal が2回出ている)。値の種類を数えたいときは Distinct() を使う

「次の状態に進める」処理は、Enum.GetValues<T>() の配列の中での位置を使うと、値が連番でなくても動く形で書けます(コード中の Next メソッド)。

enumに表示名を付ける方法

enum の名前は英語の識別子なので、画面には「未着手」「作業中」のような表示名を出したくなります。代表的な3つの方法を並べて実行します。

// 表示名を付ける3つの方法
using System.ComponentModel.DataAnnotations;
using System.Reflection;

foreach (var s in Enum.GetValues<TaskStatus>())
{
    Console.WriteLine($"{s} | {s.ToLabel()} | {s.GetDisplayName()} | {Labels.Of(s)}");
}
Console.WriteLine(((TaskStatus)99).ToLabel());
Console.WriteLine(((TaskStatus)99).GetDisplayName());

// 選択肢(value と表示名の組)を作る
var options = Enum.GetValues<TaskStatus>()
    .Select(s => new { Value = (int)s, Text = s.ToLabel() });
foreach (var o in options) Console.WriteLine($"<option value=\"{o.Value}\">{o.Text}</option>");

public enum TaskStatus
{
    [Display(Name = "未着手")] Todo,
    [Display(Name = "作業中")] InProgress,
    [Display(Name = "完了")]   Done,
}

public static class TaskStatusExtensions
{
    // 方法1:switch 式
    public static string ToLabel(this TaskStatus s) => s switch
    {
        TaskStatus.Todo => "未着手",
        TaskStatus.InProgress => "作業中",
        TaskStatus.Done => "完了",
        _ => $"不明({(int)s})",
    };

    // 方法2:Display 属性をリフレクションで読む(どの enum にも使える)
    public static string GetDisplayName<T>(this T value) where T : struct, Enum
    {
        var name = value.ToString();
        var field = typeof(T).GetField(name);
        var attr = field?.GetCustomAttribute<DisplayAttribute>();
        return attr?.GetName() ?? name;
    }
}

// 方法3:辞書で持つ
public static class Labels
{
    private static readonly Dictionary<TaskStatus, string> Map = new()
    {
        [TaskStatus.Todo] = "未着手",
        [TaskStatus.InProgress] = "作業中",
        [TaskStatus.Done] = "完了",
    };

    public static string Of(TaskStatus s) => Map.TryGetValue(s, out var label) ? label : s.ToString();
}

実行結果(.NET 8):

Todo | 未着手 | 未着手 | 未着手
InProgress | 作業中 | 作業中 | 作業中
Done | 完了 | 完了 | 完了
不明(99)
99
<option value="0">未着手</option>
<option value="1">作業中</option>
<option value="2">完了</option>
方法 長所 気を付ける点
switch 式の拡張メソッド 速く、読みやすい。未定義の値の扱いも書ける enum ごとにメソッドを書く
[Display(Name = "...")] 属性 + リフレクション 定義のすぐ横に表示名を書ける。どの enum にも同じメソッドが使える 呼ぶたびにリフレクションが動く。回数が多い箇所では結果を辞書に入れて使い回すとよい(速度の計測は未実行)
辞書 表示名を外部ファイルなどから読み込む形に広げやすい メンバーを足したときに辞書の追加を忘れやすい

未定義の値(99)を渡したとき、switch 式版は「不明(99)」、属性版は「99」を返しました。属性版は GetField が null を返すため、ToString の結果がそのまま使われています。

メンバーが少なく固定なら switch 式、enum の種類が多いなら Display 属性、という分け方が使いやすい形です。一覧取得と組み合わせると、出力の最後の3行のように選択肢の HTML を作れます。

JSONでenumを名前のまま出力する

System.Text.Json は、既定では enum を数値で出力します。名前で出力したいときは JsonStringEnumConverter を追加します。

// JSON との変換(System.Text.Json)
using System.Text.Json;
using System.Text.Json.Serialization;

var task = new TaskItem("Report", TaskStatus.InProgress);

Console.WriteLine(JsonSerializer.Serialize(task));                 // 既定は数値

var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
Console.WriteLine(JsonSerializer.Serialize(task, options));        // 名前で出力

var back = JsonSerializer.Deserialize<TaskItem>("{\"Title\":\"Check\",\"Status\":\"Done\"}", options);
Console.WriteLine(back);

try
{
    JsonSerializer.Deserialize<TaskItem>("{\"Title\":\"Check\",\"Status\":\"Finished\"}", options);
}
catch (JsonException e)
{
    Console.WriteLine("JsonException: " + e.Message);
}

public enum TaskStatus { Todo, InProgress, Done }
public record TaskItem(string Title, TaskStatus Status);

実行結果(.NET 8):

{"Title":"Report","Status":1}
{"Title":"Report","Status":"InProgress"}
TaskItem { Title = Check, Status = Done }
JsonException: The JSON value could not be converted to TaskItem. Path: $.Status | LineNumber: 0 | BytePositionInLine: 36.
設定 出力
既定 "Status":1
JsonStringEnumConverter を追加 "Status":"InProgress"
定義に無い名前("Finished")を読み込む JsonException

数値のままだと、あとで enum の途中にメンバーを足したときに意味がずれます。外部とやり取りする JSON や保存するデータは名前にしておくと、定義の並び替えや追加の影響を受けにくくなります。

よくあるつまずきと直し方

症状 原因 直し方
Requested value 'done' was not found. Parse は大文字小文字を区別する ignoreCase: true を付ける、または TryParse にする
変換できないはずの入力なのに Todo として動く TryParse の戻り値を見ずに out 変数を使っている 戻り値の bool で分岐する
定義していない数値の enum が保存されている 数値文字列やキャストがそのまま通った IsDefined で確認する(この記事の EnumUtil)
Flags の表示が数値になる [Flags] 属性が無い 属性を付ける
HasFlag(None) が常に True 0 のビットは常に「含む」扱い == Permission.None で判定する
Flags の値を 1, 2, 3 と連番にした 3 が 1 と 2 の組み合わせと重なる 1, 2, 4, 8 にする(1 << n)
一覧の順番が定義順と違う GetValues / GetNames は値の順 値を並べたい順に振る、または自分で並べ替える
JSON に数値で出力される 既定の動作 JsonStringEnumConverter を追加する

enumを定義するときのチェックリスト

確認すること チェック
0 に対応するメンバー(None や最初の状態)がある □
保存・通信に使う enum は、値を明示している(あとから途中に足しても数値が変わらない) □
Flags の値は 2 のべき乗になっている □
外から来た値は TryParse と IsDefined で確認している □
switch 式に _ の分岐がある □
表示名の置き場所(switch 式・属性・辞書)を決めている □

よくある質問

Q. Enum.Parse と Enum.TryParse はどちらを使えばよいですか? A. 画面入力やファイルなど、外から来た文字列には TryParse が向いています。変換できないときに例外ではなく false が返るため、if で分岐できます。自分で書いた設定値のように「合わないなら止めたい」ものは Parse で例外にすると気づきやすくなります。どちらも数値文字列を受け付けるので、IsDefined での確認を組み合わせます。

Q. enum を文字列にするとき、ToString と nameof はどう違いますか? A. ToString() は変数に入っている実行時の値を名前にします。nameof(TaskStatus.Done) はコンパイル時に "Done" という文字列に置き換わるため、対象が固定のときに使います。変数の中身を文字列にしたいときは ToString です。

Q. Enum.GetValues(typeof(T)) と Enum.GetValues<T>() はどちらを使えばよいですか? A. 型引数で書ける Enum.GetValues<T>() は T[] を返すので、キャストなしで LINQ に渡せます。この記事では .NET 8 で両方を実行し、同じ内容が得られることを確認しました。typeof を使う書き方は以前からあるコードで見かける形です。

Q. Flags の enum で、ある値が有効な範囲かどうかを確認するにはどうすればよいですか? A. 組み合わせの値には IsDefined が False を返すため、すべてのフラグをまとめた All を用意して (value & ~Permission.All) == 0 で確認します。この記事の実行結果では、5(Read と Delete)が True、16(未定義のビット)が False になりました。

まとめ

  • enum → 文字列は ToString、文字列 → enum は TryParse、数値とはキャストで変換する
  • Parse は変換できないと ArgumentException、TryParse は false。大文字小文字を無視するには ignoreCase を true にする
  • Parse・TryParse は "10" のような数値文字列も受け付け、キャストは未定義の数値も通す。IsDefined で確認する
  • TryParse の out 変数は、変換できなかったとき既定値(0)になる。必ず戻り値で分岐する
  • Flags は値を 1, 2, 4, 8 にして、追加は OR、削除は AND と NOT、判定は HasFlag
  • 一覧は Enum.GetValues<T>() と Enum.GetNames<T>()。並びは値の小さい順になる
  • 表示名は switch 式・Display 属性・辞書の3通り。メンバー数と enum の数で選ぶ

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「C# enumの使い方|文字列変換・Parse・Flags・一覧取得まとめ」

株式会社RJCでは、C#のenumをはじめとした型の使い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?