トップ回答者
シリアル問題 DictionaryとISerializable

質問
-
Visual Studio 2005 C# .Net 2.0
下記の問題が出てきました。
ご存知の方がいらっしゃったら、教えていただければ幸いです。
ISerializableを利用して、シリアライズをコントロールしたいですが、Dictionaryの場合、おかしいと思っています。
コンストラクターの中で、データの取得はできるはずだと思うが、①のところでデータの取得は失敗
Deserializeの実行後、②のところでデータを出力はできたので、取得は成功したことが分かる
でも、コンストラクターの中でデータ取得はできると思っているが、もしかしてそうではないですか?
[Serializable]
public class CategoryName : ISerializable
{
public Dictionary<long, String> Category1_ = new Dictionary<long, String>();
public CategoryName() { }
protected CategoryName(SerializationInfo info, StreamingContext context)
{
Category1_ = (Dictionary<long, String>)info.GetValue("Category1_", typeof(Dictionary<long, String>));
try
{
Console.WriteLine(Category1_[1]); //① ここで出力したいが、データの取得はできず、エラーが発生する
}
catch (Exception e)
{
Console.WriteLine(e.StackTrace);
}
}
[SecurityPermission(SecurityAction.LinkDemand,
Flags = SecurityPermissionFlag.SerializationFormatter)]
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Category1_", Category1_);
}
}
class Program
{
static void Main(string[] args)
{
String filename = "c:\\save";
//書き込む
CategoryName CategoryName_ = new CategoryName();
CategoryName_.Category1_.Add(1, "jalsdfjiouwerqu9u823rufasdfasdlkjfasdklf");
using (System.IO.FileStream fs = new System.IO.FileStream(filename, System.IO.FileMode.Create, System.IO.FileAccess.Write))
{
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter bf = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
try
{
bf.Serialize(fs, CategoryName_);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message + "\n" + ex.StackTrace);
}
fs.Close();
}
//読み取る
CategoryName b;
using (System.IO.FileStream fs = new System.IO.FileStream(filename, System.IO.FileMode.Open, System.IO.FileAccess.Read))
{
try
{
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter f = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
b = (CategoryName)f.Deserialize(fs);
Console.WriteLine(b.Category1_[1]); //② ここでもう一度出力する。データの出力ができた。
}
catch (Exception ex)
{
Console.WriteLine(ex.Message + "\n" + ex.StackTrace);
}
fs.Close();
}
}
}
回答
すべての返信
-
辞書自身のデシリアライズがまだ終わっていないからでは?
(辞書に限ったことではないのですが)辞書の中身が復元されて、辞書の中身にアクセスできるようになるのは、Deserialize() が完了したときです。
オブジェクトグラフの後方に配置されたオブジェクトは、前方に配置されたオブジェクトからアクセスすることはできません。シリアライズ後に内容にあわせて自身を初期化するためには、ISerializable インターフェースだけではなく、IDeserializationCallback インターフェースなどを利用することになります。
- 回答の候補に設定 山本春海 2010年5月24日 9:00
-
回答ありがとうございました。
しかし、IDeserializationCallbackを実装してみたが、結果は一緒です。
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
using System.Security.Permissions;
namespace ConsoleApplication1
{
[Serializable]
public class CategoryName : ISerializable, IDeserializationCallback //区分名リスト
{
public Dictionary<long, String> Category1_ = new Dictionary<long, String>();
public CategoryName() { }
protected CategoryName(SerializationInfo info, StreamingContext context)
{
Category1_ = (Dictionary<long, String>)info.GetValue("Category1_", typeof(Dictionary<long, String>));
try
{
Console.WriteLine(Category1_[1]);
}
catch (Exception e)
{
Console.WriteLine(e.StackTrace);
}
}
[SecurityPermission(SecurityAction.LinkDemand,
Flags = SecurityPermissionFlag.SerializationFormatter)]
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Category1_", Category1_);
}
public void OnDeserialization(object sender)
{
try
{
Console.WriteLine(Category1_[1]); //③ OnDeserializationでも取得失敗
}
catch (Exception e)
{
Console.WriteLine(e.StackTrace);
}
}
}
class Program
{
static void Main(string[] args)
{
String filename = "c:\\save";
//書き込む
CategoryName CategoryName_ = new CategoryName();
CategoryName_.Category1_.Add(1, "jalsdfjiouwerqu9u823rufasdfasdlkjfasdklf");
using (System.IO.FileStream fs = new System.IO.FileStream(filename, System.IO.FileMode.Create, System.IO.FileAccess.Write))
{
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter bf = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
try
{
bf.Serialize(fs, CategoryName_);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message + "\n" + ex.StackTrace);
}
fs.Close();
}
//読み取る
CategoryName b;
using (System.IO.FileStream fs = new System.IO.FileStream(filename, System.IO.FileMode.Open, System.IO.FileAccess.Read))
{
try
{
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter f = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
b = (CategoryName)f.Deserialize(fs);
Console.WriteLine(b.Category1_[1]);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message + "\n" + ex.StackTrace);
}
fs.Close();
}
}
}
} -
デシリアライズは内部のオブジェクトから順にインスタンス化されていきます。で、その後逆に外部のオブジェクトからインスタンス化完了後の処理(ISerializationCallback.OnDeserialization)が行われていきます。外部オブジェクトの OnDeserialization が呼び出されるのは内部オブジェクトの OnDeserialization より先なので、その時点では内部オブジェクトは不完全です。
一番分かりやすいのは、Dictionary をそのまま SerializationInfo に格納するのではなく、KeyValuePair の形でひとつずつ(あるいは ToArray() とかして)SerializationInfo に格納することでしょう。
- 回答の候補に設定 山本春海 2010年5月24日 9:00
-
デシリアライズは内部のオブジェクトから順にインスタンス化されていきます。で、その後逆に外部のオブジェクトからインスタンス化完了後の処理(ISerializationCallback.OnDeserialization)が行われていきます。外部オブジェクトの OnDeserialization が呼び出されるのは内部オブジェクトの OnDeserialization より先なので、その時点では内部オブジェクトは不完全です。
MSのサンプルを見て、OnDescrializationはNonSerializedのメンバーの初期化に使われるような使い方です。
もしOnDescrializationが呼び出される時点で、内部オブジェクトが不完全であれば、NonSerializedメンバーの初期化はできないです。
この点について、ちょっと理解できないです。
一番分かりやすいのは、Dictionary をそのまま SerializationInfo に格納するのではなく、KeyValuePair の形でひとつずつ(あるいは ToArray() とかして)SerializationInfo に格納することでしょう。
確かに、Dictionaryを使わずに、中身を自分でシリアル化すればいけますが、、、、
避ける方法はないでしょうか。
-
IObjectReference を使っても無理ですね。GetRealObject() は OnDeserialized イベントより早く発生するので、まったく効果がありませんでした。
少し整理しておくと、
○ オブジェクトが、自身のデシリアライズ完了を知ることができないか?
デシリアライズの完了は、Deserialized イベント (OnDeserializedAttribute または IDeserializationCallback) を利用して知ることができる。
○ デシリアライズ完了の Deserialized イベントは、いつ発生するのか
個々のオブジェクトではなく、全てのオブジェクトが再構築されおわった段階で呼び出される。
○ デシリアライズ時の個々のオブジェクトの再構築はどこからされるのか
MSDN 等のドキュメントにある通り、内側から外側にむけて構築されます。
○ デシリアライズ時のオブジェクト間の再構築順序はどうなっているか
シリアライザとデシリアライザの実装に依存していると思われますが、不完全なオブジェクト参照を扱う手段がないため、個々のオブジェクトんおデシリアライズの実装によってオブジェクトが取り出された順に再構築されると思われます。
○ デシリアライズ完了の Deserialized イベントは、どの順序で発生するのか
オブジェクトの格納順序や取り出し順序と関係なく、デシリアライザの実装に依存した順序で呼び出されます。
実験した感じでは、.NET 2.0 の BniaryFormatter では、シリアライズ前のヒープ上の順序に依存しているように見えましたが、偶然かもしれません。(実験の範囲では、オブジェクトグラフ上の関係とは無関係に new する順序の変更で入れ替え等で呼ばれる順序が変化することを確認しています)
また、今回の問題点に関して補足すると、(全部 MSDN の説明に書いてありますが)Dictionary は、自身が base class になった場合に、Add() にカスタマイズが行われている場合があることを考慮しています。このため、Dictionary はデシリアライズ用のコンストラクタの中では、SerializationInfo に保存された中身を取り出すだけで、自身に Add() を行いません。取り出された各要素が Add() されるのは、Deserialized イベント (OnDeserialization メソッド) の時点です。
このため、辞書の中身にキーを用いてアクセスできるようになるのは、辞書のインスタンスに対して Deserialized イベントが発生した後になります。前述のように、Deserialized イベントの順序は制御が難しいため、Dictionary のように Deserialized イベント を利用したメンバに安全にアクセスできるタイミングは実質的に存在しないのではないかと思います。この例であれば、メンバの Category1_ の Deserialized イベント が先に発生していた場合には、CategoryName 自身の Deserialized イベントで Dictionary の中身にアクセスできることになります。
こんなかんじなので、ケースバイケースで対応するしかないのかな、と思います。たとえば今回のケースでは Category1_ を読み取り専用にできる場合、以下のようなかんじになるでしょう。
[Serializable] public class CategoryName : ISerializable { private MyDictionary category1 = new MyDictionary(); public Dictionary<long, String> Category1_ { get { return this.category1; } } public CategoryName() { } protected CategoryName(SerializationInfo info, StreamingContext context) { category1 = (MyDictionary)info.GetValue("Category1_", typeof(MyDictionary)); category1.DictionaryDeserialized += Category1_Deserialized; } [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] public virtual void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("Category1_", category1); } [NonSerialized] bool raiseDeserializationEvent = false; [OnDeserialized] private void OnCategoryNameDeserialized(StreamingContext context) { if (raiseDeserializationEvent) this.OnDeserialization(); else raiseDeserializationEvent = true; } private void Category1_Deserialized(object sender, EventArgs e) { if (raiseDeserializationEvent) this.OnDeserialization(); else raiseDeserializationEvent = true; } protected virtual void OnDeserialization() { // this と Category1_ が、共にデシリアライズ完了したのでアクセス可能 Console.WriteLine("my deserialization"); Console.WriteLine(Category1_[1]); } [Serializable] private class MyDictionary : Dictionary<long, String> { public MyDictionary() { } protected MyDictionary(SerializationInfo info, StreamingContext context) : base(info, context) { } public event EventHandler DictionaryDeserialized; public override void OnDeserialization(object sender) { base.OnDeserialization(sender); if (this.DictionaryDeserialized != null) this.DictionaryDeserialized(this, EventArgs.Empty); } } }