より良いエンジニアを目指して

1日1つ。良くなる!上手くなる!

EnumにAttributeとして情報を付与して取得する

EnumにAttributeを付与して、Attributeを取得する方法です。

狙いとしては下記のようにenumにAttributeを付与しておき、付属情報を取得するというものです。

列挙型でありつつ、もう少し情報を付与したいというケースです。

        private enum Fruits
        {
            [EnumLabel("りんご")]
            Apple,
            [EnumLabel("ぶどう")]
            Grape
        }

        Console.WriteLine(EnumLabelAttribute.GetLabel(Fruits.Apple));  // りんご

他の方の記事を参考にしながら自分にあった形にすることにしました。ほぼ自分への備忘録です。

  • トリッキーな印象を受けるのでExtensionにしない
  • キャッシュする

カスタム属性の実装は、これまでしたことなかったので、属性ってこういうことね、と理解が深まります。

実装コード

    /// <summary>
    /// <see cref="Attribute"/>をキャッシュするクラスです。
    /// </summary>
    internal static class EnumAttributeCache<TAttribute> where TAttribute : Attribute
    {
        private static readonly ConcurrentDictionary<Enum, TAttribute> Cache = new ConcurrentDictionary<Enum, TAttribute>();

        /// <summary>
        /// <see cref="ConcurrentDictionary.GetOrAdd"/>を呼び出します。
        /// </summary>
        internal static TAttribute GetOrAdd(Enum enumKey, Func<Enum, TAttribute> valueFactory)
            => Cache.GetOrAdd(enumKey, valueFactory);
    }

    [System.AttributeUsage(System.AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
    public class EnumLabelAttribute : System.Attribute
    {

        public string Label;

        /// <inheritdoc />
        public EnumLabelAttribute(string label)
        {
            Label = label;
        }
        /// <summary>
        /// <see cref="Label"/>を取得します。
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static string GetLabel(Enum value)
        {
            return EnumAttributeCache<EnumLabelAttribute>.GetOrAdd(value, _ => GetLabelInternal(value)).Label;
        }
        /// <summary>
        /// キャッシュされていない初回時に<see cref="Label"/>を取得します。
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        private static EnumLabelAttribute GetLabelInternal(Enum value)
        {
            var fieldInfo = value.GetType().GetField(value.ToString());
            var attributes
                = fieldInfo.GetCustomAttributes(typeof(EnumLabelAttribute), false)
                    .Cast<EnumLabelAttribute>().ToList();
            if (!attributes.Any())
            {
                throw new InvalidOperationException($"[{value}]からはAttributeが取得できませんでした。");
            }
            return attributes.First();
        } 
    }

テスト・呼び出しコード

    [TestFixture]
    public class EnumLabelAttributeTest
    {
        private const string NoneLabelName = "なし";

        private enum TestEnum
        {
            [EnumLabel(NoneLabelName)]
            None,
            // あえてAttributeを付与しない
            NotLabel
        }
        /// <summary>
        /// <see cref="EnumLabelAttribute.GetLabel"/>をテストします。
        /// </summary>
        [Test]
        public void GetLabel()
        {
            Assert.AreEqual(EnumLabelAttribute.GetLabel(TestEnum.None),NoneLabelName,"初回取得");
            Assert.AreEqual(EnumLabelAttribute.GetLabel(TestEnum.None),NoneLabelName,"二度目にキャッシュから取得");
        }
        /// <summary>
        /// <see cref="EnumLabelAttribute"/>が指定されていない値に対して、<see cref="EnumLabelAttribute.GetLabel"/>が実行された異常系をテストします。
        /// </summary>
        [Test]
        public void GetLabelNoneAttribute()
        {
            Assert.Catch<InvalidOperationException>(() => { EnumLabelAttribute.GetLabel(TestEnum.NotLabel); });
        }
    }

f:id:rimever:20190506215817p:plain

参考記事

属性を使用した列挙型の拡張とコードスニペット - Qiita

列挙体からの属性取得の高速化 - Qiita

C#で文字列とEnumを関連付ける | 生存日記

カスタム属性の作成 (C#) | Microsoft Docs