.NET ROOM.NET ROOMトップへCOMRADE総合トップへ
「4:オブジェクトのシリアライズ」へ

SerializableAttributeの問題点

このようにSerializableAttributeは、簡単にシリアライズ/デシリアライズを行なうことができる便利なものです。
ほとんどの場合は単にSerializableAttributeを指定するだけで十分です。
ただし、SerializableAttributeを使用する場合に発生するいくつかの問題点があります。

問題点1: インスタンスのフィールドがすべてシリアライズされる

問題点2: Staticフィールドのシリアライズがうまくいかない

問題点3: 派生クラスにSerializableAttributeを指定する場合、
基底クラスにもSerializableAttributeが指定されている必要がある


それぞれの問題点に関して、もう少し詳しく説明してゆきましょう。

まず、問題点1に関してですが、SerializableAttributeを使用する際、
とくに指定しない場合はフィールドがPrivateフィールドを含めてすべてシリアライズされます。
これは以下のような場合にとくに問題となります。

パスワードなどの機密情報をPrivateフィールドに保存している場合は
そのままシリアライズされてしまう


付録のPasswordHolderSerializeTestディレクトリに収録しているコードを実行すると、
PasswordHolderクラスがシリアライズされたバイナリファイル(passwd.bin)が作成されます。
このファイルをバイナリエディタで開いてみると、パスワードがそのまま
平文で保存されていることがわかります。
この他、Privateフィールドにメソッドのみの内部クラスなど、
データを永続化する必要のないクラスのインスタンスの参照を保持している場合もあります。

 問題点2は、constフィールドでも同様です。
これは問題点1で説明したように、単純なシリアライズではインスタンスのフィールドしか
シリアライズされないために生じます。
定数やStaticな値は、通常保存する必要がないので問題にはならないはずなのですが、
参照先がconst、もしくはStaticな値と同じであるかどうかを検証する必要があるときには問題となります。
これは対象となるフィールドが値型であれば問題がありませんが、参照型である場合に問題になります。
問題になるケースの例としては、Singletonデザインパターンのクラスを作成した場合などが考えられます。

 リスト3のように、SerializableClassにStaticなフィールドと
それを返すプロパティを追加して確認してみましょう。

リスト3:Staticフィールドとプロパティを追加したSerializableClass
[Serializable]
public class SerializableClass {
 static readonly SerializableClass static_field =
  new SerializableClass(1);
 int data;
 public SerializableClass(int data) {
  Data = data;
 }
 public int Data {
  get { return data; }
  set { data = value; }
 }
 public static SerializableClass StaticProperty {
  get { return static_field; }
 }
}


確認方法はリスト4です。
なお、ここで紹介したコードは、付録のIncorrectStaticFieldSerializeTestディレクトリに収録しています。
Staticなフィールドを使用しているので出力結果の2行目は“True”になると期待しますが、
実際は“False”になってしまいます。

リスト4:SerializableAttributeに関する問題点2の確認方法
const string FILENAME = "statictest.bin";
SerializableClass sc =
 SerializableClass.StaticProperty, deserialized_sc;
BinaryFormatter bf = new BinaryFormatter();
using (FileStream fs = File.OpenWrite(FILENAME)) {
 bf.Serialize(fs, sc);
}
using (FileStream fs = File.OpenRead(FILENAME)) {
 deserialized_sc = (SerializableClass)bf.Deserialize(fs);
}
Console.WriteLine("Original : {0}, Deserialized : {1}",
            sc.Data, deserialized_sc.Data);
Console.WriteLine(sc == deserialized_sc);


 続いて問題点3です。
単にSerializableAttributeを付けただけでは、基底クラスがシリアライズ可能ではないので、
BinaryFormatterなどでシリアライズする際にSerializeExceptionがThrowされてしまい、
きちんとシリアライズできません。
そのため、クラスをシリアライズしてはいけないという明確な理由がない限り、
独自クラスはシリアライズ可能にしておいたほうが賢明かもしれません。

 以上がSerializableAttributeを使用する際の問題点です。
