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

 

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

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

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

内容

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

はじめに

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

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

永続的な記憶域

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

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

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

値によるマーシャリング

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

オブジェクトが 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();

// 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 に置き換え、前と同じように Serialize と Serialize を呼び出します。 前の例では、このフォーマッタの出力は、次のようになります。

<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>

Serializable 属性は継承できないことに注意してください。 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 を実装しない場合は、既定のシリアル化ポリシーが使用され、NonSerialized としてマークされていないすべてのフィールドがシリアル化されます。

バージョン管理

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

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

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

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

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

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