enum(列挙型)は、値が名前付き定数の決まった集合である型です。注文の状態、曜日、ログのレベルなどです。内部では各名前は整数ですが、型システムによって、OrderStatus が普通の int や別のenumと混同されることはありません。
enumの宣言と使い方
波かっこの中にメンバーの名前を並べます。既定では最初が 0 で、次のものは1ずつ大きくなります。
出力:
Paid
On its way
True
2
enumは本物の型です。OrderStatus を受け取るメソッドを、誤って 3 や LogLevel で呼ぶことはできません。enumは値型なので、決して null にならず、== で値として比較されます。
明示的な値と基になる型
数値は自分で割り当てられます。数値がプログラムの外に出る場合(データベースの列、HTTPのステータス、ファイル形式)は重要です。番号を振り直すと保存されたデータが壊れるからです。
出力:
404
Created
418
1
Byte
注目すべき点が2つあります。int をenumにキャストしても決して失敗しません。(HttpStatus)418 は名前がないだけの有効な値で、数値として表示されます。そして基になる型には任意の整数型(byte、short、long など)を使えますが、それが問題になるのは格納サイズを気にするコードだけです。既定の int がほぼ常に正しい選択です。
後からメンバーを追加するときは、末尾に追加するか、明示的な値を与えます。Paid と Shipped の間に Refunded を挿入すると、その後のすべてのメンバーの番号が何も言わずに変わります。
enumから文字列へ
ToString() はメンバーの名前を返し、Console.WriteLine や文字列補間もこれを使います。書式文字列で出力を変えられます。
出力:
Warning
2
00000002
[Warning]
Error
Error
Needs attention
メンバーの名前は識別子なので、空白を含められず、翻訳もされません。ユーザーに見せるテキストには、Label のように、または Dictionary<LogLevel, string> で、値を自分で対応付けます。各メンバーに [Description("Needs attention")] 属性を付けてリフレクションで読むコードベースもあります。その検索の仕組みはリフレクションと属性のページで示しています。
文字列からenumへ:ParseとTryParse
Enum.Parse は名前を値に戻し、何も一致しなければ ArgumentException を投げます。Enum.TryParse は代わりに false を返し、自分で制御できない入力にはこちらが適しています。
出力:
Large
Medium
Parse threw ArgumentException for Huge
small parsed=True value=Small defined=True
XL parsed=False value=Small defined=True
2 parsed=True value=Large defined=True
7 parsed=True value=7 defined=False
最後の2行が落とし穴です。どちらのメソッドも数値の文字列を受け付けるので、"7" は名前のない Size として正常に解析されます。そして失敗した TryParse は結果を 0 にし、ここではそれが有効に見える Small になります。テキストがクエリ文字列、設定ファイル、フォームから来るなら、常に戻り値と Enum.IsDefined の両方を確認します。
if (Enum.TryParse(input, true, out Size size) && Enum.IsDefined(typeof(Size), size))
{
// safe to use size
}
.NET Core 2.0以降には、キャストの不要なジェネリック版 Enum.Parse<Size>("Large") があります。
すべての値の一覧
Enum.GetValues はすべてのメンバーを数値の順に並べて返し(符号なしとして比較されるので、負のメンバーは最後になります)、Enum.GetNames はその名前を返します。ドロップダウンを埋めたり、すべての選択肢に対して検証したりするのに使います。
出力:
Free 0 EUR/month
Starter 9 EUR/month
Pro 29 EUR/month
Team 99 EUR/month
Free | Starter | Pro | Team
3 paid plans
Enum.GetValues(typeof(Plan)) はただの Array を返すので、LINQ の前に Cast<Plan>() を使っています。.NET 5以降では、Enum.GetValues<Plan>() が型付きの Plan[] を直接返します。
Flags:値を組み合わせる
1つの選択ではなく、選択肢の集合を表すenumもあります。ファイルの権限、店の営業日、通知の経路などです。各メンバーに独自のビット(1、2、4、8など)を割り当て、None = 0 を加え、enumに [Flags] を付けます。すると値は | で組み合わせられます。
出力:
Read, Share
Editor, Share
True
False
Editor
3
Read, Delete
True
各演算子の働き:| はビットを立て、& ~X はビットを下ろし、^ は反転させ、(value & X) != 0 や value.HasFlag(X) は判定します。HasFlag(X) は「Xのすべてのビットが立っている」という意味なので、HasFlag(None) はどの値でも真になり、HasFlag(Editor) には Read と Write の両方が必要です。
2行目に注目してください。立っているビットの一部を名前付きの組み合わせがカバーしていると、ToString はそれを使うので、Read | Write | Share は Editor, Share と表示されます。ToString の出力を Enum.Parse 以外で解析する前に、このことを覚えておきます。
属性は演算を変えません。変えるのは書式設定です。[Flags] がないと、その値を持つ単独のメンバーがないので、Read | Share は 9 と表示されます。属性があれば、ToString と Parse の両方がカンマ区切りの形で動きます。メンバーはやはり2のべき乗でなければなりません。既定の番号付け(0、1、2)で Read, Write, Delete と書くと、Write | Delete は意味のない値の 3 になります。
enumでのswitch
enumに応じて処理するには switch が自然です。enumの変数は名前のない値を保持しうるので、default の分岐を含めます。
switch (status)
{
case OrderStatus.Pending:
case OrderStatus.Paid:
return "Preparing";
case OrderStatus.Shipped:
return "On the way";
case OrderStatus.Delivered:
return "Delivered";
default:
return "Unknown";
}
C# 8以降はswitch式のほうが短く書けます。_ のアームがないと、コンパイラは警告を出します。名前付きのメンバーが抜けていればCS8509、すべての名前を扱っていても (OrderStatus)7 のような名前のない値を扱っていなければCS8524です。
string text = status switch
{
OrderStatus.Pending or OrderStatus.Paid => "Preparing", // 'or' pattern: C# 9
OrderStatus.Shipped => "On the way",
OrderStatus.Delivered => "Delivered",
OrderStatus.Cancelled => "Cancelled",
_ => throw new ArgumentOutOfRangeException(nameof(status)),
};
既定値と定義されていない値
どのenumの既定値も、その値のメンバーがあるかどうかに関係なく 0 です。フィールド、配列の要素、失敗した TryParse はすべてこれを生み出します。これを前提に設計します。
0は実際の選択肢ではなく、意味のある「未設定」のメンバー(None、Unknown)にします。そうしないと、初期化されていないフィールドが最初の実際の選択肢として何も言わずに読まれます。- 外部からの数値は
Enum.IsDefinedで検証します。[Flags]のenumでは、名前のない組み合わせ(Read | Share)に対してIsDefinedはfalseを返すので、代わりにビットを確認します:すべてのビットをカバーするAllメンバーを用意して(value & ~Permissions.All) == 0とします。
よくある間違い
TryParseだけを信用する。 数値の文字列は解析でき、失敗した解析は0を生み出します。Enum.IsDefinedを加えます。- 保存する値に暗黙の番号付けを頼る。 メンバーを挿入すると、その後のものの番号が変わります。永続化するenumには明示的な値を割り当てます。
- 2のべき乗でないFlags。 既定の番号付け(0、1、2、3)ではビットが重なります。1、2、4、8か
1 << nを使います。 ToString()をユーザーに見せる。 メンバーの名前はコードの識別子です。値を表示用のテキストに対応付けます。- switchに
defaultがない。 enumは名前付きのメンバー以外の値を保持しえます。
よくある質問
C#でenumを文字列に変換するには?
ToString() を呼びます:OrderStatus.Shipped.ToString() は "Shipped" を返し、文字列補間も同じです。ToString("D") は代わりに数値を返します。コンパイル時にわかっている名前なら、nameof(OrderStatus.Shipped) が定数になります。空白を含む、または翻訳されたユーザー向けのテキストには、メンバー名に頼らず、値を自分で文字列に対応付けます(switch や辞書)。
C#で文字列をenumに変換するには?
Enum.TryParse<OrderStatus>(text, true, out var status) を使います。テキストがどのメンバーにも一致しないとき、例外を投げる代わりに false を返します(true は大文字と小文字を区別しない指定です)。Enum.Parse(typeof(OrderStatus), text) は不正な入力で ArgumentException を投げます。どちらも "42" のような数値の文字列も受け付けるので、入力がユーザーから来るなら Enum.IsDefined で結果を確認します。
C#でenumとintを相互に変換するには?
どちらの方向もキャストします:int code = (int)OrderStatus.Paid; と var status = (OrderStatus)2;。intからのキャストは、一致するメンバーのない数値でも決して失敗せず、結果は数値として表示されるenumの値になります。数値が外部から来るなら Enum.IsDefined(typeof(OrderStatus), value) で検証します。
C#でenumのすべての値をループするには?
foreach (OrderStatus s in Enum.GetValues(typeof(OrderStatus))) は、すべてのメンバーを数値の順に訪れます。.NET 5以降には、キャストの不要なジェネリック版 Enum.GetValues<OrderStatus>() があります。Enum.GetNames(typeof(OrderStatus)) は名前を文字列として返します。
C#のenumの[Flags]は何をしますか?
Read | Write のように | で組み合わせるためのビットを値とするenumであることを示します。各メンバーに2のべき乗(1、2、4、8)を割り当て、None = 0 を用意します。この属性によって、ToString() は組み合わせを "Read, Write" と表示し、Enum.Parse はその形式を読み戻せるようになります。ビットの判定には HasFlag か (value & Permissions.Write) != 0 を使います。