C#でEnumに付与した属性と属性の値を取得する

タイトルの通り、C#で自分で作成したEnum型に属性を付与し、その属性を取得およびその属性の値の取得をしたいと思います。

属性をオブジェクトとして取得して内容を取り出す流れになります。

対象の型の準備

まず以下のように自作の属性を宣言していたとします。

// 自作の属性、Nameという文字列を持つ
public class NameAttribute : Attribute
{
    public string Name { get; set; }
    public NameAttribute(string name) => this.Name = name;
}

そして、Enum型へ上記の属性を付与して以下のような宣言(CustomEnum)を行います。Enumのメンバーそれぞれに上記属性を使用して名前を与えます。

また、参考までに属性を何も持たないEnum型(MyEnum)も宣言します。

// ★★★ カスタム属性を持つ列挙子
public enum CustomEnum
{
    [Name("AAA")] First,
    [Name("BBB")] Second,
    [Name("BBB")] Third
}

// カスタム属性を持たない列挙子
public enum MyEnum
{
    A,
    B,
    C
}

値を取得する拡張メソッドの作成

今回は、Enum型に対し拡張メソッドを宣言し付与されている属性を取得したいと思います。

処理内容はコメントの通りですが、実装したメソッドの説明は以下の通りです。

メソッド名 説明
GetAttrubute\ T で指定した型を Enum から取得する。見つからない場合nullを返す。
HasAttribute\ T で指定した型を Enum が持つ場合 true を返す。持たない場合 false を返す。
TryGetAttribute\ T で指定した型を Enum が持つ場合 true を返すと同時に out T type に値が設定される。持っていない場合 false を返し out T type は null となる。

処理内容ですが、Type.GetField 実行時型情報からメンバーの情報を取得するメソッドを呼ぶと FieldInfo というリフレクションの型が取れます。でそこ FieldInfo オブジェクトに実装されているカスタム属性を取得するメソッドを呼び属性をオブジェクトとして取り出しています。

// Enum型の拡張メソッドを定義するクラス
public static class EnumExtension
{
    // 指定した属性を指定したEnumが持っていたらtrue、持っていない場合falseを返す
    public static bool HasAttribute<T>(this Enum e) where T : Attribute
    {
        return GetAttrubute<T>(e) != null;
    }

    // Tで指定した属性をEnumから取得する、見つからない場合nullを返す
    public static T GetAttrubute<T>(Enum e) where T : Attribute
    {
        FieldInfo field = e.GetType().GetField(e.ToString());
        if (field.GetCustomAttribute<T>() is T att)
        {
            return att;
        }

        return null;
    }

    // Try - Parse パターンで指定した型を取得する
    public static bool TryGetAttribute<T>(this Enum e, out T type) where T : Attribute
    {
        FieldInfo field = e.GetType().GetField(e.ToString());
        if (field.GetCustomAttribute<T>() is T att)
        {
            type = att;
            return true;
        }

        type = null;
        return false;
    }
}

拡張メソッドの使い方

上記拡張メソッドの使い方は以下の通りです。Enumのメンバーにメソッドが追加される形になるのでメンバーを直接指定して呼び出します。

HasAttribute の使い方

// HasAttribute の使い方
internal static void Main(string[] args)
{
    bool contains = CustomEnum.First.HasAttribute<NameAttribute>(); // 属性を持っている列挙子を指定
    // > true

    contains = MyEnum.A.HasAttribute<NameAttribute>(); // 属性を持たない列挙子を指定
    // > false
}

GetAttrubute の使い方

// GetAttrubute の使い方
internal static void Main(string[] args)
{
    // 属性を持っている列挙子を指定 (持っている場合属性値も表示)
    NameAttribute attribute = CustomEnum.First.GetAttrubute<NameAttribute>();
    Console.WriteLine(attribute != null ? "取得できた = " + attribute.Name : "取得できない");
    // > 取得できた = AAA

     // 属性を持たない列挙子を指定
    attribute = MyEnum.A.GetAttrubute<NameAttribute>();
    Console.WriteLine(attribute != null ? "取得できた" : "取得できない");
    // > 取得できない
}

TryGetAttribute の使い方

