C# で enum を扱うメソッドってどう書くべきなのだろう


C# の enum を扱うメソッドをジェネリックに書きたいと思うと,ジェネリック型制約に enum が使えないという問題に直面します[A]。何通りか書き方が考えらます。これを,フラグの列挙型の値を与えると,フラグを分解して返すメソッド DecomposeFlag を書きながら考えて見ます。

[Flags]
enum Flag { None = 0, A = 1, B = 2, C = 3, D = 4, E = 5 }

// DecomposeFlag(Flag.None) -> { Flag.None }
// DecomposeFlag(Flag.A) -> { Flag.A }
// DecomposeFlag(Flag.C) -> { Flag.A, Flags.B }
// DecomposeFlag(Flag.C | Flag.E) -> { Flag.A, Flag.B, Flags.D }
// DecomposeFlag(Flag.None | Flag.E) -> { Flag.A, Flags.D }

ジェネリックを諦める場合

public static ISet<Enum> DecomposeFlag(Enum value)
{
   if (value == null)
   {
      throw new ArgumentNullException("value");
   }
   var enumType = value.GetType();
   if (Convert.ToUInt64(value) == 0)
   {
      return new HashSet<Enum> { value };
   }
   var flags = (from Enum flag in Enum.GetValues(enumType)
                where Convert.ToUInt64(flag) != 0
                where value.HasFlag(flag)
                select flag).ToList();
   var result= from flag in flags
                where flags.All(e => e == flag || !flag.HasFlag(e))
                select flag
   return new HashSet<Enum>(result);
}

無理やりジェネリック

public static ISet<TEnum> DecomposeFlag<TEnum>(TEnum value)
   where TEnum : struct
{
   var enumType = typeof(TEnum);
   if (!enumType.IsEnum)
   {
      throw new NotSupportedException();
   }
   ulong bits = Convert.ToUInt64(value);
   if (bits == 0)
   {
      return new HashSet<TEnum> { value };
   }
   var flags = (from Enum flag in Enum.GetValues(enumType)
                let flagBits = Convert.ToUInt64(flag)
                where flagBits != 0
                where (bits & flagBits) == flagBits
                select flag).ToList();
   var result = from flag in flags
                let flagBits = Convert.ToUInt64(flag)
                where set.Select(Convert.ToUInt64).All(e => e == flagBits || (e | flagBits) != flagBits)
                select (TEnum)Convert.ChangeType(flag, enumType);
   return new HashSet<TEnum>(result);
}

ちなみに整数のまま扱うなら以下のように書くこともできます。

   var flags = (from Enum flag in Enum.GetValues(enumType)
                let flagBits = Convert.ToUInt64(flag)
                where flagBits != 0
                where (bits & flagBits) == flagBits
                select flagBits).ToList();
   Type underlyingType = enumType.GetEnumUnderlyingType();
   var result = from flagBits in flags
                where flags.All(e => e == flagBits || (e | flagBits) != flagBits)
                select (TEnum)Convert.ChangeType(flagBits, underlyingType);
   return new HashSet<TEnum>(result);
}

折衷案

public static ISet<TEnum> DecomposeFlag<TEnum>(Enum value)
   where TEnum : struct 
{
   if (value == null)
   {
      throw new ArgumentNullException("value");
   }
   var enumType = typeof(TEnum);
   if (enumType != value.GetType())
   {
      throw new NotSupportedException();
   }
   if (Convert.ToUInt64(value) == 0)
   {
      return new HashSet<TEnum> { (TEnum)Convert.ChangeType(value, enumType) };
   }
   var flags = (from Enum flag in Enum.GetValues(enumType)
                where Convert.ToUInt64(flag) != 0
                where value.HasFlag(flag)
                select flag).ToList();
   var removing = from flag in flags
                  where flags.All(e => e == flag || !flag.HasFlag(e))
                  select (TEnum)Convert.ChangeType(flag, enumType);
   return new HashSet<TEnum>(result);
}

結局

どれもいまいちに見えます。何か良い書き方はあるのでしょうか。

ここで唐突に F# に登場してもらいます。

let DecomposeFlag (value : 'T when 'T : enum<_>) : ISet<'T> =
   if Convert.ToUInt64(value) = 0uL
   then
      new HashSet<'T> ([value]) :> ISet<'T>
   else
      let has flag = (flag :> Enum).HasFlag
      let flags = Enum.GetValues typeof<'T>
                  |> Seq.cast<'T>
                  |> Seq.filter (fun flag -> Convert.ToUInt64 flag <> 0uL)
                  |> Seq.filter (has value)
      let result = flags
                   |> Seq.filter (fun flag -> Seq.forall (fun e -> e = flag || not (has flag e)) flags)
      new HashSet<'T> (result) :> ISet<'T>

なんだ最初から F# で書けば良かったのか,というお話でした[B]

更新履歴

  • [2012-01-08 12:20] F# 版の返り値の型を明示。その他瑣末な修正。
  • [2012-01-08 16:00] C# 版で Any を使っていたのを, F# 版 の forall にあわせて All を使うように変更。
  • [2012-01-09 06:35] ジェネリック版でジェネリック型が enum でないときに投げる例外を NoSupportedException に変更。

脚注

  1. delegate もそうですね。 []
  2. ちなみにこれを逆コンパイルして C# のコードに変換すると where T : Enum とかついてました。ずるい。 []