やりたいこと
C#において、列挙型を拡張することで下記のような表を表現したい場合があります。
列挙値 | 短縮名 | 色 | コード |
---|---|---|---|
Unknown | - | 無色 | -1 |
Male | M | 青 | 0 |
Female | F | 赤 | 1 |
Other | X | 紫 | 2 |
クラスやDictionaryを使用しても良いですが、
列挙型で表現した方が定義がコンパクトにまとまって見やすいというメリットがあります。
属性を含んだ列挙型定義
using System;
using System.Drawing;
using static ConsoleApplication1.EnumExtension;
namespace ConsoleApplication1
{
enum Gender
{
Unknown,
[ShortName("M"), Color(KnownColor.Blue), Code(0)]
Male,
[ShortName("F"), Color(KnownColor.Red), Code(1)]
Female,
[ShortName("X"), Color(KnownColor.Purple), Code(2)]
Other,
}
列挙型の定義が表の表現に近いことがわかるかと思います。
この程度の量ならswitch-case文などの分岐で行っても問題はないですが、
これが30種類以上あるコマンド表などになると死にます。
使用方法
実行コードです。
class Program
{
static void Main(string[] args)
{
foreach (Gender g in Enum.GetValues(typeof(Gender)))
{
Console.WriteLine($"Gender:{g}, 短縮名:{g.GetShortName()}, 色:{g.GetColor()}, コード:{g.GetCode()}");
}
Console.ReadKey();
}
}
実行結果です。
Gender:Unknown, 短縮名:Unknown, 色:Color [Empty], コード:-1
Gender:Male, 短縮名:M, 色:Color [Blue], コード:0
Gender:Female, 短縮名:F, 色:Color [Red], コード:1
Gender:Other, 短縮名:X, 色:Color [Purple], コード:2
属性の取得も拡張メソッドを使用することでインテリセンスを使用しながら、素直に書けます。
ヘルパークラス
属性の定義と取り出しのためのヘルパークラスです。
内容は各属性ごとの(クラス定義+属性内部値拡張メソッド)、
内部で使用されるジェネリック化された属性取り出し拡張メソッドです。
static class EnumExtension
{
#region Color属性
/// <summary>
/// Color属性
/// </summary>
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class ColorAttribute : Attribute
{
public Color Color { get; private set; }
public ColorAttribute(KnownColor color)
{
this.Color = Color.FromKnownColor(color);
}
}
/// <summary>
/// Color属性の取得
/// </summary>
public static Color GetColor(this Enum value)
=> value.GetAttribute<ColorAttribute>()?.Color
?? Color.Empty;
#endregion
#region Code属性
/// <summary>
/// Code属性
/// </summary>
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class CodeAttribute : Attribute
{
public int Code { get; private set; }
public CodeAttribute(int Code)
{
this.Code = Code;
}
}
/// <summary>
/// Code属性の取得
/// </summary>
public static int GetCode(this Enum value)
=> value.GetAttribute<CodeAttribute>()?.Code
?? -1;
#endregion
#region 短縮名属性
/// <summary>
/// 短縮名属性
/// </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>
/// 短縮名属性の取得
/// </summary>
public static string GetShortName(this Enum value)
=> value.GetAttribute<ShortNameAttribute>()?.ShortName
?? value.ToString();
#endregion
/// <summary>
/// 特定の属性を取得する
/// </summary>
/// <typeparam name="TAttribute">属性型</typeparam>
private static TAttribute GetAttribute<TAttribute>(this Enum value) where TAttribute : Attribute
{
//リフレクションを用いて列挙体の型から情報を取得
var fieldInfo = value.GetType().GetField(value.ToString());
//指定した属性のリスト
var attributes
= fieldInfo.GetCustomAttributes(typeof(TAttribute), false)
.Cast<TAttribute>();
//属性がなかった場合、空を返す
if ((attributes?.Count() ?? 0) <= 0)
return null;
//同じ属性が複数含まれていても、最初のみ返す
return attributes.First();
}
}
#コードスニペット
このように列挙型の定義と使用する側は短くなりますが、
代償としてヘルパークラスは長くなります。
そこで任意の属性とそれを所得するコードスニペットを作りました。
・属性の名前(ex. ShortName)
・属性の型(ex. String)
・属性が定義されていなかった場合の値(ex. 列挙値の文字列)
をタブ移動で入力できます。
EnumAttribute.snippet -GitHub
ショートカット入力は「attrg」です。
VisualStudioのコードスニペットマネージャーから追加できます。
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>Attribute and Getter</Title>
<Author>soi</Author>
<Shortcut>attrg</Shortcut>
</Header>
<Snippet>
<Declarations>
<Literal>
<ID>name</ID>
<ToolTip>属性の名前</ToolTip>
<Default>My</Default>
</Literal>
<Literal>
<ID>type</ID>
<ToolTip>属性の型</ToolTip>
<Default>String</Default>
</Literal>
<Literal>
<ID>defaultValue</ID>
<ToolTip>属性が定義されていなかった場合の値</ToolTip>
<Default>value.ToString()</Default>
</Literal>
</Declarations>
<Code Language="csharp">
<![CDATA[
#region $name$属性
/// <summary>
/// $name$属性
/// </summary>
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class $name$Attribute : Attribute
{
public $type$ $name$ { get; private set; }
public $name$Attribute($type$ $name$)
{
this.$name$ = $name$;
}
}
/// <summary>
/// $name$属性の取得
/// </summary>
public static $type$ Get$name$(this Enum value)
=> value.GetAttribute<$name$Attribute>()?.$name$
?? $defaultValue$;
#endregion
]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
これでオレオレ属性を好きなだけ追加して下さい。
#注意点
速度
リフレクションを使用していますので、速度は遅いです。
気になる場合はneue ccさんが紹介しているキャッシュを使用したアクセスを使用するのが良いです。
※追記 高速版を書きました。⇒ 列挙体からの属性取得の高速化
定義の強制
属性の定義を強制は出来ません。
上記例でもUnknownには属性が付いていません、その場合は拡張メソッド内の規定値が返されます。
#環境
Visual Studio 2015
.NET Framework 4.6
C#6
以下のページを参考にさせて頂きました。
属性に設定したenumの値に対応する文字列を取得する
C# の enum に関連する小技。