C#のライブラリなどにある「Try ~ Paruse」パターンという考え方で実装したメソッドの使い方は以下の通りです。

internal static void Main(string[] args)
{
    if (CustomEnum.First.TryGetAttribute(out NameAttribute attribute))
    {
        Console.WriteLine("取得できた = " + attribute.Name);
    }
    else
    {
        Console.WriteLine("取得できない");
    }
    // > 取得できた = AAA
}

複数の属性が付与されていても取得できる

1つのEnumのメンバーに以下のように複数の属性がついているEnumがあったとしても今回の処理によって適切に属性オブジェクトを取得することができます。

// 複数のカスタム属性を持つ列挙子
public enum MultiCustomEnum
{
    [Description("0000")] [Name("AAA")] First,
    [Description("1111")] [Name("BBB")] Second,
    [Obsolete("Please do not use.")] [Description("2222")] [Name("BBB")] Third
}

以下のように型を指定すれば複数の属性がEnumに付与されていても適切に属性を取得することができます。

internal static void Main(string[] args)
{
    var att = MultiCustomEnum.Third.GetAttrubute<ObsoleteAttribute>();
    if (att != null)
    {
        Console.WriteLine("Obsoleteが付与されている。 = " + att.Message);
    }
    else
    {
        Console.WriteLine("Obsoleteが付与されていない。");
    }
    // > Obsoleteが付与されている。 = Please do not use.

    var att2 = MultiCustomEnum.Third.GetAttrubute<DescriptionAttribute>();
    if (att != null)
    {
        Console.WriteLine("Descriptionが付与されている。 = " + att2.Description);
    }
    else
    {
        Console.WriteLine("Descriptionが付与されていない。");
    }
    // > Descriptionが付与されている。 = 2222
}

パフォーマンスどうなのか?

属性を取得する処理は、リフレクションを使用して実行時型情報から属性を取り出しているためパフォーマンスが悪いのかもしれません。そこで、Dictionaryで対応関係のテーブルを作成した場合とのパフォーマンスの比較を行いました。

計測結果

先に計測結果を載せます。

それぞれ実行の実行結果は以下の通りです。100万回処理を繰り返し連続で10回実行した平均値となります。

項目 時間(ms)
拡張メソッド(今回のやつ)版 3251ms
テーブル(Dictionary)版 13ms

予想通りめちゃくちゃ遅いですね…

作っておいてなんですが、予想を超えた遅さです。カスタム属性から値を取ると処理速度が250倍遅いです。

パフォーマンス測定のコード

計測に使用したコードは以下の通りです。

internal static class AppMain
{
    private static Dictionary<CustomEnum, string> _table = new Dictionary<CustomEnum, string>()
    {
        { CustomEnum.First,  "AAA" },
        { CustomEnum.Second, "BBB" },
        { CustomEnum.Third,  "CCC" },
    };

    internal static void Main(string[] args)
    {
        int cnt = 1000000; // 100万回

        var sw1 = Stopwatch.StartNew();
        for (int i = 0; i < cnt; i++)
        {
            // 上記で紹介したコードでNameを取得
            string name = CustomEnum.First.GetAttrubute<NameAttribute>().Name;
        }
        sw1.Stop();

        var sw2 = Stopwatch.StartNew();
        for (int i = 0; i < cnt; i++)
        {
            // テーブルから値を引く
            string name = _table[CustomEnum.First];
        }
        sw2.Stop();

        Console.WriteLine($"拡張メソッド(今回のやつ)版 = {sw1.ElapsedMilliseconds}ms"); // 3251ms
        Console.WriteLine($"テーブル(Dictionary)版 = {sw2.ElapsedMilliseconds}ms"); // 13ms
    }
}

結論:Enumに属性を使用するべきか?

結論ですが、概ね以下の通りです。

  • 処理速度が優先の場合使用するべきではない。とても遅い。
  • 情報の整理には有用なので速度がクリティカルでない場面では使ってよい。

上記を踏まえた上で、初期化の時だけしか呼ばれないなど、1秒に1回も呼ばれない処理での利用は全く問題ありません。ただし秒間数万回の処理には絶対使用してはいけません。時と場合に依り記述を選択しましょう。

以上です。