オブジェクトのシリアル化

Last Updated 2011/09/21


私はいまだにシリアル化という言葉に抵抗があります。というのは、その意味を直感的に理解することができないからです。.Net Framework SDK には、「シリアル化とは、オブジェクトの状態を永続化または転送できる形式に変換するプロセス」とありますから、そのまま理解するほかないのでしょう。ともあれ、このページではオブジェクトのシリアル化についてできるだけ実践的な説明をしたいと思います。なお、サンプルコードは C# を使っています。


私と同様、迷える子羊のために前文の補足をしておきます。ただし、シリアル化用語としてです。

「オブジェクトの状態」とはクラスのフィールド(プロパティと考えてよい)の設定内容のことです。「永続化」とはディスクなどの記憶媒体に記録することです。「転送できる形式」とはネットワークを通じて情報を送信できる形式、つまり、ネットワーク上で許されるルールに基づく形式のことです。

シリアル化 "serialize" の英語としての意味は、ジーニアス英和辞典によると、「(物語などを)連載する」と「連続物として放送する」の 2 つの意味しか掲載されていません。コンピュータ用語らしく説明すると、「連続するビットに変換する」ぐらいでしょうか。

余談ですがストリーム "stream" の英語としての意味は、「小さな水の流れ」です。シリアル化は水の流れの上にビット(バイトでもいいですが)を順序良く並べることと形容してもいいのかもしれません。

さて、このページのタイトルは、「オブジェクトのシリアル化」です。この場合の「オブジェクト」の実体は先に説明したように、オブジェクトの状態、つまり、クラスのプロパティ値をファイルなどに出力することです。一方、ファイルなどに記録した情報からオブジェクトを再構築することを逆シリアル化 "deserialize" と呼びます。

以後、シリアル化と呼ぶ場合は逆シリアル化も含むと考えてください。

シリアル化の基礎知識

.Net Framework の中のシリアル化に関係する主なクラスをリストアップします。個々の項目の詳細についてはしかるべき資料をあたってください。

SerializeableAttribute クラス

クラスの宣言部に付加する属性で、BinaryFormatter クラスなどの Serialize メソッドを呼び出すとき、この属性がないと例外が発生します。

  [Serializable]
  public class TestClass
  {
    ....
  }

ISerializable インターフェース

