.NET におけるオブジェクト シリアル化

Piet Obermeyer
Microsoft Corporation

2001 年 8 月

要約: シリアル化を使用すべき状況には、どのようなものがあるのでしょうか? 最も重要なのは、後にまったく同じコピーを再作成できるように、オブジェクトの状態をストレージ メディアに永続化する場合と、アプリケーション ドメイン間でオブジェクトを値によって送信する場合の 2 つです。たとえば、シリアル化は、ASP.NET でセッション状態を保存したり、Windows フォームでオブジェクトをクリップボードにコピーするために使用されています。また、リモーティングがアプリケーション ドメイン間でオブジェクトを値によって受け渡すためにも使用されています。この記事では、Microsoft .NET で使用されているシリアル化の概要を示します。

目次

はじめに
永続的ストレージ
値によるマーシャリング
基本的なシリアル化
選択的なシリアル化
カスタム シリアル化
シリアル化 プロセスのステップ
バージョニング
シリアル化のガイドライン

はじめに

シリアル化は、オブジェクト インスタンスの状態をストレージ メディアに格納するプロセスと定義することができます。このプロセスでは、オブジェクトのパブリックおよびプライベート フィールドと、クラスを含んでいるアセンブリを含めたクラスの名前が、バイトのストリームに変換された後に、データ ストリームに書き込まれます。後にオブジェクトを逆シリアル化すると、元のオブジェクトと同一のクローンが作成されます。

オブジェクト指向環境でシリアル化 メカニズムをインプリメントするときには、使いやすさと柔軟性の間でのいくつかのトレードオフを考慮しなくてはなりません。開発者がこのプロセスを十分に制御できる場合には、プロセスの大部分を自動化することができます。たとえば、単純なバイナリ シリアル化では不十分だったり、特殊な理由から、クラス内の特定のフィールドをシリアライズしたい場合があります。以下のセクションでは、.NET Framework に用意されている堅牢なシリアル化 メカニズムについて説明し、ニーズに合わせてプロセスをカスタマイズするための重要な機能をいくつか紹介します。

永続的ストレージ

オブジェクトのフィールドの値をディスクに格納し、後の段階でこのデータを取り出すという処理が必要になることがあります。これはシリアル化に頼らなくても簡単に実現できますが、このアプローチは一般に面倒で間違いを犯しやすく、複数のオブジェクトから成る階層を追跡しなくてはならないときにはいっそう複雑になります。数千ものオブジェクトを含んでいる大きなビジネス アプリケーションで、個々のオブジェクトのフィールドとプロパティをディスクとの間で読み書きするコードを書かなくてはならないという状況を想像してみてください。シリアル化は、この目的を最小限の労力で達成するための便利なメカニズムを提供します。

共通言語ランタイム (CLR) はメモリ内でのオブジェクトの配置を管理し、.NET Framework はリフレクションを使用した自動化されたシリアル化 メカニズムを提供します。オブジェクトがシリアライズされるときには、クラスの名前、アセンブリ、およびクラス インスタンスのすべてのデータ メンバがストレージに書き込まれます。オブジェクトはメンバ変数の中に他のインスタンスへの参照を格納することがよくあります。クラスがシリアライズされるとき、シリアル化 エンジンはすでにシリアライズ済みのすべての参照先オブジェクトを追跡し、同じオブジェクトが 2 度以上シリアライズされないようにします。.NET Framework が提供するシリアル化 アーキテクチャは、オブジェクト グラフと循環参照を自動的に正しく処理します。オブジェクト グラフに課せられる唯一の要件は、シリアライズされるオブジェクトから参照されているすべてのオブジェクトが Serializable としてマークされていなくてはならないということです (「基本的なシリアル化」を参照)。さもないと、シリアライザがマークされていないオブジェクトをシリアライズしようとしたときに、例外が送出されます。

シリアライズされたクラスが逆シリアル化されるときには、クラスが再作成され、すべてのデータ メンバの値が自動的に復元されます。

値によるマーシャリング

オブジェクトは、それが作成されたアプリケーション ドメインの中でのみ有効です。オブジェクトをパラメータとして渡したり、結果として返そうとする試みは、そのオブジェクトが MarshalByRefObject から派生しているか、Serializable としてマークされていない限り失敗します。オブジェクトが Serializable としてマークされていると、オブジェクトは自動的にシリアライズされ、アプリケーション ドメイン間で転送され、転送先のアプリケーション ドメインで逆シリアル化されてオブジェクトの正確なコピーが作成されます。このプロセスは一般に値によるマーシャリングと呼ばれています。

オブジェクトが MarshalByRefObject から派生していると、オブジェクトそのものではなくオブジェクト参照がアプリケーション ドメイン間で受け渡されます。MarshalByRefObject から派生したオブジェクトを Serializable としてマークすることも可能です。このオブジェクトがリモーティングで使用されると、SurrogateSelector によって事前構成されたシリアル化の責任を負うフォーマッタがシリアル化 プロセスを制御し、MarshalByRefObject から派生したすべてのオブジェクトをプロキシに置き換えます。SurrogateSelector がない場合、シリアル化 アーキテクチャは下記の標準的なシリアル化の規則に従います (「シリアル化 プロセスのステップ」を参照)。

基本的なシリアル化

クラスをシリアライズ可能にするには、次のように Serializable 属性でマークするのが一番簡単です。


[Serializable]
public class MyObject {
  public int n1 = 0;
  public int n2 = 0;
  public String str = null;
}

次のコード例は、このクラスのインスタンスをファイルにシリアライズする方法を示しています。


MyObject obj = new MyObject();
obj.n1 = 1;
obj.n2 = 24;
obj.str = "Some String";
IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream("MyFile.bin", FileMode.Create, 
FileAccess.Write, FileShare.None);
formatter.Serialize(stream, obj);
stream.Close();

この例はバイナリ フォーマッタを使ってシリアル化を行っています。開発者がしなくてはならないのは、使用したいストリームとフォーマッタのインスタンスを作成し、フォーマッタの Serialize メソッドを呼び出すことだけです。ストリームと、シリアライズの対象のオブジェクト インスタンスは、この呼び出しへのパラメータとして指定されます。この例では明示的に示していませんが、クラスのすべてのメンバ変数が、プライベートとしてマークされた変数も含めてシリアライズされます。この点で、バイナリ シリアル化は、パブリック フィールドのみをシリアライズする XML シリアライザと異なります。

オブジェクトを元の状態に戻すのも、同じように簡単です。まずフォーマッタと、読み込みのためのストリームを作成し、フォーマッタに対してオブジェクトを逆シリアル化するよう指示します。その方法を次のコード例に示します。


IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream("MyFile.bin", FileMode.Open, 
FileAccess.Read, FileShare.Read);
MyObject obj = (MyObject) formatter.Deserialize(fromStream);
stream.Close();

// ここで検証する
Console.WriteLine("n1: {0}", obj.n1);
Console.WriteLine("n2: {0}", obj.n2);
Console.WriteLine("str: {0}", obj.str);

BinaryFormatter はきわめて効率的で、非常にコンパクトなバイト ストリームを生成します。このフォーマッタでシリアライズされたすべてのオブジェクトは、同じフォーマッタを使って逆シリアル化することができるため、.NET プラットフォーム上で逆シリアル化されるオブジェクトをシリアライズするためのツールとして理想的です。オブジェクトが逆シリアル化されるときには、コンストラクタが呼び出されないことに注意してください。これはパフォーマンス上の理由から課せられた制約です。しかし、これによってオブジェクトの作成者がランタイムに関して持っているいくつかの前提が破られるので、開発者はオブジェクトをシリアライズ可能にしたときの影響を理解しておく必要があります。

移植性が必要な場合には、代わりに SoapFormatter を使用してください。上のコードのフォーマッタを SoapFormatter に置き換え、同じように SerializeDeserialize を呼び出します。このフォーマッタは、上に示した例では次の出力を生成します。


<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/
  SOAP-ENV:encodingStyle=
  "http://schemas.microsoft.com/soap/encoding/clr/1.0
  http://schemas.xmlsoap.org/soap/encoding/"
  xmlns:a1="http://schemas.microsoft.com/clr/assem/ToFile">

  <SOAP-ENV:Body>
    <a1:MyObject id="ref-1">
      <n1>1</n1>
      <n2>24</n2>
      <str id="ref-3">Some String</str>
    </a1:MyObject>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Serializable 属性は継承不可能であることに注意してください。MyObject から新しいクラスを派生させた場合には、その新しいクラスにも Serializable 属性を指定しないとシリアライズ可能にはなりません。たとえば、下に示すクラスのインスタンスをシリアライズしようとすると、MyStuff 型がシリアライズ可能としてマークされていないことを知らせる SerializationException が発生します。


public class MyStuff : MyObject 
{
  public int n3;
}

シリアル化属性は便利ですが、上に述べたような制限があります。シリアル化をコンパイル済みのクラスに後から追加することはできないので、ガイドラインを読んだ上で、クラスにシリアル化のマークを付けるかどうかを決定するようにしてください (下の「シリアル化のガイドライン」を参照)。

選択的なシリアル化

クラスにはシリアライズすべきでないフィールドが含まれていることがあります。たとえば、あるクラスがメンバ変数にスレッド ID を格納していたとします。このクラスが逆シリアル化されるとき、クラスのシリアライズの際に ID が格納されたスレッドはすでに実行されていない可能性があるので、この値をシリアライズすることには意味がありません。メンバ変数がシリアライズされないようにするには、次のように NonSerialized 属性でマークを付けます。


