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

更新: 2020-09-13 (日) 投稿: 2020-09-11 (金)

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

環境

  • Windows 10 Pro 64bit 1909
  • Visual Studio 2019 16.7.1
  • .NET Core 3.1
  • C# 8.0

結論

定義

using System;

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

代入

var v = MyKeys.A;

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

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

var ab = MyKeys.A | MyKeys.B;

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

ビット演算判定 (.NET Core 2.0以前、.NET Frameworkでの高速判定)

var ab = MyKeys.A | MyKeys.B;

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

// よく使うEnumは拡張メソッドを使うと楽
ab.HasBitFlag(MyKeys.A);             // A を含む
ab.HasBitFlag(MyKeys.A | MyKeys.B);  // A ,B 両方を含む (and判定)

static class EnumExtensions
{
    public static bool HasBitFlag(this MyKeys value, MyKeys flag) => (value & flag) == flag;
}

上記のサンプルコード
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)
    {
    }
}

感謝

更新: 2020-09-13 (日) 投稿: 2020-09-11 (金)