atsukanrockのブログ

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

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

はじめに

以前のエントリで、今の.NET Frameworkでは、シリアル化可能なタイプセーフenumを、スマートに実装する方法はないのではないか、と述べた。それに対し、ピコピコさんから「できるよ」というコメントをいただいた。

そのアドバイスに従い実装してみたところ、見事、シリアル化可能なタイプセーフenumの実装に成功した*1。本エントリでは、その方法を示す。

なお、問題解決に繋がるコメントをいただいたピコピコさんに感謝します。ピコピコさんのコメントがなければ、私が本エントリでの実装方法に辿り着く可能性は、極めて低かったです。

また、以前のエントリにトラックバックをいただいたid:zeclさんにも感謝します。私のブログを読んでいる人は少ないはずなので、zeclさんからのトラックバックがなければ、ピコピコさんが私のブログを読まれることもなかったかもしれません。

実装方法

さて、本題だ。どうやって実装すれば良いのか。まずは、以前のエントリに対するピコピコさんのコメントを、以下に引用する。

IObjectReferenceと同時にISerializableを実装すれば
コンストラクタ(SerializationInfo info, StreamingContext context)で
SerializationInfoを受け取れます。

何と、そんな仕様があるとは!!

ピコピコさんがどうやって上記の仕様を知ったのか、すごく気になる。IObjectReferenceインターフェイスのドキュメントは、はっきり言って私には意味不明な文章だが、実はこれを読みとくと、上記の仕様に繋がるのだろうか…。

上記のコメントのとおりだとすれば、話は簡単である。以下の手順で実装できそうだ。

  1. IObjectReferenceの実装クラスで、ISerializableも同時に実装する
  2. コンストラクタで受け取ったSerializationInfoから逆シリアル化に必要な情報を取り出し、インスタンスフィールドに保持しておく
  3. GetRealObjectでは、コンストラクタで取り出しておいた情報を使う

サンプル

以下にサンプルを示す。今回は、NUnitでのテストコードも付ける。テストコードについてのコメントは、以前のエントリを参照されたい。

#region テストクラス

[TestFixture]
public class SerializableTypeSafeEnumTest
{
    [Test(Description = "シリアル化可能なタイプセーフ enum")]
    public void SerializableTypeSafeEnum()
    {
        Hoge hoge100_1 = Hoge.Hoge100;
        Hoge hoge200_1 = Hoge.Hoge200;

        Hoge hoge100_2 = Util.DeepCopy(hoge100_1);
        Assert.AreSame(hoge100_1, hoge100_2);
        Assert.IsTrue(hoge100_1 == hoge100_2);
        Assert.IsTrue(object.ReferenceEquals(hoge100_1, hoge100_2));
        Assert.AreEqual(100, hoge100_2.Value);

        Hoge hoge200_2 = Util.DeepCopy(hoge200_1);
        Assert.AreSame(hoge200_1, hoge200_2);
        Assert.IsTrue(hoge200_1 == hoge200_2);
        Assert.IsTrue(object.ReferenceEquals(hoge200_1, hoge200_2));
        Assert.AreEqual(200, hoge200_2.Value);

        Fuga fuga1000_1 = Fuga.Fuga1000;
        Fuga fuga2000_1 = Fuga.Fuga2000;

        Fuga fuga1000_2 = Util.DeepCopy(fuga1000_1);
        Assert.AreSame(fuga1000_1, fuga1000_2);
        Assert.IsTrue(fuga1000_1 == fuga1000_2);
        Assert.IsTrue(object.ReferenceEquals(fuga1000_1, fuga1000_2));
        Assert.AreEqual(1000, fuga1000_2.Value);

        Fuga fuga2000_2 = Util.DeepCopy(fuga2000_1);
        Assert.AreSame(fuga2000_1, fuga2000_2);
        Assert.IsTrue(fuga2000_1 == fuga2000_2);
        Assert.IsTrue(object.ReferenceEquals(fuga2000_1, fuga2000_2));
        Assert.AreEqual(2000, fuga2000_2.Value);
    }
}

#endregion

#region タイプセーフ enum の実装

[Serializable]
public abstract class TypeSafeEnum<T> : ISerializable
    where T : TypeSafeEnum<T> // T にはタイプセーフ enum 自身の型を指定する
{
    protected TypeSafeEnum()
    {
        __instances.Add(this);
    }

    private const string INFO_NAME_SERIAL = "_serial";

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

    private static int __serial = 0;

    private readonly int _serial = __serial++;

    void ISerializable.GetObjectData(
        SerializationInfo info, StreamingContext context)
    {
        info.AddValue(INFO_NAME_SERIAL, _serial);
        info.SetType(typeof(ObjectReference<T>));
    }

    [Serializable]
    private sealed class ObjectReference<U> : IObjectReference, ISerializable
        where U : TypeSafeEnum<U>
    {
        private ObjectReference(SerializationInfo info, StreamingContext context)
        {
            _serial = info.GetInt32(INFO_NAME_SERIAL);
        }

        private readonly int _serial;

        Object IObjectReference.GetRealObject(StreamingContext context)
        {
            return TypeSafeEnum<U>.__instances[_serial];
        }

        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            throw new NotImplementedException();
        }
    }
}

[Serializable]
public sealed class Hoge : TypeSafeEnum<Hoge>
{
    private Hoge(int value)
    {
        _value = value;
    }

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

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

    [NonSerialized]
    private readonly int _value;

    public int Value
    {
        get
        {
            return _value;
        }
    }
}

[Serializable]
public sealed class Fuga : TypeSafeEnum<Fuga>
{
    private Fuga(int value)
    {
        _value = value;
    }

    public static readonly Fuga Fuga1000 = new Fuga(1000);

    public static readonly Fuga Fuga2000 = new Fuga(2000);

    [NonSerialized]
    private readonly int _value;

    public int Value
    {
        get
        {
            return _value;
        }
    }
}

#endregion

#region テスト用ユーティリティ

internal static class Util
{
    public static T DeepCopy<T>(T value)
    {
        using (MemoryStream stream = new MemoryStream())
        {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(stream, value);
            stream.Position = 0;
            return (T)formatter.Deserialize(stream);
        }
    }
}

#endregion

*1:と思う。穴がないことを祈る…