(C#)オブジェクトをファイルに保存して、読み込んで復元する

オブジェクトの内容をファイルに保存して、後日、そのファイルから読み込んでオブジェクトを復元する。たとえばソフトを終了するときにソフトの設定情報オブジェクトを保存して、次回の起動時に設定を復元する、ということをしたい。これを.NETのクラスで実装しようと何度か試みているのだが、どうもうまくいかない。

オブジェクトを保存したり復元したりするのを、C#ではシリアライズ、デシリアライズと呼ぶらしい。

.NETにはシリアライズするクラスがあって、これをつかって設定の保存とかを実装したいのだけど、毎回挫折している。なかなか、うまくいかない。

といっても保存と復元のメソッドを自分で書くのはしんどいので、できれば.NETのライブラリで簡単に終わらせたい。ということで、今回もこりずにシリアライズにチャレンジしてみる。以前はXMLだったが、今回はJSONでいってみたい。

(JSONではどうもうまくいかなかったので、後日、XMLで試した記事をその2にまとめた。)

JSONでシリアライズするときには、DataContractJsonSerializerというクラスを使うらしい。

手順

(1)参照設定に「System.Runtime.Serialization.dll」を追加する。

(2)usingを書く。

using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;

(3)クラスを書く。
今回はSQLのWhere句で使う式をツリー形式で持つためのクラスをサンプルにしてみる。

たとえば「where [英語] >= 70 and [テスト名称] = '2017年7月テスト'」という式をツリー形式で持ちたいとする。

図で表すとこんな感じ。


クラスは以下の通り。

       [DataContract]
        internal class mm50列情報
        {
            [DataMember]
            internal string colName;
            internal Type colType;
        }

        internal enum mmOp_e
        {
            mm等しい,
            mm等しくない,
            mm以上,
            mm以下,
            mmかつ,
            mmまたは,
        }

        [DataContract]
        internal class mm検索条件Base
        {

        }

        [DataContract]
        internal class mm比較式 : mm検索条件Base
        {
            [DataMember]
            internal mm50列情報 Column;
            [DataMember]
            internal mmOp_e Op;
            [DataMember]
            internal object v1;
            [DataMember]
            internal object v2;
        }

        [DataContract]
        internal class mm数値比較式 : mm比較式
        {
            internal mm数値比較式(mm50列情報 col, mmOp_e op, object v1, object v2)
            {
                this.Column = col;
                this.Op = op;
                this.v1 = v1;
                this.v2 = v2;
            }
        }

        [DataContract]
        internal class mm文字列比較式 : mm比較式
        {
            internal mm文字列比較式(mm50列情報 col, mmOp_e op, string s)
            {
                this.Column = col;
                this.Op = op;
                this.v1 = s;
            }
        }
        [DataContract]
        [KnownType(typeof(mm数値比較式))]
        [KnownType(typeof(mm文字列比較式))]
        internal class mm論理式 : mm検索条件Base
        {
            [DataMember]
            internal mm比較式 条件1 { set; get; }
            [DataMember]
            internal mm比較式 条件2 { set; get; }
            [DataMember]
            internal mmOp_e Op { set; get; }
        }

いくつか決まりごとがある。
  • 保存したいクラスには[DataContract]を追記する。
  • 保存したいメンバーには[DataMember]を追記する。
  • 継承しているクラスを使うときには[KnownType]を追記する。
    たとえば「mm論理式」クラスでは「条件1」メンバーが「mm比較式」型だが、ここには「mm数値比較式」のオブジェクトが代入されるかもしれない。そうしたときに継承するクラスを追記する。
なかなか面倒なのだが、保存と復元をすべてお任せにできるのだから、このくらいのコストは仕方がない。

(4)オブジェクトを保存し、復元する。
保存と復元はDataContractJsonSerializerクラスを使う。

 mm50列情報 col1 = new mm50列情報() { colName = "英語", colType = typeof(int) };
  mm比較式 cn1 = new mm数値比較式(col1, mmOp_e.mm以上, 70, null);

  mm50列情報 col2 = new mm50列情報() { colName = "テスト名称", colType = typeof(string) };
  mm比較式 cn2 = new mm文字列比較式(col2, mmOp_e.mm等しい, "2017年7月テスト");

  mm論理式 ex = new mm論理式() { 条件1 = cn1, Op = mmOp_e.mmかつ, 条件2 = cn2 };

  // 論理式を保存する
  var js = new DataContractJsonSerializer(ex.GetType());
  System.IO.FileStream st = new System.IO.FileStream("test.json", System.IO.FileMode.OpenOrCreate);
  js.WriteObject(st, ex);

  // 論理式を復元する
  st.Seek(0, System.IO.SeekOrigin.Begin);
  object obj =   js.ReadObject(st);
  ex = obj as mm論理式;

注意しないといけないのは、保存するときにクラスの型を指定する必要がある。そして、復元するときは、保存したときに指定したクラスのオブジェクトしか復元できない。つまり、復元するときは、このオブジェクトは何のクラスだったのかを知っておく必要がある。

保存内容される内容は以下の通り。改行が無いのですこぶる読みにくい。


読みやすく改行を入れてみる。

{
  "Op":4,
  "条件1":{
    "__type":"testA.mm数値比較式:#serializ",
    "Column":{ "colName":"英語"},"Op":2, "v1":70, "v2":null
  },
  "条件2":{
    "__type":"testA.mm文字列比較式:#serializ",
    "Column":{"colName":"テスト名称"},
    "Op":0,"v1":"2017年7月テスト","v2":null
  }
}

いくつか気がつくのだけど、
  • enumは整数値で保存される。
    もしうっかりしてenmuのメンバーの順番を変えると復元できなくなる。たとえば「mm以上」の前に「mm未満」を追加するとアウト。enumメンバーには初期値を設定しておくのがよさそう。
  • 継承したクラスの情報が追記されている。
  • 自分が何者か(クラス情報)は無い。
JSON情報だけもらっても、どのクラスのオブジェクトなのかを知っていないと復元はできない。ということは、継承したクラスが数種類あって、それらのオブジェクトをいくつか保存する場合は、クラス名も保存しておく必要があるということですな。

未解決問題

あと、解決できていないのが、「Type colType;」メンバーを保存できないこと。下記のように型情報を追加してみたのだが、保存時に例外が起きる。

        [DataContract]
        [KnownType(typeof(int))]
        [KnownType(typeof(string))]
        internal class mm50列情報
        {
            [DataMember]
            internal string colName;
            [DataMember]
            internal Type colType;
        }

例外は以下の通り。

System.Runtime.Serialization.SerializationException: 'データ コントラクト名 'RuntimeType:http://schemas.datacontract.org/2004/07/System' を持つ型 'System.RuntimeType' は予期されていません。DataContractSerializer を使用している場合は DataContractResolver を使用することを検討するか、静的に認知されていないすべての型を既知の型の一覧に追加してください。このためには、たとえば KnownTypeAttribute 属性を使用するか、シリアライザーへ渡される既知の型の一覧にこれらの型を追加します。'

さすがにTypeデータはシリアライズできないのかな。

まとめ

DataContractJsonSerializerを使って保存と復元をするときの注意点をまとめてみる。
  • 将来、クラスの構造が変わると保存したデータから復元するのが大変そう。
  • クラスの構造を簡素にしておくのがよさそう。
  • 継承は控えめに。継承するごとに[KnownType]の追記が増えていくことになって面倒。
  • 保存と復元のための簡素なクラスを作っておいて、複雑な処理は別のクラスに作るのがよさそう。
  • 逆に、保存と復元用に簡素なオブジェクトを用意するのもありかも。
  • 保存するときクラス名も別に保存しておくこと。
  • なかなか面倒。
JSONをあきらめてXMLで試した記事もまとめた。(2017/10/16追記)




コメント

このブログの人気の投稿

varchar をデータ型 numeric に変換中に、算術オーバーフロー エラーが発生しました。