ISerializable インターフェースには GetObjectData メソッドしか設定されていませんが、シリアル化するクラスが ISerializable インターフェースを継承して、GetObjectData メソッドをオーバーライドすることで、シリアル化する対象を自由に設定することができます。

  [Serializable]
  public class TestClass : ISerializable
  {
    ....

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

このインターフェースを継承するクラスの場合、かならず、GetObjectData メソッドをオーバーライドしなければなりません。このメソッドは上記のコードを見てのとおり、SerializationInfo オブジェクトと StreamingContext 構造体を受け取ります。

SerializationInfo オブジェクトにシリアル化の対象とするプロパティを追加します。AddValue メソッドの最初の引数は通常、プロパティ名を指定します。実際には任意の文字列でいいのですが、重複する文字列を指定することはできません。2 番目の引数はシリアル化するプロパティ値を保持する変数名を指定します。プロパティそのものでもいいし、プロパティに対する private なメンバ変数でもかまいません。

StreamingContext 型は私がテストした範囲では必ず StreamingContextStates.All が戻ります。それ以外の値が戻るケースがあるのかどうか、何が戻るかについては確認していません。また、戻り値によってどう対処すればいいのかについても分かりません。

NonSerializedAttribute クラス

この属性を付加したメンバはシリアル化の対象から除外するものですが、ISerializable インターフェースを継承する場合は GetObjectData メソッドでシリアル化するメンバを指定できますからこの属性を使う必要はありません。

OptionalFieldAttribute クラス

たいていのアプリケーションはバージョンアップするものです。したがって、シリアル化するプロパティが増減する可能性があります。旧バージョンで存在したプロパティを新しいバージョンで削除したり、プロパティを追加すると、旧バージョンで作成したシリアル化情報を新しいバージョンで読み込むと例外が発生します。

このようなケースに対応するため、この属性が用意されています。しかし、この属性がどのようなケースで有効になるかはやってみなければ分からないといったところです。そこで、確実なことだけを説明しておきます。

ただし、この属性は SoapFormatter クラスでは有効ではないようです。

OnDeserializedAttribute、OnDeserializingAttribute、OnSerializedAttribute、OnSerializingAttribute クラス

これらの属性はメソッドに付加するもので、クラス名を見て想像できると思いますが、シリアル化を開始する直前、シリアル化を終了したあと、逆シリアル化を開始する直前、逆シリアル化を終了したあとに呼び出されます。どう使うかはアプリケーション作成者しだいです。属性を設定したメソッドのコード例を 1 つだけ示しておきます。

  [OnSerializing()]
  private void OnSerializingMethod(StreamingContext context)
  {
    ....
  }

しかし、上記の各メソッドは引数として StreamingContext 型だけが戻るところをみると、この情報にしたがって何かをするために用意されたものかもしれません。

BinaryFormatter クラス

シリアル化情報をバイナリファイルに保存する場合は BinaryFormatter クラスを使います。このクラスは private または public にかかわらずシリアル化の対象としますが、ISerializable インターフェースを継承するクラスの場合は、GetObjectData メソッドをオーバーライドすることで何をシリアル化の対象とするかを指定できます。

クラスを BinaryFormatter クラスを使ってシリアル化する

BinaryFormatter クラスを使ってクラスのシリアル化情報をバイナリファイルに出力し、それを逆シリアル化する手順を説明します。まず、シリアル化するクラスの作り方から説明します。ポイントはクラスの宣言部に SerializableAttribute 属性を付加すること、クラスは ISerializable インターフェースを継承すること、逆シリアル化用コンストラクタを持つこと、GetObjectData メソッドをオーバーライドすること、の 4 点です。

using System;
using System.Runtime.Serialization; // ISerializable
using System.Windows.Forms; // MessageBox

namespace emanual.Utility
{
  [Serializable] // クラスをシリアル化の対象とする属性を付加する
  class SerializeTestClass : ISerializable // ISerializable インターフェースの手順にしたがう
  {
    // メンバ変数
    private string FPersonName;
    private string FCityName;
    private int FAge;
    private Color FColor;

    // プロパティ
    public string PersonName { get { return FPersonName; } set { FPersonName = value; } }
    public string CityName { get { return FCityName; } set { FCityName = value; } }
    public int Age { get { return FAge; } set { FAge = value; } }
    public Color Color { get { return FColor; } set { FColor = value; } }

    //-------------------------------------------------------------------------------------
    // コンストラクタ
    // (このコンストラクタはテスト用ですので、実情に合わせて変更してください)
    public SerializeTestClass(string personName, string cityName, int age, Color color)
    {
      // プロパティのデフォルト値を設定する
      FPersonName = personName;
      FCityName = cityName;
      FAge = age;
      FColor = color;
    }

    //-------------------------------------------------------------------------------------
    // 逆シリアル化用コンストラクタ
    public SerializeTestClass(SerializationInfo info, StreamingContext context)
    {
      try
      {
        FPersonName = info.GetString("PersonName");
        FCityName = info.GetString("CityName");
        FAge = info.GetInt32("Age");
        FColor = (Color)info.GetValue("Color", typeof(Color));
      }
      catch (Exception e)
      {
        MessageBox.Show(e.Message);
      }
    }

    //-------------------------------------------------------------------------------------
    // SerializeTestClass オブジェクトをシリアル化するときに呼び出されるメソッド
    void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
    {
      try
      {
        // シリアル化するプロパティを設定する
        info.AddValue("PersonName", PersonName);
        info.AddValue("CityName", CityName);
        info.AddValue("Age", Age);
        info.AddValue("Color", Color);
      }
      catch (Exception e)
      {
        MessageBox.Show(e.Message);
      }
    }

    //-------------------------------------------------------------------------------------
  } // end of SerializeTestClass class
} // end of namespace

次に、上記のクラスをシリアル化する手順を説明します。出力ファイル名は DataFile.dat です。

using System.IO; // FileStream
using System.Runtime.Serialization.Formatters.Binary; // BinaryFormatter

private void button1_Click(object sender, EventArgs e)
{
  emanual.Utility.SerializeTestClass obj = null;
  FileStream stream = null;
  BinaryFormatter formatter = null;

  try
  {
    obj = new emanual.Utility.SerializeTestClass("河北潤二", "金沢市", 32, Color.AliceBlue);
    stream = new FileStream("DataFile.dat", FileMode.Create);
    formatter = new BinaryFormatter();

    // オブジェクトをシリアル化する
    formatter.Serialize(stream, obj);
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message);
  }
  finally
  {
    stream.Close();
  }
}

