【C#】ビットフィールドのenumから値をすべて取り出す

ビットフィールドして宣言された enum (=FlagsAttribute が付与されているenum型) に複数の値が指定されている場合に設定されてるすべての値を別々に取り出す実装例の紹介です。

変数内メンバーを全て列挙する

例えば以下のようにビットフィールドとして宣言された enum があります。

// 以下のようにenumが定義されている

[Flags]
public enum Sample
{
    Apple      = 0b0001,
    Orange     = 0b0010,
    Pineapple  = 0b0100,
    Grapefruit = 0b1000,
}

上記の enum はひとつの変数に複数の値を以下のように保持できます。

var s = Sample.Apple | Sample.Orange;

この時変数 s に設定されている Sample のメンバーを別々にすべて取り出す実装例は以下の通りです。

public static class EnumExtension
{
    public static IEnumerable<T> GetFlagMembers<T>(this T self) where T : struct, Enum, IConvertible
    {
        var att = typeof(T).GetCustomAttributes<FlagsAttribute>();
        if (att is null)
        {
            throw new NotSupportedException("This type is 'FlagsAttribute' not specified.");
        }

        ulong a = self.ToUInt64(System.Globalization.CultureInfo.InvariantCulture);

        foreach (T m in Enum.GetValues<T>())
        {
            ulong b = m.ToUInt64(System.Globalization.CultureInfo.InvariantCulture);
            if ((b & a) == b)
            {
                yield return m;
            }
        }
    }
}

パフォーマンスを追求するなら以下のように具体的に処理方がややに早いです。(.NET Core5で確認)

が、メンテを怠ると不具合が出る可能性があるのであまりおすすめはできません。

// こっちの方が15%弱くらい早い
private static readonly Sample[] items = new Sample[]
{
    Sample.Apple,
    Sample.Orange,
    Sample.Pineapple,
    Sample.Grapefruit,
};

public static IEnumerable<Sample> GetFlagMembers(Sample value)
{
    foreach (var s in items)
    {
        if ((value & s) == s)
        {
            yield return s;
        }
    }
}

これを以下のように使用するとメンバーが取り出せます。

var s = Sample.Apple | Sample.Orange;

// ビットを分解して個々のメンバーとして取得できる
foreach (var item in s.GetFlagMembers())
{
    Console.WriteLine($"{item}");
    // > Apple
    // > Orange
}

ビット演算の基本操作(判定・追加・削除)

以下、余談ですがビット演算は以下のように判定・追加・削除ができます。

enum 型以外も同じように操作できます。

var s = Sample.Apple | Sample.Orange;

// フラグの有無:単一の値
bool contains = Sample.HasFlag(Sample.Apple);
contains = (s & Sample.Apple) == Sample.Apple; // こっちの方が動作が軽い

// フラグの有無:AND判定
Sample s2 = Sample.Apple | Sample.Pineapple;
contains = (s & s2) == s2; // Apple と Pineapple の両方を持っているかどうかの判定

// フラグの有無:OR判定
contains = (s & s2) != 0; // Apple もしくは Pineapple を持っているかどうかの判定

// フラグの追加
s = s | Sample.Pineapple; // s に Pineapple を追加
s |= Sample.Pineapple;

// フラグの削除
s = s & ~Sample.Pineapple; // s から Pineapple を削除
s &= ~Sample.Pineapple;