ここではBinaryFormatterおよびSoapFormatterを使ってシリアライズする場合の動作の制御について解説します。

§1 シリアライズ対象からの除外 (NonSerializedAttribute)

NonSerializedAttributeを使用すると、フィールドをシリアライズ対象から除外することが出来ます。 BinaryFormatterおよびSoapFormatterではスコープに関わらずすべてのフィールドをシリアライズしようとしますが、シリアライズする必要の無いフィールドや実行時のみに使用されるフィールドなどは、この属性を使用することでシリアライズしないようにすることが出来ます。

以下の例では、Account.LastLoginフィールドにNonSerializedAttributeを付与しています。 デシリアライズ後にAccount.LastLoginフィールドの値が復元されていない点に注目してください。

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

[Serializable]
class Account {
  public int ID;
  public string Name;

  [NonSerialized]
  public DateTime LastLogin;

  public override string ToString()
  {
    return string.Format("{0}:{1} LastLogin={2}", ID, Name, LastLogin);
  }
}

class Sample {
  static void Main()
  {
    Account alice = new Account();

    alice.ID = 2;
    alice.Name = "Alice";
    alice.LastLogin = new DateTime(2010, 10, 20);

    Console.WriteLine(alice);

    // シリアライズ
    using (Stream stream = File.OpenWrite("alice.bin")) {
      BinaryFormatter formatter = new BinaryFormatter();

      formatter.Serialize(stream, alice);
    }

    // デシリアライズ
    using (Stream stream = File.OpenRead("alice.bin")) {
      BinaryFormatter formatter = new BinaryFormatter();

      Account deserialized = (Account)formatter.Deserialize(stream);

      Console.WriteLine(deserialized);
    }
  }
}
実行結果
2:Alice LastLogin=2010/10/20 0:00:00
2:Alice LastLogin=0001/01/01 0:00:00


§2 追加されたフィールドの無視 (OptionalFieldAttribute)

OptionalFieldAttributeは新しいバージョンで追加されるフィールドに付与します。 例えば、あるバージョンでの型をシリアライズした後、型にフィールドを追加した新しいバージョンでデシリアライズしようとすると、古いバージョンのストリームには追加した新しいフィールドが含まれていないため例外がスローされます。 このような場合に例外をスローしないよう、追加したフィールドにはOptionalFieldAttributeを付与しておく必要があります。

例として、SoapFormatterを使って、二つのバージョンを跨いだシリアライズ・デシリアライズを行ってみます。 まずは、バージョン1のコードを使ってシリアライズを行います。

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;

// バージョン1のAccountクラス
[Serializable]
class Account {
  public int ID;
  public string Name;

  public override string ToString()
  {
    return string.Format("{0}:{1}", ID, Name);
  }
}

class Sample {
  static void Main()
  {
    Account alice = new Account();

    alice.ID = 2;
    alice.Name = "Alice";

    Console.WriteLine(alice);

    // シリアライズ
    using (Stream stream = File.OpenWrite("alice.soap.xml")) {
      SoapFormatter formatter = new SoapFormatter();

      formatter.Serialize(stream, alice);
    }
  }
}

バージョン1のAccountクラスには二つのフィールドIDとNameがあります。 このコードをビルドして実行ファイルser.exeを作成、実行します。 シリアライズされたファイルalice.soap.xmlの内容は次のようになります。

alice.soap.xml
<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<a1:Account id="ref-1" xmlns:a1="http://schemas.microsoft.com/clr/assem/ser%2C%20Version%3D0.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<ID>2</ID>
<Name id="ref-3">Alice</Name>
</a1:Account>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

続いて、先ほどのコードを書き換え、バージョン2のコードを使ってデシリアライズを行います。

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;

// バージョン2のAccountクラス
[Serializable]
class Account {
  public int ID;
  public string Name;

  // バージョン2で新しく追加したフィールド
  [OptionalField]
  public string ContactAddress;

  public override string ToString()
  {
    return string.Format("{0}:{1} ContactAddress={2}", ID, Name, ContactAddress);
  }
}

class Sample {
  static void Main()
  {
    // デシリアライズ
    using (Stream stream = File.OpenRead("alice.soap.xml")) {
      SoapFormatter formatter = new SoapFormatter();

      Account deserialized = (Account)formatter.Deserialize(stream);

      Console.WriteLine(deserialized);
    }
  }
}

バージョン2のAccountクラスには、バージョン1から存在するフィールドID・Nameと、バージョン2で新しく追加したフィールドContactAddressがあります。 このコードをビルドして実行ファイルser.exeを作成、実行すると、特に問題なくデシリアライズ出来ます。

実行結果
2:Alice ContactAddress=

しかし、これをOptionalFieldAttributeを付与していない状態で実行すると、以下のようにSerializationExceptionがスローされます。

OptionalFieldAttributeを付与していない状態で実行した場合
ハンドルされていない例外: System.Runtime.Serialization.SerializationException:
メンバの数が間違っています。オブジェクト Account には 3 メンバ含まれていますが、逆シリアル化されたメンバの数は 2 です。
   場所 System.Runtime.Serialization.Formatters.Soap.ReadObjectInfo.PopulateObjectMembers()
   場所 System.Runtime.Serialization.Formatters.Soap.ObjectReader.ParseObjectEnd(ParseRecord pr)
   場所 System.Runtime.Serialization.Formatters.Soap.ObjectReader.Parse(ParseRecord pr)
   場所 System.Runtime.Serialization.Formatters.Soap.SoapHandler.EndElement(String prefix, String name, String urn)
   場所 System.Runtime.Serialization.Formatters.Soap.SoapParser.ParseXml()
   場所 System.Runtime.Serialization.Formatters.Soap.SoapParser.Run()
   場所 System.Runtime.Serialization.Formatters.Soap.ObjectReader.Deserialize(HeaderHandler handler, ISerParser serParser)
   場所 System.Runtime.Serialization.Formatters.Soap.SoapFormatter.Deserialize(Stream serializationStream, HeaderHandler handler)
   場所 System.Runtime.Serialization.Formatters.Soap.SoapFormatter.Deserialize(Stream serializationStream)
   場所 Sample.Main()

なお、BinaryFormatterを使った場合はOptionalFieldAttributeが付与されていなくても特に例外はスローされないようですが、念のため付与しておいたほうがよいと思われます。

§3 シリアライズ前後のコールバック (OnSerializingAttribute, OnSerializedAttribute, OnDeserializingAttribute, OnDeserializedAttribute)

シリアライズ・デシリアライズの前後で値の検証やデフォルト値の設定などの追加の処理を行いたい場合は、次の属性を使用することでシリアライザにコールバックさせることが出来ます。 これらの属性はフィールドではなくコールバック用のメソッドに付与します。

コールバックを指定するための属性
属性 機能
OnSerializingAttribute シリアライズ前にコールバックするメソッドを指定する
OnSerializedAttribute シリアライズ後にコールバックするメソッドを指定する
OnDeserializingAttribute デシリアライズ前にコールバックするメソッドを指定する
OnDeserializedAttribute デシリアライズ後にコールバックするメソッドを指定する

なお、これらの属性を付与するメソッドは、StreamingContextを引数にとる必要があります。

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

[Serializable]
class Account {
  public int ID;
  public string Name;

  public override string ToString()
  {
    return string.Format("{0}:{1}", ID, Name);
  }

  [OnSerializing]
  private void OnSerializing(StreamingContext context)
  {
    Console.WriteLine("OnSerializing");
  }

  [OnSerialized]
  private void OnSerialized(StreamingContext context)
  {
    Console.WriteLine("OnSerialized");
  }

  [OnDeserializing]
  private void OnDeserializing(StreamingContext context)
  {
    Console.WriteLine("OnDeserializing");
  }

  [OnDeserialized]
  private void OnDeserialized(StreamingContext context)
  {
    Console.WriteLine("OnDeserialized");
  }
}

class Sample {
  static void Main()
  {
    Account alice = new Account();

    alice.ID = 2;
    alice.Name = "Alice";

    Console.WriteLine(alice);

    using (Stream stream = File.OpenWrite("alice.bin")) {
      BinaryFormatter formatter = new BinaryFormatter();

      Console.WriteLine("Serialize");

      formatter.Serialize(stream, alice);
    }

    using (Stream stream = File.OpenRead("alice.bin")) {
      BinaryFormatter formatter = new BinaryFormatter();

      Console.WriteLine("Deserialize");

      Account deserialized = (Account)formatter.Deserialize(stream);

      Console.WriteLine(deserialized);
    }
  }
}
実行結果
2:Alice
Serialize
OnSerializing
OnSerialized
Deserialize
OnDeserializing
OnDeserialized
2:Alice

§4 シリアライズ動作の制御 (ISerializable)

ISerializableインターフェイスを使うことで、ここまでに解説してきた属性を用いるよるも柔軟にシリアライズ・デシリアライズ時の動作を制御することが出来ます。 例えば、シリアライズする値とその名前を指定したり、シリアライズ・デシリアライズの前後でデフォルト値の設定や値のチェックの処理を追加することも出来ます。

ISerializableを実装する場合は、シリアライズを行うためのメソッドISerializable.GetObjectDataとデシリアライズを行うためのコンストラクタを用意する必要があります。 これらはどちらもパブリックである必要はありません。

このメソッドとコンストラクタでは、引数の一つにシリアライズ・デシリアライズする値を格納するためのSerializationInfoを取ります。 シリアライズする値を追加するにはAddValueメソッド、デシリアライズされた値を取得するにはGetStringメソッドGetValueメソッドなどを使います。 この時、値とその名前を指定出来ますが、名前にはフィールド名ではなく任意の名前を代わりに指定することも出来ます。

次の例は、ISerializableを使ってシリアライズ・デシリアライズを行う例です。

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;

[Serializable]
class Account : ISerializable {
  public int ID;
  public string Name;
  public Uri[] ContactAddresses;
  public DateTime LastLogin;

  public Account(int id, string name)
  {
    this.ID = id;
    this.Name = name;
  }

  // デシリアライズの際に呼び出されるコンストラクタ
  private Account(SerializationInfo info, StreamingContext context)
  {
    this.ID = info.GetInt32("ID");
    this.Name = info.GetString("名前");
    this.ContactAddresses = (Uri[])info.GetValue("連絡先", typeof(Uri[]));

    this.LastLogin = new DateTime(2000, 1, 1);
  }

  // ISerializable.GetObjectDataの明示的な実装
  void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
  {
    info.AddValue("ID", ID);
    info.AddValue("名前", Name);
    info.AddValue("連絡先", ContactAddresses, typeof(Uri[]));
  }

  public override string ToString()
  {
    return string.Format("{0}:{1} LastLogin={2} ContactAddresses={3}",
                         ID,
                         Name,
                         LastLogin,
                         string.Join(", ", Array.ConvertAll<Uri, string>(ContactAddresses, Convert.ToString)));
  }
}

class Sample {
  static void Main()
  {
    Account alice = new Account(2, "Alice");

    alice.ContactAddresses = new Uri[] {
      new Uri("mailto:alice@mail.example.net"),
      new Uri("http://example.com/~alice/")
    };
    alice.LastLogin = new DateTime(2010, 10, 20);

    Console.WriteLine(alice);

    // シリアライズ
    using (Stream stream = File.OpenWrite("alice.soap.xml")) {
      SoapFormatter formatter = new SoapFormatter();

      formatter.Serialize(stream, alice);
    }

    // デシリアライズ
    using (Stream stream = File.OpenRead("alice.soap.xml")) {
      SoapFormatter formatter = new SoapFormatter();

      Account deserialized = (Account)formatter.Deserialize(stream);

      Console.WriteLine(deserialized);
    }
  }
}
実行結果
2:Alice LastLogin=2010/10/20 0:00:00 ContactAddresses=mailto:alice@mail.example.net, http://example.com/~alice/
2:Alice LastLogin=2000/01/01 0:00:00 ContactAddresses=mailto:alice@mail.example.net, http://example.com/~alice/

結果を見てのとおり、適切にデシリアライズされていることが分かると思います。 なお、SerializationInfo.AddValueで指定した値の名前は、値を格納するXML要素の名前としても使われます。 以下はシリアライズされたファイルalice.soap.xmlの内容です。 Account.LastLoginフィールドの値はSerializationInfoに追加していないため、シリアライズされた内容には含まれていない点にも注目してください。

alice.soap.xml
<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<a1:Account id="ref-1" xmlns:a1="http://schemas.microsoft.com/clr/assem/ser%2C%20Version%3D0.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<ID>2</ID>
<名前 id="ref-4">Alice</名前>
<連絡先 href="#ref-5"/>
</a1:Account>
<SOAP-ENC:Array id="ref-5" SOAP-ENC:arrayType="a2:Uri[2]" xmlns:a2="http://schemas.microsoft.com/clr/nsassem/System/System%2C%20Version%3D2.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Db77a5c561934e089">
<item href="#ref-6"/>
<item href="#ref-7"/>
</SOAP-ENC:Array>
<a2:Uri id="ref-6" xmlns:a2="http://schemas.microsoft.com/clr/nsassem/System/System%2C%20Version%3D2.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Db77a5c561934e089">
<AbsoluteUri id="ref-8">mailto:alice@mail.example.net</AbsoluteUri>
</a2:Uri>
<a2:Uri id="ref-7" xmlns:a2="http://schemas.microsoft.com/clr/nsassem/System/System%2C%20Version%3D2.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Db77a5c561934e089">
<AbsoluteUri id="ref-9">http://example.com/~alice/</AbsoluteUri>
</a2:Uri>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

