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