ようこそ。睡眠不足なプログラマのチラ裏です。

C#でTypeSafeEnumを書いてみた

タイプセーフenumパターンというのがあったのですね。知りませんでした。
ジョシュア ・ブロック氏が「Effective Java」という本の中で提案していたものらしいです*1


いやはや、もっと幅広く勉強せんとなぁ・・。ということで、
id:bleis-tiftさんのエントリを参考にして、C#でタイプセーフなenumをちょっくら書いてみました。
http://d.hatena.ne.jp/bleis-tift/20080808/1218133517


AbstractなTypeSafeEnumを作る

ということで、さっそくAbstractなTypeSafeEnumをこさえました。
たぶん、こんな感じでよいでしょう。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace ClassLibrary1
{
    /// <summary>
    /// タイプセーフなEnum
    /// </summary>
    /// <typeparam name="T">自身を言及するために自身の型指定をしてね</typeparam>
    public abstract class TypeSafeEnum<T>
     :IComparable<TypeSafeEnum<T>>, IEquatable<TypeSafeEnum<T>> where T : TypeSafeEnum<T>
    {
        private static Dictionary<int, T> _values = new Dictionary<int, T>();
        private Func<string> _method = () => "";

        #region コンストラクタ
        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="method">リテラル文字列を返すFunc</param>
        protected TypeSafeEnum(Func<string> method){
            this._method = method;
        }

        #endregion

        #region プロパティ
        private string _name = "";
        public string Name { get { return _name; } }

        private int _ordinalNumber = 0;
        public int OrdinalNumber { get { return this._ordinalNumber; } }
        #endregion

        #region メソッド

        /// <summary>
        /// 初期処理
        /// </summary>
        public static void Initialize()
        {
            Type type = typeof(T);
            var fields = from f in type.GetFields(BindingFlags.GetField | BindingFlags.Public |
                                                  BindingFlags.Static | BindingFlags.DeclaredOnly)
                         where !f.IsLiteral
                         select f;

            foreach (var field in fields)
            {
                var t = (T)field.GetValue(null);
                t._name = field.Name;

                var items = (TypeSafeEnumOrdinalNumberAttribute[])field.GetCustomAttributes(typeof(TypeSafeEnumOrdinalNumberAttribute), false);
                if (items.Count() == 0) throw new FieldAccessException();
                if (!_values.ContainsKey(items[0].OrdinalNumber)) _values.Add(items[0].OrdinalNumber, t);
                t._ordinalNumber = items[0].OrdinalNumber;
            }
        }

        /// <summary>
        /// リテラル文字列を取得します。
        /// </summary>
        /// <returns></returns>
        public string GetLiteral()
        {
            return _method();
        }

        /// <summary>
        /// 数値からTypeSafeEnum(T)を取得します。
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public static T ValueOf(int id)
        {
            if (_values.ContainsKey(id)) return _values[id];
            throw new ArgumentOutOfRangeException();
        }

        /// <summary>
        /// TypeSafeEnumのすべての値のコレクションを取得します。
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<T> GetValues() 
        {
            if (_values.Count == 0) Initialize();
            foreach (var v in _values) yield return v.Value;
        }

        /// <summary>
        /// TypeSafeEnumのすべてのキーのコレクションを取得します。
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<int> GetKeys()
        {
            if (_values.Count == 0) Initialize();
            foreach (var v in _values) yield return v.Key;
        }

        #endregion

        #region オーバーライド
        public override string ToString()
        {
            return this._name;
        }

        public override bool Equals(object obj)
        {
            return this.Equals((TypeSafeEnum<T>)obj);
        }

        public override int GetHashCode()
        {
            return this._ordinalNumber.GetHashCode();
        }
        #endregion

        #region 演算子オーバーロード
        public static bool operator >(TypeSafeEnum<T> x, TypeSafeEnum<T> y)
        {
            return (x.CompareTo(y) > 0);
        }

        public static bool operator <(TypeSafeEnum<T> x, TypeSafeEnum<T> y)
        {
            return (x.CompareTo(y) < 0);
        }

        public static bool operator >=(TypeSafeEnum<T> x, TypeSafeEnum<T> y)
        {
            return (x.CompareTo(y) >= 0);
        }

        public static bool operator <=(TypeSafeEnum<T> x, TypeSafeEnum<T> y)
        {
            return (x.CompareTo(y) <= 0);
        }

        public static bool operator ==(TypeSafeEnum<T> x, TypeSafeEnum<T> y)
        {
            return x.Equals(y);
        }

        public static bool operator !=(TypeSafeEnum<T> x, TypeSafeEnum<T> y)
        {
            return (!x.Equals(y));
        }
        #endregion

        #region IComparable<T> メンバ
        public int CompareTo(TypeSafeEnum<T> other)
        {
            return this._ordinalNumber - other._ordinalNumber;
        }
        #endregion

        #region IEquatable<T> メンバ
        public bool Equals(TypeSafeEnum<T> other)
        {
            return (this._ordinalNumber == other._ordinalNumber);
        }
        #endregion
    }
}


OrdinalNumberを指定するTypeSafeEnumOrdinalNumberAttribute

続きまして、OrdinalNumberを指定するための属性を。