シリアル化情報を保持するファイルを読み込んで、逆シリアル化します。

private void button2_Click(object sender, EventArgs e)
{
  emanual.Utility.SerializeTestClass obj = null;
  FileStream stream = null;
  BinaryFormatter formatter = null;

  try
  {
    stream = new FileStream("DataFile.dat", FileMode.Open);
    formatter = new BinaryFormatter();

    // オブジェクトを逆シリアル化する
    obj = (emanual.Utility.SerializeTestClass)formatter.Deserialize(stream);
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message);
  }
  finally
  {
    stream.Close();
  }

  // 逆シリアル化の結果を表示する
  Text = String.Format("{0} {1} {2} {3}", obj.PersonName, obj.CityName, obj.Age, obj.Color);
}

SoapFormatter クラス

シリアル化情報を SOAP 形式の XML ドキュメントとして保存する場合は SoapFormatter クラスを使います。XmlSerializer クラスを使っても XML 形式のファイルに保存できますが、SoapFormatter クラスのほうが圧倒的に簡単だし、SOAP 形式でまずいことってあるのでしょうか。

クラスを SoapFormatter クラスを使ってシリアル化する

シリアル化するクラスは前記の SerializeTestClass クラスと同じです。SoapFormatter クラスの使い方は BinaryFormatter クラスと同じです。

using System.IO; // FileStream
using System.Runtime.Serialization.Formatters.Soap; // SoapFormatter

private void button1_Click(object sender, EventArgs e)
{
  emanual.Utility.SerializeTestClass obj = null;
  FileStream stream = null;
  SoapFormatter formatter = null;

  try
  {
    obj = new emanual.Utility.SerializeTestClass("河北潤二", "金沢市", 32, Color.AliceBlue);
    stream = new FileStream("DataXmlFile.dat", FileMode.Create);
    formatter = new SoapFormatter();

    // オブジェクトをシリアル化する
    formatter.Serialize(stream, obj);
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message);
  }
  finally
  {
    stream.Close();
  }
}

シリアル化情報を保持するファイルを読み込んで、逆シリアル化します。

private void button2_Click(object sender, EventArgs e)
{
  emanual.Utility.SerializeTestClass obj = null;
  FileStream stream = null;
  SoapFormatter formatter = null;

  try
  {
    stream = new FileStream("DataXmlFile.dat", FileMode.Open);
    formatter = new SoapFormatter();

    // オブジェクトを逆シリアル化する
    obj = (emanual.Utility.SerializeTestClass)formatter.Deserialize(stream);
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message);
  }
  finally
  {
    stream.Close();
  }

  // 逆シリアル化の結果を表示する
  Text = String.Format("{0} {1} {2} {3}", obj.PersonName, obj.CityName, obj.Age, obj.Color);
}

型変換クラス

シリアル化・逆シリアル化するとき、フォーマッターは型変換クラスを利用してオブジェクトを文字列に、または文字列からオブジェクトに変換しなけれなりません。.Net Framework は標準のデータ型に対しては標準の型変換クラスを提供しますから、変換は自動的に行われます。

一方、クラスで独自のデータ型を使う場合は、それに対応する型変換クラスを用意しなければなりません。型変換クラスは  TypeConverter クラスから派生し、シリアル化の対象とするメンバに TypeConverterAttribute 属性を使って設定します。

「クラスを BinaryFormatter クラスを使ってシリアル化する」で使用した SerializeTestClass クラスを以下のように変更します。なお、シリアル化および逆シリアル化の手順はまったく同じです。

using System;
using System.Drawing;
using System.Runtime.Serialization; // ISerializable
using System.Windows.Forms; // MessageBox
using System.ComponentModel; // TypeConverter
using System.Globalization; // CultureInfo

namespace emanual.Utility
{
  [Serializable]
  class SerializeTestClass : ISerializable
  {
    [TypeConverter(typeof(AddressConverter))] // ← 必須
    private Address FAddress;
    private Color FColor;

