atsukanrockのブログ

Microsoft系技術を中心にぼちぼち更新します

.NET Frameworkで、シリアル化可能なタイプセーフenumを実装する

はじめに

本エントリでは、.NET Frameworkでシリアル化可能なタイプセーフenumを実装する方法を述べる。
結論から先に述べると、今の私にはそのうまい方法が思いつかなかった。今の.NET Frameworkでは、泥臭くしか実装できないのではないか。その考えにいたった経緯を述べる。

タイプセーフenumとは

とりあえず「I'm Feeling Lucky」で検索されるページへリンクしておく。

J2SE 5.0 Tiger 虎の穴 Typesafe Enum

その特徴をいくつか挙げると、

  • 実体がただの整数である列挙型*1とは違い、通常のクラスである
  • クラスのクライアントは、クラスのインスタンスを生成することができない
  • クラスのインスタンスの個数が、完全に制御されている
  • クラス自身が、その全てのインスタンスを公開している

といったものである。.NET Frameworkの通常の列挙型にある欠点のいくつか*2を補うことができるので、列挙型を実装する場合には積極的に利用したいパターンである。

なぜシリアル化したいのか

ASP.NETでタイプセーフenumを使う場合、

  • ViewStateオブジェクト
  • Sessionオブジェクト

などにタイプセーフenumインスタンスを保存したくなることがあるだろう。これらのオブジェクトに格納できるのはシリアライズ可能な型のインスタンスだけなので*3、タイプセーフenumをシリアル化可能な型として実装する必要が出てくる。

シリアル化可能にすると

タイプセーフenumの様々な特徴のうち、シリアル化可能にすると実現が難しいのが、「インスタンスの個数が完全に制御されている」という点だ。なぜ実現が難しいのだろうか。

インスタンスの個数制御が難しい理由

.NETのシリアル化を実装するには大きく分けて以下の2つの方法がある。しかし、これら2つの方法のいずれであっても、上記の「インスタンスの個数が完全に制御されている」という特徴を実現するのが難しい。そのため、インスタンスの個数制御が難しい。

  1. 属性(Serializable属性など)による、基本的なシリアル化
  2. インタフェース(ISerializableインタフェースなど)を実装することによる、カスタムのシリアル化

インスタンスの個数を制御するためには、逆シリアル化によって新しいインスタンスが生成されてはいけない。さらに、逆シリアル化によって復元されるインスタンスを、個数制御下にあるインスタンスのうちのいずれかと同一のもの(ポインタが指す先が等しいもの)にしなければならない。

この要求を満たすためには、逆シリアル化の結果であるインスタンスを挿げ替えることができなくてはならない。しかし、.NETのシリアル化の仕組みではこの挿げ替えを実装することができない。以下で、実装方法ごとに検証する。

実装方法1 基本的なシリアル化

この実装方法では、Serializable属性をはじめとするシリアル化制御用の各属性(その多くはSystem.Runtime.Serialization名前空間に属する)を、シリアル化する型のメンバに適用することで、シリアル化を実現する。

型のメンバごとに、シリアル化するかしないかを制御することはできるが、挿げ替えを実装することはできない。

実装方法2 カスタムのシリアル化

この実装方法では、ISerializableインタフェースをはじめとするシリアル化制御用の各インタフェース(その多くはSystem.Runtime.Serialization名前空間に属する)を、シリアル化する型が実装することで、シリアル化を実現する。

基本的な実装

カスタムのシリアル化の基本的な実装は以下のようになる。

[Serializable]
public class Hoge : ISerializable
{
    // sealedクラスではないので、protected
    protected Hoge(SerializationInfo info, StreamingContext context)
    {
        _iValue = (Int32)info.GetValue("_iValue", typeof(Int32));
    }

    private Int32 _iValue;

    public Int32 Value
    {
        get { return _iValue; }
        set { _iValue = value; }
    }

    void ISerializable.GetObjectData(
        SerializationInfo info, StreamingContext context)
    {
        info.AddValue("_iValue", typeof(Int32));
    }
}

この方法では挿げ替えを実装できない。なぜなら、逆シリアル化の処理をコンストラクタで行っているからだ。コンストラクタでは、どうあがいても生成中のインスタンスを挿げ替えることはできない。

