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 に変更。