もし自作のクラスで上記のような問題が発生した場合はどうすれば良いのでしょうか?

  これらの問題に関してはそれぞれ解決方法があるので、以降で説明してゆきます。


ページトップへ


シリアライズ対象のフィールドを指定
問題点1の解決方法


 フィールドがすべてシリアライズされてしまうという問題は、
シリアライズの対象になるフィールドを指定することで解消できます。
解決方法には、次の2つがあります。

@シリアライズしたくないフィールドを指定する
Aシリアライズの方法を自分で制御する


 @は、「NonSerializedAttribute」を該当するフィールドに 対して指定するだけです。
NonSerializedAttributeは、AttributeUsageがAttributeTargets.FieldであるAttributeなので、
フィールドにのみ指定可能です。

 前述のとおり、ほとんどの場合は単にSerializableAttri buteを指定するだけで問題ないのですが、
このNonSeria lizedAttributeを加えることで、さらにカバーできる範囲が広がります。

 Aは、ISerializableインターフェイスを実装することで可能になります。
ただし、ISerializableインターフェイスを使用する場合は、単にSerializableAttributeを指定する場合と違って
複雑なので、本当に必要な場合を除いて、ここまで説明してきた方法で対処したほうが良いでしょう。

 場合によっては、NonSerializedAttributeを使用してシリアライズ時に
保持しなかったフィールドをロードし直さなければならないこともあります。
その場合には、IDeserializationCallbackインターフェイスを使用できます。

 IDeserializationCallbackインターフェイスは、以下のようなインターフェイスです。

    public interface IDeserializationCallback {
     void OnDeserialization(object sender);
    }

 メソッドのみで構成されるクラスの初期化などに使用すると良いでしょう。

 問題点2と問題点3を解決するためには、先ほど簡単に触れた、
ISerializableインターフェイスを実装することによる「シリアライズの制御」が必要になります。
そこで、解決方法を説明する前に、ISerializableインターフェイスに関して少し説明しておきましょう。


ページトップへ


ISerializableインターフェイス

 ISerializableインターフェイスは、以下のようなインターフェイスです。

   public interface ISerializable {
    void GetObjectData(SerializationInfo info,
                 StreamingContext context);
   }

 基本的な使い方をリスト5に示します。

リスト5:ISerialiizeableインターフェイスの実装
[Serializable]
public class SerializableClass : ISerializable {
 int data;
 public SerializableClass(int data) {
  Data = data;
 }
 SerializableClass(SerializationInfo info,
            StreamingContext context) {
  data = (int)info.GetValue("data", typeof(int));
 }
 void ISerializable.GetObjectData(
  SerializationInfo info, StreamingContext context) {
  info.AddValue("data", data);
 }
 public int Data {
  get { return data; }
  set { data = value }
 }
}


 AddValueメソッドで値を格納し、GetValueメソッドで値を取り出します。
値の格納/取り出しに使用する名前はなんでもかまいませんが、間違わないように、
通常は格納対象のフィールド名をそのまま使用したほうが良いかもしれません。
なお、GetValueメソッドが呼び出されるのは、SerializableClassのコンストラクタです。
このSerializationInfoとStreamingContextを引数に取るコンストラクタは、
オブジェクトのデシリアライズ時に呼ばれる特殊なコンストラクタで、Privateでもかまいません。
派生クラスを作成する可能性がある場合は、internal、あるいはprotectedにしておくことをお勧めします。

 先のパスワードの例では、パスワードを暗号化して保存しておくという方法も考えられます。

 SerializableAttributeと違い、ISerialiizeableインターフェイスから派生する場合は、
GetObjectDataの中でAddValueメソッドで直接指定しなければ、
シリアライズされませんので注意してください。


前へ | ページトップへ | 次へ