ushortオブジェクトの列挙型アイテムを取得するためのジェネリックメソッド


5

アイデアは、enum collection内のすべての一致を見つけるために提供されるushortのビット単位のスキャンを実行することです。

私が開発したコードは次のとおりです:

public List<T> GetEnumItemsFromUshort<T>(ushort input) where T : struct, IComparable, IConvertible, IFormattable
    {
        var output = new List<T>();
        foreach (T enumValue in Enum.GetValues(typeof(T)))
        {
            if ((input & enumValue.ToUInt16(new CultureInfo("en-US"))) == enumValue.ToUInt16(new CultureInfo("en-US")))
            {
                output.Add(enumValue);
            }
        }
        return output;
    }

ご覧のとおり、私はジェネリック型を使用して戻り値の型を定義しています。

サイトを検索したところ、ジェネリック型を次のstruct, IComparable, IConvertible, IFormattableで制約すると、enumのように動作することがわかりました。

コードは、見つかった一致を含むために提供された列挙型のリストを生成します。

次に、入力値のスキャンに進み、マスクされたすべてのアイテム(前述のビット単位の演算)が一致するものを見つけます。

一致が見つかった場合は、列挙型のケースが戻りリストに追加されます。

これが良い実装か悪い実装か、そしてそれをより良くする方法を知りたいのですが、期待通りに機能しますが、最適化の余地があるように感じます。

例を示すために、次の列挙型と入力を想定します。

public ushort sample = 6;

public enum SampleCases
{
    Case1 = 0x1,
    Case2 = 0x2,
    Case3 = 0x4,        
    Case4 = 0x8,
}

このコードの出力は、Case2とCase 3の2つのアイテムを含むSampleCasesタイプのリストになります。両方(ビット単位)が提供されているサンプルに含まれているためです。これがあなたのための例として機能するかどうか教えてください。

7

Code can be greatly simplified but first I'd list few things:

1) If your enum is a bitmap then you should mark it with [Flags].

[Flags]
public enum SampleCases
{
    Case1 = 0x1,
    Case2 = 0x2,
    Case3 = 0x4,        
    Case4 = 0x8,
}

2) you shouldn't convert an enum value to ushort, enums default type is int and for [Flags] you may (easily?) run out of bits. If value is out of ushort range (your input) simply drop it, Convert.ToInt16() will throw OverflowException for out of range values.

3) You shouldn't create a new instance of CultureInfo every time you need it. It may be expansive. Moreover you're using en-US culture, you already have the CultureInfo.InvariantCulture.

3) To know if a value is valid you can use Enum.IsDefined().

To put all together:

public List<T> GetEnumItemsFromUshort<T>(ushort input)
    where T : struct, IComparable, IConvertible, IFormattable
{
    const int bitsInUInt16 = sizeof(ushort) * 8;

    return Enumerable.Range(0, bitsInUInt16 - 1)
        .Select(x => 1 << x)
        .Where(x => (input & x) == x && Enum.IsDefined(typeof(T), x))
        .Select(x => (T)Convert.ChangeType(x, typeof(T)))
        .ToList();
}

It's a minor change and a micro-optimization but I'd also change return type to IEnumerable<T>. If caller will need a list then it will be able to call ToList() itself and if it does not need it then you will avoid an unnecessary copy. Also first Select() may be dropped and embedded in Where() clause.

Note that 0 is not handled, you may want to include it or not; I'd leave it out because it's always matched unless you have input == 0. Note that to do not have a 0 default value for an enum is a dangerous practice, value types are 0 initialized and an uninitialized enum will have an unknown value.

Inverting point of view you may write:

public List<T> GetEnumItemsFromUshort<T>(ushort input)
    where T : struct, IComparable, IConvertible, IFormattable
{
    return Enum.GetValues(typeof(T))
        .Cast<object>()
        .Where(x => ((ushort)x & input) == (ushort)x)
        .Cast<T>()
        .ToList();
}

This 2nd version will also handle shared bits like GuestDefault, UserDefault and AdminDefault in this example:

[Flags]
enum SampleCases {
    None = 0,
    Read = 1,
    Write = 2,
    Execute = 4,
    GuestDefault = Read,
    UserDefault = Read | Execute,
    AdminDefault = Read | Write | Execute
}

Note the hacky .Cast<object>() to use .Where() over a non-generic enumeration (also paying boxing price). Here I tried to keep code short but I wouldn't implement this with LINQ (generated enumerator with yield return will be more clear, IMO). Also this code returns duplicates and aliases, if it's not what you want then you have to handle them (checking for distinct values and for multiple bits set on the same value).

Very last point: your constraints for generic parameter reduce wrong usage of your function but it doesn't really limit usage to enums: most primitive types implement same interfaces. I'd also add an explicit check:

if (!typeof(T).IsEnum))
    throw new ArgumentException("Template argument must be an enum type.");