ISerializableインターフェイスを実装するクラスを継承可能にするには、コンストラクタとメソッドを派生クラスに公開し、オーバーライドできるようにしておく必要があります。 派生クラスでは、SerializationInfoを基底クラスに引き渡すことで基底クラスのフィールドをシリアライズ・デシリアライズさせるようにします。 以下の例では、基底クラスでISerializableを実装し、その派生クラスをシリアライズしています。

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;

[Serializable]
class AccountBase : ISerializable {
  protected int ID;
  protected string Name;

  protected AccountBase(int id, string name)
  {
    this.ID = id;
    this.Name = name;
  }

  // デシリアライズの際に呼び出されるコンストラクタ
  protected AccountBase(SerializationInfo info, StreamingContext context)
  {
    this.ID = info.GetInt32("ID");
    this.Name = info.GetString("名前");
  }

  // ISerializable.GetObjectDataの明示的な実装
  void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
  {
    // オーバーライド可能なメソッドを呼び出す
    GetObjectData(info, context);
  }

  // シリアライズする値を追加する
  protected virtual void GetObjectData(SerializationInfo info, StreamingContext context)
  {
    info.AddValue("ID", ID);
    info.AddValue("名前", Name);
  }
}

[Serializable]
class Account : AccountBase {
  public Uri[] ContactAddresses;

  public Account(int id, string name)
    : base(id, name)
  {
  }

  // デシリアライズの際に呼び出されるコンストラクタ
  protected Account(SerializationInfo info, StreamingContext context)
    : base(info, context)
  {
    this.ContactAddresses = (Uri[])info.GetValue("連絡先", typeof(Uri[]));
  }

  // GetObjectDataをオーバーライド
  protected override void GetObjectData(SerializationInfo info, StreamingContext context)
  {
    // 基底クラスのシリアライズ情報を取得
    base.GetObjectData(info, context);

    info.AddValue("連絡先", ContactAddresses, typeof(Uri[]));
  }

  public override string ToString()
  {
    return string.Format("{0}:{1} ContactAddresses={2}",
                         ID,
                         Name,
                         string.Join(", ", Array.ConvertAll<Uri, string>(ContactAddresses, Convert.ToString)));
  }
}

class Sample {
  static void Main()
  {
    Account alice = new Account(2, "Alice");

    alice.ContactAddresses = new Uri[] {
      new Uri("mailto:alice@mail.example.net"),
      new Uri("http://example.com/~alice/")
    };

    Console.WriteLine(alice);

    // シリアライズ
    using (Stream stream = File.OpenWrite("alice.soap.xml")) {
      SoapFormatter formatter = new SoapFormatter();

      formatter.Serialize(stream, alice);
    }

    // デシリアライズ
    using (Stream stream = File.OpenRead("alice.soap.xml")) {
      SoapFormatter formatter = new SoapFormatter();

      Account deserialized = (Account)formatter.Deserialize(stream);

      Console.WriteLine(deserialized);
    }
  }
}
実行結果
2:Alice ContactAddresses=mailto:alice@mail.example.net, http://example.com/~alice/
2:Alice ContactAddresses=mailto:alice@mail.example.net, http://example.com/~alice/

§5 デシリアライズ後のコールバック (IDeserializationCallback)

IDeserializationCallbackインターフェイスもデシリアライズが完了した時点で呼び出されるコールバックメソッドを実装するためのものです。 OnDeserializedAttributeと機能と動作は同じです。 引数としてStreamingContextの代わりにobjectをとりますが、有効な値は渡されないようです。

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

[Serializable]
class Account : IDeserializationCallback {
  public int ID;
  public string Name;

  [NonSerialized]
  private bool deserialized = false;

  public override string ToString()
  {
    return string.Format("{0}:{1} {2}", ID, Name, deserialized ? "(deserialized)" : string.Empty);
  }

  void IDeserializationCallback.OnDeserialization(object sender)
  {
    deserialized = true;

    Console.WriteLine("OnDeserialization: sender={0}", sender);
  }
}

class Sample {
  static void Main()
  {
    Account alice = new Account();

    alice.ID = 2;
    alice.Name = "Alice";

    Console.WriteLine(alice);

    using (Stream stream = File.OpenWrite("alice.bin")) {
      BinaryFormatter formatter = new BinaryFormatter();

      Console.WriteLine("Serialize");

      formatter.Serialize(stream, alice);
    }

    using (Stream stream = File.OpenRead("alice.bin")) {
      BinaryFormatter formatter = new BinaryFormatter();

      Console.WriteLine("Deserialize");

      Account deserialized = (Account)formatter.Deserialize(stream);

      Console.WriteLine(deserialized);
    }
  }
}
実行結果
2:Alice
Serialize
Deserialize
OnDeserialization: sender=
2:Alice (deserialized)