C#のEnumを(Javaのように)別の値を持たせるなど拡張する
- 2010-11-13
Enumに文字列を与えたいというのは少なくなくよくあると思います。例えばFruits.Appleには.ToString()したら「リンゴ」と出て欲しいなー、とか。それならFruits.リンゴと、日本語名つければ?というのはごもっとも。でも、同時に「林檎」とも付けたいなー、とかも思ってしまったりするわけです。しません?Java→C#な人が一番不満に思うのはEnumのようですし(JavaのEnumは高機能!)。
例えばこんな風にかけたらいいな、って。
// こうやって属性定義するだけ!
public enum Color
{
[Japanese("黒"), Hex("000000"), Rgb(0, 0, 0)]
Black,
[Japanese("白"), Hex("FFFFFF"), Rgb(255, 255, 255)]
White,
[Japanese("赤"), Hex("FF0000"), Rgb(255, 0, 0)]
Red
}
class Program
{
static void Main(string[] args)
{
var red = Color.Red;
Console.WriteLine(red.ToHex()); // FF0000
Console.WriteLine(red.ToJpnName()); // 赤
Console.WriteLine(red.ToRgb()); // 255000000
}
}
んね、非常にすっきり定義出来て幸せ度高い。これをもし普通に書くならば
interface IColor
{
string EngName { get; }
string JpnName { get; }
string Rgb { get; }
string Hex { get; }
}
public class Red : IColor
{
public string EngName { get { return "Red"; } }
public string JpnName { get { return "赤"; } }
public string Rgb { get { return "255000000"; } }
public string Hex { get { return "FF0000"; } }
}
といった感じになって面倒くさいことこの上ない(いや別にこれクラスの意味あんまなくてnew Color("Red", "赤",..)といった感じにインスタンスでいいやん、という感じではありますががが。Enumはswitch要因なとこがメインなので、そもそもColorという例が良くないかしら...) まあともかく、Enumは素晴らしい。属性もまた素晴らしい。んで、Enumへの別名などの定義の仕組みは非常に単純で、個別のEnumへ拡張メソッドを定義しているだけです。Enumと拡張メソッドは相性が良い。
public static class ColorExtensions
{
private static Dictionary<Color, RgbAttribute> rgbCache;
private static Dictionary<Color, HexAttribute> hexCache;
private static Dictionary<Color, JapaneseAttribute> jpnCache;
static ColorExtensions()
{
// Enumから属性と値を取り出す。
// この部分は汎用的に使えるようユーティリティクラスに隔離してもいいかもですね。
var type = typeof(Color);
var lookup = type.GetFields()
.Where(fi => fi.FieldType == type)
.SelectMany(fi => fi.GetCustomAttributes(false),
(fi, Attribute) => new { Color = (Color)fi.GetValue(null), Attribute })
.ToLookup(a => a.Attribute.GetType());
// キャッシュに突っ込む
jpnCache = lookup[typeof(JapaneseAttribute)].ToDictionary(a => a.Color, a => (JapaneseAttribute)a.Attribute);
hexCache = lookup[typeof(HexAttribute)].ToDictionary(a => a.Color, a => (HexAttribute)a.Attribute);
rgbCache = lookup[typeof(RgbAttribute)].ToDictionary(a => a.Color, a => (RgbAttribute)a.Attribute);
}
public static string ToJpnName(this Color color)
{
return jpnCache[color].Value;
}
public static string ToHex(this Color color)
{
return hexCache[color].Value;
}
public static string ToRgb(this Color color)
{
var rgb = rgbCache[color];
return string.Format("{0:D3}{1:D3}{2:D3}", rgb.R, rgb.G, rgb.B);
}
}
// 属性などり
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class JapaneseAttribute : Attribute
{
public string Value { get; private set; }
public JapaneseAttribute(string value)
{
Value = value;
}
}
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class RgbAttribute : Attribute
{
public int R { get; private set; }
public int G { get; private set; }
public int B { get; private set; }
public RgbAttribute(int r, int g, int b)
{
R = r;
G = g;
B = b;
}
}
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class HexAttribute : Attribute
{
public string Value { get; private set; }
public HexAttribute(string value)
{
Value = value;
}
}
少し、処理が多いですかね。属性定義の部分はスルーでいいので、本質的にはToJpnNameなどの拡張メソッド定義の部分と、静的コンストラクタでEnumから属性と値を取り出してる部分だけなので、そんなでもないです。静的コンストラクタを使っているのは、ここでDictionaryに突っ込むことで、毎回リフレクションがゴニョゴニョと走り出すのを避けています。
属性は固定で複数を付加するなら名前付き引数でもいいですね。
まとめ
まとめというか、このネタは以前にも enumの日本語別名とか三項演算子ネストとか という記事で書いてたりはしたのですが、もう少し実用的になるような形にしました。
C#のEnumは、私は好きです。素朴というか、シンプルで使いやすいですよね。Visual Studioでswitch使うと一気に列挙してくれるのもいいし、勿論ビットフラグも。機能だけならクラス立てれば代替出来ないこともないけれど、色々と遥かに面倒くさくなる。Enumはシンプルに、すっきり書けるのが素敵。Javaは高機能(コンパイル時にクラス作るわけですしね)なのはいいとしても、EnumSetとか、なんだか大仰で、その辺があまり好きではない。
この拡張メソッドは、隙間を少し埋めてくれるかな、と思います。