概要
この記事は属性を使用した列挙型の拡張とコードスニペットの続きです。
前回は列挙体と属性を用いてソースコード上で表に似た表現ができました。
ただし内部でリフレクションを使用しているため速度はあまり早くありません。
そこで今回はConcurrentDictionaryを使用したキャッシュを用いることで、これの高速化を行います。
実行結果やデモコードは前回と同じです。
コード
前回のコードを少し変更して、各種属性クラスとその取得拡張メソッド定義とその土台となる部分を別クラスに分けます。
まず土台となるクラスが以下です。
public static class EnumAttributeExtensionCore
{
/// <summary>
///属性ごとにキャッシュを作るためのジェネリッククラス
/// </summary>
/// <typeparam name="TAttribute">属性型</typeparam>
private static class EnumAttributeCache<TAttribute> where TAttribute : Attribute
{
private static ConcurrentDictionary<Enum, TAttribute> body = new ConcurrentDictionary<Enum, TAttribute>();
/// <summary>
/// ConcurrentDictionaryのGetOrAddを呼び出す
/// </summary>
internal static TAttribute GetOrAdd(Enum enumKey, Func<Enum, TAttribute> valueFactory)
=> body.GetOrAdd(enumKey, valueFactory);
}
/// <summary>
/// 特定の属性を取得する
/// </summary>
/// <typeparam name="TAttribute">属性型</typeparam>
public static TAttribute GetAttribute<TAttribute>(this Enum enumKey) where TAttribute : Attribute
{
//キャッシュに無かったら、リフレクションを用いて取得、キャッシュへの追加をして返す
return EnumAttributeCache<TAttribute>.GetOrAdd(enumKey, _ => enumKey.GetAttributeCore<TAttribute>());
}
/// <summary>
/// リフレクションを使用して特定の属性を取得する
/// </summary>
/// <typeparam name="TAttribute">属性型</typeparam>
public static TAttribute GetAttributeCore<TAttribute>(this Enum enumKey) where TAttribute : Attribute
{
//リフレクションを用いて列挙体の型から情報を取得
var fieldInfo = enumKey.GetType().GetField(enumKey.ToString());
//指定した属性のリスト
var attributes
= fieldInfo.GetCustomAttributes(typeof(TAttribute), false)
.Cast<TAttribute>();
//属性がなかった場合、nullを返す
if ((attributes?.Count() ?? 0) <= 0)
return null;
//同じ属性が複数含まれていても、最初のみ返す
return attributes.First();
}
}
前回のGetAttribute
メソッドはGetAttributeCore
に変更されています。
そして新しく定義したGetAttribute
メソッドの中身は以下です。
- キャッシュにあれば、それを使用
- キャッシュになければ、
GetAttributeCore
を呼び出し、キャッシュに追加
キャッシュの中身はConcurrentDictionaryです。
それを属性の型ごとに用意するために、クラス内ジェネリッククラスを使用しています。
なぜ直接ジェネリッククラスにせずクラス内クラスにしているかといいますと、拡張メソッドはジェネリッククラスでは定義できないからです。
次に属性クラスとその取得拡張メソッド定義です。
本題ではないので、Code属性とColor属性は省略します。
static class EnumExtension
{
#region ShortName属性
/// <summary>
/// ShortName属性
/// </summary>
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class ShortNameAttribute : Attribute
{
public String ShortName { get; private set; }
public ShortNameAttribute(String ShortName)
{
this.ShortName = ShortName;
}
}
/// <summary>
/// ShortName属性の取得(キャッシュ使用)
/// </summary>
public static String GetShortName(this Enum value)
=> value.GetAttribute<ShortNameAttribute>()?.ShortName
?? value.ToString();
/// <summary>
/// ShortName属性の取得(キャッシュ非使用)
/// </summary>
public static String GetShortNameCore(this Enum value)
{
return value.GetAttributeCore<ShortNameAttribute>()?.ShortName
?? value.ToString();
}
#endregion
#region Color属性
...
#region Code属性
...
}
ShortName取得メソッドにキャッシュを使用する通常のメソッドとパフォーマンス比較用のキャッシュ非使用のメソッドがあります。
パフォーマンス比較
上から順に
- 常にリフレクションを使用して列挙値から属性を取得
- キャッシュを使用して列挙値からから属性を取得
- 列挙値の文字列を取得
Method | Mean | Error | StdDev |
--------------------- |------------:|-----------:|-----------:|
GetFShortNameClassic | 3,510.10 ns | 58.9117 ns | 55.1061 ns |
GetFShortName | 49.33 ns | 0.4351 ns | 0.4070 ns |
GetFToString | 354.57 ns | 2.6516 ns | 2.4803 ns |
というわけで、2桁近く速くなりました。
参考
http://engineering.grani.jp/entry/2017/07/28/145035
http://neue.cc/2010/11/13_285.html
環境
Visual Studio 2017
.NET Framework 4.6
C#6