using System;

namespace ClassLibrary1
{
    /// <summary>
    /// TypeSafeEnumのOrdinalNumberを設定する属性
    /// </summary>
    [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
    public class TypeSafeEnumOrdinalNumberAttribute : Attribute
    {
        /// <summary>序数</summary>
        private int _ordinalNumber;
        public int OrdinalNumber { get { return this._ordinalNumber; } }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="ordinalNumber">序数</param>
        public TypeSafeEnumOrdinalNumberAttribute(int ordinalNumber)
        {
            this._ordinalNumber = ordinalNumber;
        }
    }
}

TypeSafeEnumを継承してみましょう。

では、実際にタイプセーフなEnumを使ってみましょう。

using System;
using System.Linq;
using ClassLibrary1;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main()
        {
            try
            {
                foreach (var i in NarutoNoSato.GetKeys())
                {
                    System.Console.WriteLine("[{0}] {1}",i.ToString(),NarutoNoSato.ValueOf(i));
                }

                Console.WriteLine();

                var sato = NarutoNoSato.GetValues();
                foreach (var s in sato)
                {
                    System.Console.WriteLine("[{0}] {1} \t\t{2}", s.OrdinalNumber, s.Name, s.GetLiteral());
                }

                Console.WriteLine();

                if (sato.ElementAt(0) != NarutoNoSato.Suna)
                {
                    System.Console.WriteLine("{0}と{1}は違うよ!", sato.ElementAt(0).GetLiteral(), NarutoNoSato.Suna.GetLiteral());
                }

                if (sato.ElementAt(0) == NarutoNoSato.Konoha)
                {
                    System.Console.WriteLine("{0}と{1}は等しいよ!", sato.ElementAt(0).GetLiteral(), NarutoNoSato.Konoha.GetLiteral());
                }
            }
            finally {
                Console.ReadLine();
            }
        }
    }

    /// <summary>
    /// NARUTOの里
    /// </summary>
    public sealed class NarutoNoSato : TypeSafeEnum<NarutoNoSato>
    {
        private NarutoNoSato(Func<string> method) : base(method){}

        static NarutoNoSato()
        {
            Initialize();
        }

        [TypeSafeEnumOrdinalNumberAttribute(0x0000)]
        public static readonly NarutoNoSato Konoha = new NarutoNoSato(() => "木ノ葉隠れの里");
        [TypeSafeEnumOrdinalNumberAttribute(0x0001)]
        public static readonly NarutoNoSato Suna = new NarutoNoSato(() => "砂隠れの里");
        [TypeSafeEnumOrdinalNumberAttribute(0x0002)]
        public static readonly NarutoNoSato Kiri = new NarutoNoSato(() => "霧隠れの里");
        [TypeSafeEnumOrdinalNumberAttribute(0x0004)]
        public static readonly NarutoNoSato Iwa = new NarutoNoSato(() => "岩隠れの里");
        [TypeSafeEnumOrdinalNumberAttribute(0x0008)]
        public static readonly NarutoNoSato Kumo = new NarutoNoSato(() => "雲隠れの里");
        [TypeSafeEnumOrdinalNumberAttribute(0x0010)]
        public static readonly NarutoNoSato Oto = new NarutoNoSato(() => "音隠れの里");
        [TypeSafeEnumOrdinalNumberAttribute(0x0020)]
        public static readonly NarutoNoSato Kusa = new NarutoNoSato(() => "草隠れの里");
        [TypeSafeEnumOrdinalNumberAttribute(0x0040)]
        public static readonly NarutoNoSato Ame = new NarutoNoSato(() => "雨隠れの里");
        [TypeSafeEnumOrdinalNumberAttribute(0x0080)]
        public static readonly NarutoNoSato Taki = new NarutoNoSato(() => "滝隠れの里");
        [TypeSafeEnumOrdinalNumberAttribute(0x0100)]
        public static readonly NarutoNoSato Yu = new NarutoNoSato(() => "湯隠れの里");
        [TypeSafeEnumOrdinalNumberAttribute(0x0200)]
        public static readonly NarutoNoSato Hoshi = new NarutoNoSato(() => "星隠れの里");
    }
}


実行結果

[0] Konoha
[1] Suna
[2] Kiri
[4] Iwa
[8] Kumo
[16] Oto
[32] Kusa
[64] Ame
[128] Taki
[256] Yu
[512] Hoshi


[0] Konoha 木ノ葉隠れの里
[1] Suna 砂隠れの里
[2] Kiri 霧隠れの里
[4] Iwa 岩隠れの里
[8] Kumo 雲隠れの里
[16] Oto 音隠れの里
[32] Kusa 草隠れの里
[64] Ame 雨隠れの里
[128] Taki 滝隠れの里
[256] Yu 湯隠れの里
[512] Hoshi 星隠れの里


木ノ葉隠れの里と砂隠れの里は違うよ!
木ノ葉隠れの里と木ノ葉隠れの里は等しいよ!

うん。できてるみたいですね(・ω・)
とういうか、今更なんだろうけどEffective Java読まなきゃだな・・。
いろいろためになりそうなことが載ってそう。

*1:後にJavaSE5.0で採用されたみたいです