none
シリアル問題 DictionaryとISerializable RRS feed

  • 質問

  • 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();
                }
               
            }
        }

    2010年5月18日 2:22

回答

  • ダミーオブジェクトによるデシリアライズ対応は難しそうですね。(シリアライザが OnDeserialization を呼ぶ順序が不定なため、グラフへの挿入位置や挿入順序に依存しすぎる)

    グラフへの追加順か参照順ぐらいだろうと思っていたのですが、適当にツリー構造やループ構造を持つオブジェクトをデシリアライズした感じでは、.NET 2.0 ではシリアライズ前にヒープに並んでいた順序っぽいかんじに見えるようなかんじで呼び出されてびっくりでした(・ω・

    • 回答としてマーク get_star 2010年6月1日 8:42
    2010年6月1日 3:42

すべての返信

  • 辞書自身のデシリアライズがまだ終わっていないからでは?

    (辞書に限ったことではないのですが)辞書の中身が復元されて、辞書の中身にアクセスできるようになるのは、Deserialize() が完了したときです。

    オブジェクトグラフの後方に配置されたオブジェクトは、前方に配置されたオブジェクトからアクセスすることはできません。シリアライズ後に内容にあわせて自身を初期化するためには、ISerializable インターフェースだけではなく、IDeserializationCallback インターフェースなどを利用することになります。

     

    • 回答の候補に設定 山本春海 2010年5月24日 9:00
    2010年5月18日 4:32
  • 回答ありがとうございました。

    しかし、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();
                }
               
            }
        }
    }

    2010年5月18日 7:16
  • デシリアライズは内部のオブジェクトから順にインスタンス化されていきます。で、その後逆に外部のオブジェクトからインスタンス化完了後の処理(ISerializationCallback.OnDeserialization)が行われていきます。外部オブジェクトの OnDeserialization が呼び出されるのは内部オブジェクトの OnDeserialization より先なので、その時点では内部オブジェクトは不完全です。

    一番分かりやすいのは、Dictionary をそのまま SerializationInfo に格納するのではなく、KeyValuePair の形でひとつずつ(あるいは ToArray() とかして)SerializationInfo に格納することでしょう。

    • 回答の候補に設定 山本春海 2010年5月24日 9:00
    2010年5月18日 7:33
  • デシリアライズは内部のオブジェクトから順にインスタンス化されていきます。で、その後逆に外部のオブジェクトからインスタンス化完了後の処理(ISerializationCallback.OnDeserialization)が行われていきます。外部オブジェクトの OnDeserialization が呼び出されるのは内部オブジェクトの OnDeserialization より先なので、その時点では内部オブジェクトは不完全です。

    MSのサンプルを見て、OnDescrializationはNonSerializedのメンバーの初期化に使われるような使い方です。

    もしOnDescrializationが呼び出される時点で、内部オブジェクトが不完全であれば、NonSerializedメンバーの初期化はできないです。

    この点について、ちょっと理解できないです。

    一番分かりやすいのは、Dictionary をそのまま SerializationInfo に格納するのではなく、KeyValuePair の形でひとつずつ(あるいは ToArray() とかして)SerializationInfo に格納することでしょう。

    確かに、Dictionaryを使わずに、中身を自分でシリアル化すればいけますが、、、、

    避ける方法はないでしょうか。

    2010年5月18日 8:26
  • OnDeserialization では早すぎたんですね、すいません。
    # IObjectReference は良く使うのですが

    > 確かに、Dictionaryを使わずに、中身を自分でシリアル化すればいけますが、、、、
    > 避ける方法はないでしょうか。

    今回の場合、値がプリミティブなので大丈夫ですが、一般的には中身をシリアライズするという選択では解決できない問題ですね。(非プリミティブ型だとグラフへの追加かデシリアライズ時に複製されてしまうため)

    2010年5月26日 4:17
  • 時間がないのでアイデアだけ

    * ダミーオブジェクトを使ってデシリアライズイベントをハンドルする
    * IObjectReference を使ってフィックスアップのタイミングを取得して処理する

    といったかんじで対応できそうではあります。

    2010年5月28日 3:57
  • ダミーオブジェクトによるデシリアライズ対応は難しそうですね。(シリアライザが OnDeserialization を呼ぶ順序が不定なため、グラフへの挿入位置や挿入順序に依存しすぎる)

    グラフへの追加順か参照順ぐらいだろうと思っていたのですが、適当にツリー構造やループ構造を持つオブジェクトをデシリアライズした感じでは、.NET 2.0 ではシリアライズ前にヒープに並んでいた順序っぽいかんじに見えるようなかんじで呼び出されてびっくりでした(・ω・

    • 回答としてマーク get_star 2010年6月1日 8:42
    2010年6月1日 3:42
  • 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);
          }
        }
      }
    
    
    2010年6月2日 4:00