[Serializable]
public class MyObject 
{
  public int n1;
  [NonSerialized] public int n2;
  public String str;
}

カスタム シリアル化

オブジェクトの ISerializable インターフェイスをインプリメントすることで、シリアル化 プロセスをカスタマイズすることができます。これは、逆シリアル化の後にメンバ変数の値が無効になるが、オブジェクトの完全な状態を再構築するためには変数に値を与えなくてはならないようなケースで特に便利です。ISerializable をインプリメントするためには、GetObjectData メソッドと、オブジェクトが逆シリアル化されるときに使用される特殊なコンストラクタをインプリメントする必要があります。次のサンプル コードは、前のセクションの MyObject クラスの ISerializable をインプリメントする方法を示しています。


[Serializable]
public class MyObject : ISerializable 
{
  public int n1;
  public int n2;
  public String str;

  public MyObject()
  {
  }

  protected MyObject(SerializationInfo info, StreamingContext context)
  {
    n1 = info.GetInt32("i");
    n2 = info.GetInt32("j");
    str = info.GetString("k");
  }

  public virtual void GetObjectData(SerializationInfo info, 
StreamingContext context)
  {
    info.AddValue("i", n1);
    info.AddValue("j", n2);
    info.AddValue("k", str);
  }
}

シリアル化の際に GetObjectData が呼び出されるとき、開発者はメソッド呼び出しで指定された SerializationInfo オブジェクトにデータを格納する責任を負います。単に、シリアライズする変数を名前と値のペアとして追加してください。名前としては任意のテキストを使用することができます。開発者は、逆シリアル化の際にオブジェクトを復元するのに必要なデータを与えるという前提さえ守れば、SerializationInfo にどのメンバ変数を追加するかを自由に決めることができます。基本オブジェクトが ISerializable をインプリメントしている場合、派生クラスは基本クラスの GetObjectData メソッドを呼び出さなくてはなりません。

ISerializable をクラスに追加するときには、GetObjectData と特殊なコンストラクタの両方をインプリメントしなくてはならないという点に注意してください。コンパイラは GetObjectData がない場合には警告を発しますが、コンストラクタをインプリメントするよう強制することは不可能なので、コンストラクタが存在しなかった場合には警告は発せられず、コンストラクタなしのクラスの逆シリアル化を試みると例外が送出されます。このデザインが SetObjectData メソッドの代わりに選ばれたのは、セキュリティとバージョニングに関連する問題を回避するためです。たとえば、SetObjectData メソッドは、インターフェイスの一部として定義する場合にはパブリックでなくてはならず、ユーザーは SetObjectData メソッドが複数回呼び出されるのを防ぐためのコードを書かなくてはならなくなります。悪意のあるアプリケーションが、何らかの操作を実行しているオブジェクトの SetObjectData メソッドを呼び出したときに生じる問題は、大変な頭痛の種となるでしょう。

逆シリアル化の際には、SerializationInfo がこの目的のために用意されたコンストラクタを使ってクラスに渡されます。オブジェクトが逆シリアル化されるときには、コンストラクタに課せられた可視性の制約はすべて無視されるので、クラスは public、protected、internal、または private のいずれとしてマークしてもかまいません。一般にコンストラクタは protected にすることを推奨されますが、シールされているクラスについては、コンストラクタを private にする必要があります。オブジェクトの状態を復元するときには、単にシリアル化の際に使用した名前を使って、SerializationInfo から変数の値を取り出します。基本クラスが ISerializable をインプリメントしている場合には、基本オブジェクトが自分のオブジェクトを復元できるように、基本クラスのコンストラクタを呼び出すようにしてください。

ISerializable をインプリメントしているクラスから新しいクラスを派生させる場合、派生クラスは、シリアライズの必要がある変数を含んでいるときには、コンストラクタと GetObjectData メソッドの両方をインプリメントする必要があります。次のコードは、前に示した MyObject クラスでの例を示しています。


[Serializable]
public class ObjectTwo : MyObject
{
  public int num;

  public ObjectTwo() : base()
  {
  }

  protected ObjectTwo(SerializationInfo si, StreamingContext context) : 
base(si,context)
  {
    num = si.GetInt32("num");
  }

  public override void GetObjectData(SerializationInfo si, 
StreamingContext context)
  {
    base.GetObjectData(si,context);
    si.AddValue("num", num);
  }
}

逆シリアル化 コンストラクタの中で基本クラスを呼び出すのを忘れないようにしてください。これを怠ると、基本クラスのコンストラクタは決して呼び出されず、オブジェクトは逆シリアル化の後に完全に作成されなくなります。

