.NET Frameworkでのオブジェクトのシリアル化

 

ピート・オーバーメイヤーとジョナサン・ホーキンス
Microsoft Corporation

2001 年 8 月
更新日: 2002 年 3 月

概要: シリアル化を使用する理由 最も重要な 2 つの理由は、オブジェクトの状態をストレージ メディアに保持し、後の段階で正確なコピーを再作成できるようにし、あるアプリケーション ドメインから別のアプリケーション ドメインに値を指定してオブジェクトを送信することです。 たとえば、シリアル化は、セッション状態を ASP.NET に保存したり、Windows フォームのクリップボードにオブジェクトをコピーしたりするために使用されます。 また、リモート処理でオブジェクトを 1 つのアプリケーション ドメインから別のアプリケーション ドメインに値渡しするためにも使用されます。 この記事では、Microsoft .NET Frameworkで使用されるシリアル化の概要について説明します。 (9 ページの印刷)

内容

はじめに
永続的な記憶域
値によるマーシャリング
基本的なシリアル化
選択的シリアル化
カスタムのシリアル化
シリアル化プロセスの手順
バージョン管理
シリアル化のガイドライン

はじめに

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

オブジェクト指向環境でシリアル化機構を実装する場合は、使いやすさと柔軟性の間での数多くのトレードオフについて考慮する必要があります。 プロセスを十分に制御できる場合は、プロセスの大部分を自動化できます。 たとえば、単純なバイナリ シリアル化では不十分な状況が発生する場合や、シリアル化が必要なクラス内のフィールドを決定するだけの明確な理由がある場合があります。 以下のセクションでは、.NET Framework に用意されている堅牢なシリアル化機構について検討し、必要に応じてプロセスをカスタマイズするためのいくつかの重要な機能について説明します。

永続的な記憶域

多くの場合、オブジェクトのフィールドの値をディスクに格納し、後の段階でこのデータを取得する必要があります。 これはシリアル化を使用しなくても簡単に実現できますが、シリアル化を使用しない方法は煩雑でエラーの原因となることが多く、オブジェクトの階層を追跡する必要がある場合にはさらに複雑になります。 Imagine何千ものオブジェクトを含む大規模なビジネス アプリケーションを作成し、各オブジェクトのディスクとの間でフィールドとプロパティを保存および復元するコードを記述する必要があります。 シリアル化は、最小限の労力でこの目標を達成するための便利なメカニズムを提供します。

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

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

値によるマーシャリング

オブジェクトは、作成されるアプリケーション ドメインでのみ有効です。 オブジェクトが MarshalByRefObject から派生しているか 、シリアル化可能としてマークされていない限り、オブジェクトをパラメーターとして渡すか、結果として返そうとすると失敗します。 オブジェクトが シリアル化可能としてマークされている場合、オブジェクトは自動的にシリアル化され、1 つのアプリケーション ドメインから他方のアプリケーション ドメインに転送され、逆シリアル化されて 2 番目のアプリケーション ドメイン内のオブジェクトの正確なコピーが生成されます。 通常、このプロセスは値によるマーシャリングと呼ばれます。

オブジェクトが MarshalByRefObject から派生すると、オブジェクト参照は、オブジェクト自体ではなく、あるアプリケーション ドメインから別のアプリケーション ドメインに渡されます。 MarshalByRefObject から派生するオブジェクトをシリアル化可能としてマークすることもできます。 このオブジェクトをリモート処理で使用すると、 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();

// Here's the proof
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=https://schemas.xmlsoap.org/soap/encoding/
  xmlns:SOAP- ENV=https://schemas.xmlsoap.org/soap/envelope/
  SOAP-ENV:encodingStyle=
  "https://schemas.microsoft.com/soap/encoding/clr/1.0
  https://schemas.xmlsoap.org/soap/encoding/"
  xmlns:a1="https://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>

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

public class MyStuff : MyObject 
{
  public int n3;
}

シリアル化属性の使用は便利ですが、前述のように制限があります。 シリアル化はコンパイル後にクラスに追加できないため、クラスをシリアル化にマークするタイミングに関するガイドライン (後述のシリアル化ガイドラインを参照) を参照してください。

選択的シリアル化

クラスには、シリアル化できないフィールドが含まれていることがよくあります。 たとえば、クラスのメンバー変数の 1 つにスレッド 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 は、この目的で提供されるコンストラクターを使用してクラスに渡されます。 オブジェクトが逆シリアル化されると、コンストラクターに配置された可視性制約はすべて無視されるため、クラスをパブリック、保護、内部、またはプライベートとしてマークできます。 クラスがシールされていない限り、コンストラクターを保護することをお勧めします。その場合、コンストラクターはプライベートとしてマークする必要があります。 オブジェクトの状態を復元するには、シリアル化中に使用される名前を使用して 、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 を実装していない場合は、既定のシリアル化ポリシーが使用され、非シリアル化としてマークされていないすべてのフィールドがシリアル化されます。

バージョン管理

.NET Frameworkでは、バージョン管理とサイド バイ サイド実行のサポートが提供され、クラスのインターフェイスが変わらない場合は、すべてのクラスがバージョン間で機能します。 シリアル化はインターフェイスではなくメンバー変数を扱うので、バージョン間でシリアル化されるクラスにメンバー変数を追加または削除する場合は注意が必要です。 これは、 ISerializable を実装しないクラスに特に当てはまります。 メンバー変数の追加、変数の型の変更、名前の変更など、現在のバージョンの状態の変更は、以前のバージョンでシリアル化された場合、同じ型の既存のオブジェクトを正常に逆シリアル化できないことを意味します。

バージョン間でオブジェクトの状態を変更する必要がある場合、クラス作成者には次の 2 つの選択肢があります。

  • ISerializable を実装します。 これにより、シリアル化と逆シリアル化のプロセスを正確に制御できるため、逆シリアル化中に将来の状態を正しく追加および解釈できます。
  • 非正規化された属性を使用して、不要なメンバー変数をマークします。 このオプションは、クラスの異なるバージョン間で軽微な変更が予想される場合にのみ使用してください。 たとえば、新しい変数が新しいバージョンのクラスに追加された場合、その変数を NonSerialized としてマークして、クラスが以前のバージョンと互換性を保たれるようにすることができます。

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

新しいクラスを設計する場合は、コンパイル後にクラスをシリアル化できないため、シリアル化を検討する必要があります。 いくつかの質問: アプリケーション ドメイン間でこのクラスを送信する必要がありますか。 このクラスをリモート処理で使用する可能性があるかどうかについて検討します。 ユーザーはこのクラスに対して何をしますか? シリアル化する必要がある新しいクラスを私から派生させるかもしれません。 判断に迷った場合は、クラスをシリアル化可能としてマークします。 次の場合を除き、すべてのクラスをシリアル化可能としてマークすることをお勧めします。

  • アプリケーション ドメインを越えることはありません。 シリアル化が必要なく、クラスがアプリケーション ドメインをまたがる必要がある場合は、 MarshalByRefObject からクラスを派生させます。
  • クラスには、クラスの現在のインスタンスにのみ適用できる特別なポインターが格納されます。 たとえば、クラスにアンマネージ メモリまたはファイル ハンドルが含まれている場合は、これらのフィールドが非シリアル化としてマークされているか、クラスをまったくシリアル化しないことを確認します。
  • 一部のデータ メンバーには機密情報が含まれています。 この場合、 ISerializable を実装し、必要なフィールドのみをシリアル化することをお勧めします。