C#のEnumを(Javaのように)別の値を持たせるなど拡張する

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とか、なんだか大仰で、その辺があまり好きではない。

この拡張メソッドは、隙間を少し埋めてくれるかな、と思います。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive