C# Enum 完全ガイド — Description 属性 / [Flags] / 数値変換の使い分け5パターン

みなさんこんにちは!ヒロポンです!

C#業務系で、こんな書き方してませんか??

if (status == 1){ /* 販売中 */ }
else if (status == 2){ /* 一時停止 */ }
else if (status == 9){ /* 廃番 */ }

これ業務系でMagic Number流派ってやつで、半年後に「1ってなんだっけ?」状態になって保守性が落ちる典型例なんですよね。俺も最初の.NET Framework 4.7.2 / VS2019 / C# 7.3案件で、先輩のコード読んでてstatus == 55が何を意味するか分からなくてDB設計書ひっくり返した経験があります。

ん?普通にEnum使えばいいんじゃ??って思いますよね。

その通りで、業務系の区分値管理はEnum一択です。けど、Enumの使い方には業務系で頻出する5パターンがあって、これを押さえてないと「Enum使ってるのに保守性低い」状態になります。

今回はC# Enumを業務系で使い倒す5パターンを、コード付きで整理します。コード6本構成。

目次

忙しい人向けに最初にまとめ

  • 業務系の区分値管理はEnum一択、Magic Number流派からの移行が現実解
  • 5パターン: 基本宣言と数値変換 / Description属性で画面表示 / [Flags]でビット組合せ / GetValuesでComboBoxバインド / TryParse + IsDefinedでDB値検証
  • DBのint値とのマッピングは(int)EnumValue / (EnumType)dbValueでキャスト
  • DBから来た数値の検証はEnum.IsDefinedが業務SE鉄則

なぜEnumか— Magic Numberとの比較

業務系で区分値をintで扱うMagic Number流派とEnum流派の違いを並べると、こんな感じです。

項目 Magic Number (int) Enum
意図の明示 コメントが頼り 型で明示
IDE補完 効かない 効く
タイプセーフ 別カラムの値混入リスク 型エラーで検出
画面表示 別Dictionary等が必要 Description属性で紐付け
DBマッピング そのまま使える キャストで簡単

業務系で半年経った後の保守性で大きく差が出ます。俺の現場でも先輩から受け継いだMagic Number流派のコードをEnum流派にリファクタした時、可読性が一気に上がって「もっと早くやればよかった」って言ったメンバーが何人もいました。

パターン1:基本宣言と数値変換

Enumの基本形とDB連携。int値とのキャスト方法を押さえます。

//商品ステータスのEnum
public enum ProductStatus
{
    Active = 1,        //販売中
    Suspended = 2,     //一時停止
    Discontinued = 9   //廃番
}

//使い方
class Program
{
    static void Main()
    {
        // Enum → intキャスト(DB保存用)
        int dbValue = (int)ProductStatus.Active;   // 1

        // int → Enumキャスト(DB読み込み用)
        var status = (ProductStatus)1;              // ProductStatus.Active

        //比較
        if (status == ProductStatus.Active)
        {
            Console.WriteLine("販売中の商品");
        }

        // ToString()で名前取得
        string name = status.ToString();            // "Active"
    }
}

明示的に= 1 = 2 = 9のように数値を指定するのが業務系の鉄則。指定しないと0, 1, 2の連番になりますが、DBのint値とズレた時に事故になります。連番にしない理由は、過去DBの「9 =廃番」のような業務系特有の区分値マッピングを再現する必要があるためです。

パターン2: Description属性で画面表示

業務系で頻出するのが、Enumの値に対する日本語の画面表示名。属性+拡張メソッドで実装します。

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

public enum ProductStatus
{
    [Description("販売中")]
    Active = 1,

    [Description("一時停止")]
    Suspended = 2,

    [Description("廃番")]
    Discontinued = 9
}

//拡張メソッドでDescriptionを取得
public static class EnumExtensions
{
    public static string GetDescription(this Enum value)
    {
        FieldInfo field = value.GetType().GetField(value.ToString());
        if (field == null)return value.ToString();

        var attribute = field.GetCustomAttribute<DescriptionAttribute>();
        return attribute?.Description ?? value.ToString();
    }
}

//使い方
class Program
{
    static void Main()
    {
        var status = ProductStatus.Active;
        string label = status.GetDescription();  // "販売中"

        Console.WriteLine($"ステータス: {label}");
    }
}

Description属性+ Reflectionで取得する拡張メソッドの組み合わせ。WinFormsのLabel / ASP.NET MVCのRazor両方で使える定番パターンです。

ハマりポイント。Reflectionで属性を取得するコストはちょっとあるので、ループで大量呼び出しすると遅くなります。業務系ではDictionaryでキャッシュするか、初回呼び出し時のみ取得して以降は静的フィールドに保持するパターンが現実解です。

//キャッシュ版
public static class EnumDescriptionCache
{
    private static readonly Dictionary<Enum, string> _cache = new Dictionary<Enum, string>();

    public static string GetDescriptionCached(this Enum value)
    {
        if (_cache.TryGetValue(value, out var cached))return cached;

        var description = value.GetDescription();
        _cache[value] = description;
        return description;
    }
}

10万件のループでDescription取得する画面(業務系DataGridViewあるある)では、キャッシュ版でこんな感じに速度が変わります。

パターン3: [Flags]属性でビット組合せ

権限管理/オプション組合せ/ステータスフラグなど、複数値の組み合わせを1つの変数で扱う時に使います。

using System;

[Flags]
public enum Permission
{
    None = 0,
    Read = 1,            // 0001
    Write = 2,           // 0010
    Delete = 4,          // 0100
    Admin = 8,           // 1000
    ReadWrite = Read | Write,           // 0011
    All = Read | Write | Delete | Admin // 1111
}

class Program
{
    static void Main()
    {
        //複数権限をORで組み合わせ
        var userPermission = Permission.Read | Permission.Write;

        // HasFlagで権限チェック([Flags] 前提)
        if (userPermission.HasFlag(Permission.Read))
        {
            Console.WriteLine("読み取り権限あり");
        }

        //ビット演算で追加
        userPermission |= Permission.Delete;  // Read + Write + Delete

        //ビット演算で削除
        userPermission &= ~Permission.Write;  // Read + Delete

        // ToString()で人間可読表示([Flags] 前提)
        Console.WriteLine(userPermission);    // "Read, Delete"
    }
}

ハマりポイント3つ。

1つ目: [Flags]付け忘れると、HasFlagは動作するけどToString()で数値("3"等)が返ってきて画面表示できません。[Flags]は必須属性扱いで覚えると事故防げます。

2つ目:ビット演算用の値は2の累乗(1, 2, 4, 8, 16, …)で宣言しないとビットORで衝突します。Read = 3みたいな宣言をすると、Read | Write = 3Readだけ立ってる扱いになって挙動が壊れます。

3つ目: None = 0の宣言はHasFlag(None)が常にtrueになる挙動なので、Noneチェックは== Permission.Noneで書く必要があります。これも業務系で踏むハマりポイントです。

俺の現場でも、最初に権限管理を作った時に[Flags]忘れてHasFlagが想定通り動かなくて、半日ハマった経験があります。[Flags]Enum宣言の上に漏らさず書くとテンプレ化しておくのが業務SE鉄則!!

パターン4: GetValues / GetNamesでComboBoxバインド

WinForms / ASP.NETのComboBoxにEnumを動的バインドするパターン。Enum.GetValuesでループします。

using System;
using System.Linq;
using System.Windows.Forms;

public class StatusForm : Form
{
    private ComboBox statusComboBox;

    public StatusForm()
    {
        statusComboBox = new ComboBox();
        statusComboBox.DropDownStyle = ComboBoxStyle.DropDownList;

        // Enumの全値を取得してComboBoxにバインド
        var items = Enum.GetValues(typeof(ProductStatus))
            .Cast<ProductStatus>()
            .Select(s => new
            {
                Value = (int)s,
                Display = s.GetDescription()// Description属性で表示
            })
            .ToList();

        statusComboBox.DataSource = items;
        statusComboBox.ValueMember = "Value";
        statusComboBox.DisplayMember = "Display";

        Controls.Add(statusComboBox);
    }
}

ここではDescription属性で「販売中/一時停止/廃番」がComboBoxに表示されて、SelectedValueでint値が取れる構造になります。WinForms ComboBoxのDataBinding詳細はWinForms ComboBoxのDataBindingとSelectedValueで詰まる時の整理で書いてます。

Enum.GetNamesは文字列の配列("Active", "Suspended", "Discontinued")を返すバージョン。日本語表示が不要で英語名でOKな場合はこっちを使う、と覚えておくとこんな感じで使い分けできます。

パターン5: Parse / TryParse + IsDefinedでDB値検証

DBから読み込んだ文字列/数値をEnumに変換する時、業務系ではTryParse + IsDefinedの組み合わせが鉄則。

using System;

public enum ProductStatus
{
    Active = 1,
    Suspended = 2,
    Discontinued = 9
}

class Program
{
    static void Main()
    {
        //文字列→ Enum変換(DBの文字列カラムから読み込み)
        string dbStringValue = "Active";

        // ✅ TryParseで安全に変換(ignoreCase: trueで大文字小文字無視)
        if (Enum.TryParse<ProductStatus>(dbStringValue, ignoreCase: true, out var status))
        {
            Console.WriteLine($"変換成功: {status}");
        }
        else
        {
            Console.WriteLine("変換失敗");
        }

        //数値→ Enum変換+ IsDefinedで検証
        int dbIntValue = 999;  // DBに想定外の値が入ってた

        if (Enum.IsDefined(typeof(ProductStatus), dbIntValue))
        {
            var statusFromInt = (ProductStatus)dbIntValue;
            Console.WriteLine($"検証OK: {statusFromInt}");
        }
        else
        {
            Console.WriteLine($"未定義の値: {dbIntValue}");
            //エラーログ/デフォルト値処理など
        }
    }
}