    public Address Address { get { return FAddress; } set { FAddress = value; } }
    public Color Color { get { return FColor; } set { FColor = value; } }

    //-------------------------------------------------------------------------------------
    // コンストラクタ
    public SerializeTestClass(string personName, string cityName, int age, Color color)
    {
      // プロパティのデフォルト値を設定する
      FAddress = new Address(personName, cityName, age);
      FColor = color;
    }

    //-------------------------------------------------------------------------------------
    // 逆シリアル化用コンストラクタ
    public SerializeTestClass(SerializationInfo info, StreamingContext context)
    {
      try
      {
        FAddress = (Address)info.GetValue("Address", typeof(Address));
        FColor = (Color)info.GetValue("Color", typeof(Color));
      }
      catch (Exception e)
      {
        MessageBox.Show(e.Message);
      }
    }

    //-------------------------------------------------------------------------------------
    // オブジェクトをシリアル化するときに呼び出されるメソッド
    void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
    {
      try
      {
        // シリアル化するプロパティを設定する
        info.AddValue("Address", FAddress);
        info.AddValue("Color", FColor);
      }
      catch (Exception e)
      {
        MessageBox.Show(e.Message);
      }
    }

    //-------------------------------------------------------------------------------------
  } // end of SerializeTestClass class

  //**************************************************************************************
  // AddressConverter class
  //**************************************************************************************
  internal class AddressConverter : TypeConverter
  {
    public override bool GetPropertiesSupported(ITypeDescriptorContext context)
    {
      return true;
    }

    //-------------------------------------------------------------------------------------
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
      if (sourceType == typeof(string))
        return true;

      return base.CanConvertFrom(context, sourceType);
    }

    //-------------------------------------------------------------------------------------
    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
      if (value is string)
      {
        try
        {
          string s = (string)value;
          string[] sa = s.Split(',');

          Address address = new Address();
          address.PersonName = sa[0];
          address.CityName = sa[1];
          address.Age = Convert.ToInt32(sa[2]);

          return address;
        }
        catch
        {
          throw new ArgumentException("書式が不正です");
        }
      }

      return base.ConvertFrom(context, culture, value);
    }

    //-------------------------------------------------------------------------------------
    public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
    {
      if (destinationType == typeof(Address))
        return true;

      return base.CanConvertTo(context, destinationType);
    }

    //-------------------------------------------------------------------------------------
    public override object ConvertTo(ITypeDescriptorContext context,
          CultureInfo culture, object value, Type destinationType)
    {
      if (destinationType == typeof(string) && value is Address)
      {
        Address address = (Address)value;

        return address.PersonName + "," + address.CityName + "," + address.Age.ToString();
      }

      return base.ConvertTo(context, culture, value, destinationType);
    }

    //-------------------------------------------------------------------------------------
  } // end of AddressConverter class

  //**************************************************************************************
  // Address class
  //**************************************************************************************
  [Serializable]
  internal class Address : ISerializable
  {
    private string FPersonName;
    private string FCityName;
    private int FAge;

    public string PersonName { get { return FPersonName; } set { FPersonName = value; } }
    public string CityName { get { return FCityName; } set { FCityName = value; } }
    public int Age { get { return FAge; } set { FAge = value; } }

    public Address()
    {
    }

    public Address(string name, string city, int age)
    {
      FPersonName = name;
      FCityName = city;
      FAge = age;
    }

    //-------------------------------------------------------------------------------------
    // 逆シリアル化用コンストラクタ
    public Address(SerializationInfo info, StreamingContext context)
    {
      try
      {
        FPersonName = info.GetString("PersonName");
        FCityName = info.GetString("CityName");
        FAge = info.GetInt32("Age");
      }
      catch (Exception e)
      {
        MessageBox.Show(e.Message);
      }
    }

    //-------------------------------------------------------------------------------------
    // オブジェクトをシリアル化するときに呼び出されるメソッド
    void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
    {
      try
      {
        // シリアル化するプロパティを設定する
        info.AddValue("PersonName", PersonName);
        info.AddValue("CityName", CityName);
        info.AddValue("Age", Age);
      }
      catch (Exception e)
      {
        MessageBox.Show(e.Message);
      }
    }
  } // end of Address class
} // end of namespace

−以上−