オブジェクトは内側から再構築され、逆シリアル化の途中でメソッドを呼び出すと望ましくない副作用が生じることがあります。これは、呼び出し先のメソッドが、その呼び出しの時点ではまだ逆シリアル化されていないオブジェクト参照を参照している可能性があるからです。逆シリアル化されるクラスが IDeserializationCallback をインプリメントしている場合には、オブジェクト グラフ全体が逆シリアル化された時点で、OnSerialization メソッドが自動的に呼び出されます。この時点では、参照されているすべての子オブジェクトが完全に復元されているはずです。ハッシュ テーブルは、上記のイベント リスナなしでは逆シリアル化が難しいクラスの典型的な例です。逆シリアル化の際にキーと値のペアを取得するのは簡単ですが、これらのオブジェクトをハッシュ テーブルに戻す操作は、ハッシュー テーブルから派生したクラスが逆シリアル化されているという保証がないため、問題を引き起こす可能性があります。このため、この段階でハッシュ テーブルのメソッドを呼び出すことはお勧めできません。

シリアル化 プロセスのステップ

フォーマッタの Serialize メソッドが呼び出されたとき、オブジェクトのシリアル化は以下の規則に従って進行します。

  • フォーマッタが代理セレクタを持っているかどうかがチェックされます。持っている場合には、代理セレクタが指定された型のオブジェクトを処理するかどうかをチェックします。セレクタがそのオブジェクト型を処理する場合には、代理セレクタの ISerializable.GetObjectData が呼び出されます。
  • 代理セレクタが存在しない、またはその型を処理しない場合には、オブジェクトが Serializable 属性でマークされているかどうかがチェックされます。マークされていない場合には、SerializationException が送出されます。
  • 適切にマークされている場合には、オブジェクトが ISerializable をインプリメントしているかどうかがチェックされます。インプリメントしている場合には、オブジェクトの GetObjectData が呼び出されます。
  • ISerializable をインプリメントしていない場合には、デフォルトのシリアル化 ポリシーが使用され、NonSerialized としてマークされていないすべてのフィールドがシリアライズされます。

バージョニング

.NET Framework はバージョニングとサイド バイ サイド実行をサポートしており、クラスのインターフェイスが同じである限り、すべてのクラスがバージョンが違っていても動作します。シリアル化はインターフェイスではなくメンバ変数を扱うものなので、複数のバージョン間でシリアライズされるクラスのメンバ変数を追加または削除するときには注意してください。これは特に、ISerializable をインプリメントしていないクラスに当てはまります。メンバ変数の追加、変数の型の変更、または名前の変更など、現在のバージョンの状態に対して変更を加えると、以前のバージョンでシリアライズされた同じ型の既存のオブジェクトを逆シリアル化できなくなる可能性があります。

バージョン間でオブジェクトの状態を変更しなくてはならない場合、クラスの作成者には 2 つの選択肢があります。

  • ISerializable をインプリメントする。これにより、シリアル化および逆シリアル化 プロセスを細かく制御し、新しい状態を追加し、逆シリアル化の際に正しく解釈することができます。
  • 重要でないメンバ変数に NonSerialized 属性でマークを付ける。この方法は、クラスのバージョン間で小さな変更しか加えられないと予想される場合にのみ使用します。たとえば、クラスの新しいバージョンに新しい変数が追加されたとき、その変数を NonSerialized としてマークしておけば、クラスは以前のバージョンとの互換性を保ちます。

シリアル化のガイドライン

クラスはいったんコンパイルした後にシリアライズ可能にすることはできないので、シリアル化に関する検討は新しいクラスの設計時に行わなくてはなりません。たとえば、「このクラスをアプリケーション ドメイン間で送信する必要はあるか?」、「このクラスはリモーティングで使用されることはあるか?」、「ユーザーはこのクラスをどのように使用するのか?」、「ユーザーがこのクラスから、シリアライズが必要な新しいクラスを派生させる可能性はあるか?」といった質問への答えを考える必要があります。はっきりとわからない場合には、クラスをシリアライズ可能としてマークしてください。以下に示すような状況でない限り、すべてのクラスをシリアライズ可能としてマークするのが望ましいと考えられます。

  • アプリケーション ドメインを決して越えない。シリアル化の必要がないが、クラスがアプリケーション ドメインを越える必要がある場合には、クラスを MarshalByRefObject から派生させます。
  • クラスは、そのクラスの現在のインスタンスにのみ適用される特殊なポインタを格納している。たとえば、クラスがアンマネージド メモリやファイル ハンドルを含んでいる場合には、これらのフィールドを NonSerialized としてマークするか、そもそもそのクラスのシリアライズを行わないようにします。
  • 一部のデータ メンバが機密情報を含んでいる。この場合には、ISerializable をインプリメントし、必要なフィールドのみをシリアライズすることをお勧めします。

表示: