[C#] EnumのFlagsを安心して使う方法

更新: 2021-12-20 (月) 投稿: 2020-09-11 (金)

安心して使うには、0 は有効な値として使わず、None = 0、Default = 0 などと定義する。判定のHasFlag()は、.NET Core 2.1 から高速なので使い、それ以前ではビット演算で高速判定する。

環境

  • .NET 6.0.101
  • C# 10.0
  • Visual Studio 2022 Version 17.0.1
  • Windows 10 Pro 64bit 21H1 19043.1415

結論

定義

using System;

[Flags]
enum MyKeys // 命名は複数形 (Flags, Enumなどのサフィックスは付けない)
{
    None = 0,     // Default = 0, NoAccess = 0 など意味を持たない定義にする
    A = 1 << 0,   // 有効な値は1から始める
    B = 1 << 1,   // ビット左シフトで定義すると楽
    C = 1 << 2,   // シフト値を増やしていく
    AC = A | C,   // 複数の組み合わせ
}

// 以下の定義例1~4は、どれも同じ結果

// 1. 左シフト演算子 (シフト値をインクリメントするだけなのでオススメ)
[Flags]
enum MyKeys1
{
    None = 0,
    A = 1 << 0,
    B = 1 << 1,
    C = 1 << 2,
    D = 1 << 3,
    E = 1 << 4,
    F = 1 << 5,
    G = 1 << 6,
    H = 1 << 7,
    I = 1 << 8,
    J = 1 << 9,
    K = 1 << 10,
    L = 1 << 11,
    AC1 = A | C,
    AC2 = (1 << 0 | 1 << 2),
}

// 2. 16進数リテラル
[Flags]
enum MyKeys2
{
    None = 0,
    A = 0x0001,
    B = 0x0002,
    C = 0x0004,
    D = 0x0008,
    E = 0x0010,
    F = 0x0020,
    G = 0x0040,
    H = 0x0080,
    I = 0x0100,
    J = 0x0200,
    K = 0x0400,
    L = 0x0800,
    AC1 = A | C,
    AC2 = 0x0005, // 0x0001 + 0x0004 と同じ
}

// 3. 2進数リテラル (C# 7.0 以降)
// 0b_ のように 0b の後に _ を付けれるのは、C# 7.2 以降
[Flags]
enum MyKeys3
{
    None = 0,
    A = 0b_0000_0000_0001,
    B = 0b_0000_0000_0010,
    C = 0b_0000_0000_0100,
    D = 0b_0000_0000_1000,
    E = 0b_0000_0001_0000,
    F = 0b_0000_0010_0000,
    G = 0b_0000_0100_0000,
    H = 0b_0000_1000_0000,
    I = 0b_0001_0000_0000,
    J = 0b_0010_0000_0000,
    K = 0b_0100_0000_0000,
    L = 0b_1000_0000_0000,
    AC1 = A | C,
    AC2 = 0b_0101, // 0b_0100 | 0b_0001 と同じ
}

// 4. 10進数リテラル
[Flags]
enum MyKeys4
{
    None = 0,
    A = 1,
    B = 2,
    C = 4,
    D = 8,
    E = 16,
    F = 32,
    G = 64,
    H = 128,
    I = 256,
    J = 512,
    K = 1024,
    L = 2048,
    AC1 = A | C,
    AC2 = 5,
}

代入

var v = MyKeys.A;            // v = A

// フラグ追加 (複数指定可)
v |= MyKeys.B | MyKeys.C;    // v = A | B | C
// フラグ削除 (複数指定可)
v &= ~MyKeys.A & ~MyKeys.B ; // v = C

判定 (.NET Core 2.1 以降向け)

var ab = MyKeys.A | MyKeys.B;

// A を含む
ab.HasFlag(MyKeys.A);             // true
// A ,B 両方を含む (and判定)
ab.HasFlag(MyKeys.A | MyKeys.B);  // true
// A, B どちらかを含む (or判定)
(ab & (MyKeys.A | MyKeys.B)) > 0; // true

判定 (.NET Core 2.0以前、.NET Frameworkでの高速判定)

HasFlag() の性能が悪いため、ビット演算判定を使用します。

var ab = MyKeys.A | MyKeys.B;

// A を含む
(ab & MyKeys.A) == MyKeys.A;                           // true
// A ,B 両方を含む (and判定)
(ab & (MyKeys.A | MyKeys.B)) == (MyKeys.A | MyKeys.B); // true
// A, B どちらかを含む (or判定)
(ab & (MyKeys.A | MyKeys.B)) > 0;                      // true

// よく使うEnumは拡張メソッドを使うと楽できる
static class EnumExtensions
{
    public static bool HasBitFlag(this MyKeys value, MyKeys flag) => (value & flag) == flag;
}
// A を含む
ab.HasBitFlag(MyKeys.A);
// A ,B 両方を含む (and判定)
ab.HasBitFlag(MyKeys.A | MyKeys.B);

上記のサンプルコード
SharpLab

説明

0を意味ある定義にしない理由

[Flags]
enum ShibouFlags
{
    A = 0,  // A も有効なフラグのつもりで定義すると...
    B = 1
}

var b = ShibouFlags.B;

// ShibouFlags.A が含まれていないのに true になるので、正しく判定できず死亡
b.HasFlag(ShibouFlags.A); // true

(参考) .NET の定義例

// 0 の定義があるのでOK
public enum ModifierKeys
{
    None = 0,
    Alt = 1,
    Control = 2,
    Shift = 4,
    Windows = 8
}

// 0 の定義はないが、有効な値は1から始まる
[Flags]
public enum FileAccess
{
    Read = 1,
    Write = 2,
    ReadWrite = 3,
}

HasFlagのパフォーマンス

Enumは、ボクシングとリフレクションで実行される部分があるので遅いです。

.NET Framework(.NET Core 2.0以前)ではHasFlag()は遅いので、大人しく拡張メソッドを作った方が良いです。
ジェネリックで実装できないため、ボクシングも回避した高速判定の実装をするには、さらに工夫が必要になるためです。

static class EnumExtensions
{
    // Enumごとに拡張メソッドを作る
    public static bool HasBitFlag(this MyKeys value, MyKeys flag) => (value & flag) == flag;
    public static bool HasBitFlag(this ModifierKeys value, ModifierKeys flag) => (value & flag) == flag;

    // 以下の方法はオススメできない

    // 残念ながらジェネリックではコンパイルエラー
    static bool HasBitFlag<T>(this T value, T flag) where T : Enum
    {
        // CS0019: 演算子 '&' を 'T' と 'T' 型のオペランドに適用することはできません
        return (value & flag) == flag;
    }

    // Convert.ToUInt64() でボクシングが発生してしまう
    public static bool HasBitFlagUInt64<T>(this T value, T flag) where T : Enum
    {
        return (Convert.ToUInt64(value) & Convert.ToUInt64(flag)) == Convert.ToUInt64(flag);
    }

    // Enum型の引数では、ボクシングが発生してしまう
    public static bool HasBitFlag(this Enum value, Enum flag)
    {
    }
}

感謝

更新: 2021-12-20 (月) 投稿: 2020-09-11 (金)