インスタンスの挿げ替えができる方法

これに対し、ISerializableインタフェースの概要ページのサンプルの方法であれば、インスタンスの挿げ替えを実装できる。リンク先には、シリアル化可能なシングルトンな型の実装サンプルがある。以下に、シリアル化可能なシングルトンのサンプルを記述する。

// 何もできない型だが、とりあえずのサンプルとして
[Serializable]
public sealed class Singleton : ISerializable
{
    private Singleton()
    {
    }

    private static readonly Singleton __instance = new Singleton();

    public Singleton Instance
    {
        get { return __instance; }
    }

    [SecurityPermissionAttribute(SecurityAction.LinkDemand,
        Flags=SecurityPermissionFlag.SerializationFormatter)]
    void ISerializable.GetObjectData(
        SerializationInfo info, StreamingContext context)
    {
        info.SetType(typeof(Helper)); // (1)
    }

    [Serializable]
    private class Helper : IObjectReference // (2)
    {
        public Object GetRealObject(StreamingContext context) // (3)
        {
            return Singleton.Instance;
        }
    }
}

(1) SerializationInfoクラスのSetTypeメソッドを使えば、型を逆シリアル化する際に、逆シリアル化される型以外の型が使用されるようになる。

(2) (1)で指定した型は、IObjectReferenceインタフェースを実装する。

(3) IObjectReferenceインタフェースを実装した型が逆シリアル化の処理をするのが、GetRealObjectメソッドである。

しかし、このGetRealObjectメソッドの引数にSerializationInfoがない。それがどういうことかというと、このメソッドではシリアル化されたインスタンスについての情報を知ることができないということだ。StreamingContextが渡されるが、このオブジェクトでは知ることができない。その結果、逆シリアル化するインスタンス1つにつき、IObjectReferenceインタフェースを実装した型が1つ必要になる。

この問題については「株式会社コムラッド/オブジェクトのシリアライズ」の「問題点2の解決方法」でも言及されている。リンク先のページはかなり古いのだが、どうやら現在の.NET Frameworkでもこの問題は解決されていないようだ。

タイプセーフenumを実装するとなると

問題はあるにしろ、挿げ替えが実装できることがわかったので、実装してみることにする。すると、サンプルは以下のようになるだろう。

// タイプセーフenumの基底
[Serializable]
public abstract class TypeSafeEnum<T> : ISerializable
    where T : TypeSafeEnum<T> // Tにはタイプセーフenum自身の型を指定する
{
    protected TypeSafeEnum()
    {
        if (__instances.Count >= MAX_INSTANCE_COUNT)
        {
            throw new NotSupportedException("生成できるインスタンス数の上限に達しました。");
        }
        __instances.Add(this);
    }

    private const Int32 MAX_INSTANCE_COUNT = 100;

    private static Int32 __iSerial = 0; // (1)

    private static readonly List<TypeSafeEnum<T>> __instances
        = new List<TypeSafeEnum<T>>(); // (2)

    private static readonly Type[] __refTypes = new Type[] {
        typeof(Ref0<T>), typeof(Ref1<T>)/*,  Ref2...98 */, typeof(Ref99<T>),
    }; // (3)
    
    private readonly Int32 _iSerial = __iSerial++; // (1)

    void ISerializable.GetObjectData(
        SerializationInfo info, StreamingContext context)
    {
        info.SetType(__refTypes[_iSerial]); // (3)
    }

    [Serializable]
    private class Ref0<U> : IObjectReference
        where U : TypeSafeEnum<U>
    {
        public Object GetRealObject(StreamingContext context)
        {
            return TypeSafeEnum<U>.__instances[0]; // (2)
        }
    }

    [Serializable]
    private class Ref1<U> : IObjectReference
        where U : TypeSafeEnum<U>
    {
        public Object GetRealObject(StreamingContext context)
        {
            return TypeSafeEnum<U>.__instances[1]; // (2)
        }
    }

    // Ref2...98の実装がある - (4)

    [Serializable]
    private class Ref99<U> : IObjectReference
        where U : TypeSafeEnum<U>
    {
        public Object GetRealObject(StreamingContext context)
        {
            return TypeSafeEnum<U>.__instances[99]; // (2)
        }
    }
}

// タイプセーフenumの具象クラスのサンプル
[Serializable]
public sealed class Hoge : TypeSafeEnum<Hoge> // (1)
{
    private Hoge(Int32 iValue)
    {
        _iValue = iValue;
    }

    public static readonly Hoge Hoge100 = new Hoge(100);

    public static readonly Hoge Hoge200 = new Hoge(200);

    [NonSerialized]
    private readonly Int32 _iValue;

    public Int32 Value
    {
        get { return _iValue; }
    }
}

(1) TypeSafeEnumのTを指定し、そこから派生してタイプセーフenumを実装する。そうして実装されたタイプセーフenumは、具象クラスごとに0から始まるインスタンスのシリアル番号が振られる。TypeSafeEnumジェネリッククラスであるおかげで、型引数Tが異ればstaticフィールドは別の値となる。もしTypeSafeEnumを非ジェネリッククラスとしたら、例えば上記のコードで新たにFugaクラスを実装した場合、Fugaクラスのインスタンスのシリアル番号が2から始まることになる*4

(2) タイプセーフenumの全てのインスタンスを保持するリストを用意する。リストの要素番号とインスタンスのシリアル番号が一致する。

(3) ISerializable.GetObjectDataメソッド内でSerializationInfo.SetTypeメソッドを呼び出す。インスタンスのシリアル番号ごとに異なる、専用の(IObjectReferenceインタフェースを実装した)型を指定することになる。

(4) TypeSafeEnumはタイプセーフenumの実装に汎用的に使えるクラスなので、TypeSafeEnumから見ると実装するタイプセーフenumインスタンス数がどれだけあるかわからない。このことと(3)により、100個も同じような型を定義することになっている。当然、100個あっても足りない場合があるかもしれない。

本エントリの冒頭で述べたとおり、非常に泥臭い実装となってしまった。しかも、インスタンス数の制限付きである。しかし、現在の.NET Frameworkではこのようにしかできないのではないか。

そんな実装は嫌だ!!

上記の実装方法はとにかく泥臭い。「そんな実装方法は嫌だ!!」という場合、インスタンスの個数が制御「されない」がそれっぽく動作させる方法がある。==演算子オーバーロードするのだ。

インスタンスの個数が制御「される」タイプセーフenumでは、インスタンス同士の比較は参照の比較で行って良い。ある値を表すインスタンスが必ず1つしかないためだ。

また、==演算子は、オーバーロードされない限り参照の比較を行う。

これらの理由により、タイプセーフenum同士の比較は==演算子で行うのが普通である。なので、ある値を表すインスタンスが2つ以上あると普通は困る。しかしここで演算子オーバーロードを使えば、それっぽく動作するタイプセーフenumとなる。

C#でTypeSafeEnumを書いてみた - Bug Catharsis」に、演算子オーバーロードしたタイプセーフenumの良いサンプルがあるので参照されたい。リンク先のサンプルにSerializable属性を付ければ、シリアル化できてそれっぽく動作するタイプセーフenumの完成である。

なお、リンク先のサンプルはシリアル化されることを想定したものではない。なので、シリアル化された場合にインスタンス数が制御「される」必要などない。サンプル自体は、シリアル化されないという前提の下では全く問題のないものであることを断わっておく。

Javaなら…

ちなみにJavaであれば、以上で述べたような悩みとは無縁である。まず、Java 5.0以降なら言語に組み込まれた列挙型がタイプセーフenumだ。また、シリアル化の仕組みが.NETとは異なっており、「挿げ替え」を簡単に実装することができる

結論

こう言ってしまうと元も子もないのだが、.NETではタイプセーフenumのシリアル化を目指さない方が良いのではないか。

タイプセーフenumを実装する場合、大抵の場合Int32やStringなどでそのインスタンスを識別可能とするはずだ。ViewStateやSessionにタイプセーフenumを格納したいのであれば、インスタンスを識別するための値を格納しておけば良いのである。ここで妥協するのは悔しいところだが、タイプセーフenumをシリアル化可能にするための労力には釣り合わない*5

*1:例えば.NETの通常の列挙型

*2:本エントリの主旨から外れるため、その欠点が何であるかは述べない

*3:ViewStateに限っては、カスタム型コンバータという手もあるが

*4:厳密には、型のロード順による

*5:少なくとも私にとっては