ハマりポイント。(ProductStatus)999のキャスト自体は成功するというC# Enumの仕様。コンパイル時にも実行時にもエラーにならず、999という値がProductStatus型として後続処理に流れていきます。switch文で扱った時にどのcaseにも該当せずdefaultに流れて「想定外の値」が回るパターンが業務系のハマりです。

対処はEnum.IsDefinedで事前検証。DBから読み込んだ直後に検証する習慣化が、業務SE鉄則です。

switch文でのEnum網羅性— defaultの必須化

Enumをswitchで扱う時の最後のハマりポイント。

public static string GetStatusLabel(ProductStatus status)
{
    switch (status)
    {
        case ProductStatus.Active:
            return "販売中";
        case ProductStatus.Suspended:
            return "一時停止";
        case ProductStatus.Discontinued:
            return "廃番";
        default:
            // ✅ defaultは必須(未定義の値/新規追加Enum対応)
            throw new InvalidOperationException($"未定義のステータス: {status}");
    }
}

defaultを書かないと、Enumに新しい値(例: Reserved = 10)を追加した時に対応漏れに気付けません。throwするのが業務SE鉄則で、これで「未対応の値が来た」を即座に検出できます。

.NET Framework 4.7.2 / C# 7.3ではまだ使えないですが、C# 8.0以降ではswitch式(switch expression)で網羅性チェックが効くようになります。業務系で.NET Frameworkに縛られてる場合は、default明示の習慣化で対応するのが現実解です。

ハマりポイント整理

業務系で踏みやすいEnumハマりまとめ:

ハマりポイント 原因 対処
Magic Numberで書いて区分値混乱 Enum使わずint直書き Enum化
[Flags]忘れでHasFlag動かない 属性付け忘れ [Flags]を必須テンプレ化
DBから未定義の数値が来て後続処理が壊れる キャスト自体は成功 Enum.IsDefinedで検証
Enum.Parseで大文字小文字違いで例外 デフォルトは厳密一致 ignoreCase: true指定
switchで新規Enum値の対応漏れ default未実装 defaultでthrow必須化

まとめ

C# Enum業務系利用5パターン:

  1. 基本宣言と数値変換(int)EnumValue / (EnumType)dbValueでDB連携
  2. Description属性 —画面表示用の日本語名をReflectionで取得、キャッシュ推奨
  3. [Flags]属性 —ビットフラグでの組合せ管理(権限/オプション)
  4. GetValues / GetNames — ComboBox等への動的バインド
  5. TryParse + IsDefined — DB値の安全な変換と検証

Magic Number流派からEnum流派への移行は、業務系の保守性を一気に上げる施策。.NET Framework 4.7.2環境でも全機能使えるので、新規実装はEnum一択でいい感じに進められます。

よくある質問

Q1. C#で区分値管理にEnumとintどっちを使うべき?

A. Enum一択です。intでif (status == 1)のように書くMagic Number流派は、半年後に「1ってなんだっけ?」状態になって保守性が落ちます。Enumならif (status == ProductStatus.Active)で意図が明示されて、IDEの補完も効きます。DBのint値とのマッピングは(int)ProductStatus.Active / (ProductStatus)dbValueでキャストすればいいので、DB連携も問題なし。

Q2. EnumのDescription属性って何のために使う?

A.画面表示用の日本語名をEnumに紐付けるためです。[Description("販売中")]をEnumに付与し、GetDescription()拡張メソッドをReflectionで実装して、画面側でproductStatus.GetDescription()で日本語表示。WinForms / ASP.NET MVCどちらでも使える定番パターンで、リソースファイル併用で多言語化も可能です。

Q3. [Flags]属性は何のために付ける?

A. Enumをビットフラグとして使う時に付けます。[Flags] enum Permission { Read=1, Write=2, Delete=4 }で、Permission.Read | Permission.Writeの組み合わせを1つの変数で扱える。[Flags]がないと、ToString()で「3」と数値表示されるところが「Read, Write」と人間可読の文字列表示になります。

Q4. Enum.ParseとEnum.TryParseの使い分けは?

A.業務系ではTryParse一択です。Parseは存在しない文字列に対してFormatExceptionを投げるので、DBから読み込んだ値がEnumに存在しない場合に例外で落ちます。TryParseはboolを返すので、if (Enum.TryParse(dbValue, true, out var result))のパターンで安全に変換できます。さらにEnum.IsDefinedで定義済みの値かチェックすると、想定外の数値がDBに入ってた時の事故も防げます。

Q5. DBから来た数値がEnumに存在しない時のハマりは?

A. (MyEnum)999のように未定義の数値もキャスト自体は成功するので、後続処理で「想定外の値」が回るのが業務系のハマりです。対処はEnum.IsDefined(typeof(MyEnum), value)で事前検証する。Magic Number流派からEnum流派に移行した直後によく起きるハマりです。

関連記事

以上!

同じ罠でハマってる業務SE仲間いたら、どんどんシェア待ってるぜ!!


この記事が気に入ったら
いいねしてね!

どんどんシェア待ってるぜ!!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次