BinaryFormatterクラス

組み込み型以外のバイナリ化

BitConverterクラスBinaryReader/Writerクラスを使用することで、組み込み型オブジェクトとbyte型配列(バイナリ)とを相互変換することができます。
しかしこれらの方法では自作クラスなどの場合にはフィールドをひとつずつ変換する必要があり、なかなか手間がかかります。
クラスをまとめてバイナリとして扱う方法としてBinaryFormatterが提供されています。

シリアライズとデシリアライズ

プログラムは様々なデータをメモリ上に保存します。
これをファイルに保存したりネットワーク通信するために、データを一定の規則に従って並べることをシリアライズ(シリアル化)といいます。
シリアライズしたデータを元のデータに戻すことをデシリアライズ(復元)といいます。

シリアライズは日本語では「直列化」と訳されます。
複数のデータをbyte型配列にまとめることもシリアライズといえますし、テキスト形式にするのもシリアライズと言えます。
要するにデータを先頭から末尾まで順に読み込める形式に変換することをシリアライズと言います。

BinaryFormatter

BinaryFormatterクラスは「System.Runtime.Serialization.Formatters.Binary」名前空間に存在するので、コード先頭のusingに追加しておきます。
Streamクラスも使用するので、「System.IO」も追加しておきます。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
//↓を追加
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

シリアライズ

ここでは以下の自作クラスをシリアライズしてみます。


class SimpleClass
{
    public int Num;
    public string Str;

    public SimpleClass(int n = 0, string s = null)
    {
        Num = n;
        Str = s;
    }
}

シリアライズを行うにはまずシリアライズしたいクラスの直前にSerializableAttributeを付加します。
属性名の「Attribute」の部分は省略可能なので、「[Serializable]」と記述します。


[Serializable]
class SimpleClass
{
    //省略
}

これでこのクラスはシリアライズが可能となります。

BinaryFormatterはバイナリ化したデータの書き込み先にストリームを利用します。
ここでは外部ファイルに保存するためにFileStreamクラスを使用します。

データをシリアル化するにはまずBinaryFormatterのインスタンスを生成し、Serializeメソッドを実行します。


string path = "test.bin";

SimpleClass sc = new SimpleClass(123, "abc");

using (FileStream fs = new FileStream(path, FileMode.Create))
{
    BinaryFormatter bf = new BinaryFormatter();

	//データをシリアル化して
    //FileStreamに書き込み
    bf.Serialize(fs, sc);
}

第一引数はシリアル化したデータを書き込むStreamを指定します。
第二引数はシリアル化したいデータを指定します。

これで指定のパスにSimpleClassをバイナリ化したファイルが作成されます。

デシリアライズ

デシリアライズにはDeserializeメソッドを使用します。


string path = "test.bin";

SimpleClass sc;

using (FileStream fs = new FileStream(path, FileMode.Open))
{
    BinaryFormatter bf = new BinaryFormatter();
    sc = (SimpleClass)bf.Deserialize(fs);
}

Console.WriteLine("{0} {1}", sc.Num, sc.Str);
123 abc

引数には復元したい対象のStreamを指定します。
今回は先ほどシリアライズしたデータをFileStreamで読み込み、それを渡しています。
戻り値はobject型なので、適切な型にキャストします。

これでインスタンスscにはファイルから読み取った値が復元されます。

シリアライズから除外するフィールドの指定

標準ではクラスのすべてのフィールド(と自動実装プロパティ)の状態がシリアライズされますが、シリアライズの対象から外したいフィールドがある場合はそのフィールドにNonSerializedAttributeを付加します。


[Serializable]
class SimpleClass
{
    [NonSerialized]
    public int Num;
    public string Str;

    public SimpleClass(int n = 0, string s = null)
    {
        Num = n;
        Str = s;
    }
}

string path = "test.bin";

SimpleClass sc = new SimpleClass(123, "abc");

using (FileStream fs = new FileStream(path, FileMode.Create))
{
    BinaryFormatter bf = new BinaryFormatter();
    bf.Serialize(fs, sc);
}

using (FileStream fs = new FileStream(path, FileMode.Open))
{
    BinaryFormatter bf = new BinaryFormatter();
    sc = (SimpleClass)bf.Deserialize(fs);
}

Console.WriteLine("{0} {1}", sc.Num, sc.Str);
0 abc

NonSerializedAttributeが付けられたフィールドNumの値はファイルに保存されず、復元しても規定値である「0」が割り当てられていることが分かります。
NonSerializedAttributeはフィールドの直前に付ける必要があるので、フィールドNumのすぐ下のフィールドStrには影響しません。

自動実装プロパティの場合

上記の方法で除外できるのはフィールドのみで、自動実装プロパティで生成されるフィールドは除外できません。
これを除外したい場合は自動実装プロパティをあきらめてフィールドを用意し、通常のプロパティで読み書きします。


[Serializable]
class SimpleClass
{
    //以下はエラー
    //[NonSerialized]
    //public int Num { get; set; }

    [NonSerialized]
    public int _num;
    public int Num
    {
        get { return _num; }
        set { _num = value; }
    }

    public string Str;

    public SimpleClass(int n = 0, string s = null)
    {
        Num = n;
        Str = s;
    }
}

C#7.3からは以下の記述で自動実装プロパティも除外対象にできます。


[Serializable]
class SimpleClass
{
    [field: NonSerialized]
    public int Num { get; set; }
    public string Str;

    public SimpleClass(int n = 0, string s = null)
    {
        Num = n;
        Str = s;
    }
}

フィールドの追加

クラスをシリアライズしてファイル保存した後、プログラムのバージョンアップ等でクラスにフィールドを追加するとデータの整合性が取れなくなり、以前にシリアライズしたファイルは読み込めなくなります。
これに対応するには新しく追加したフィールドにOptionalFieldAttributeを付加します。

OptionalFieldAttributeは「System.Runtime.Serialization」名前空間にあるので、usingに追加するか完全修飾名を使用します。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
//↓を追加
using System.Runtime.Serialization;

まず最初に以下のクラスをシリアライズしてファイルに保存しておきます。


[Serializable]
class SimpleClass
{
    public int Num;
    public string Str;

    public SimpleClass(int n = 0, string s = null)
    {
        Num = n;
        Str = s;
    }
}

string path = "test.bin";

SimpleClass sc = new SimpleClass(123, "abc");
using (FileStream fs = new FileStream(path, FileMode.Create))
{
    BinaryFormatter bf = new BinaryFormatter();
    bf.Serialize(fs, sc);
}

このクラスに新しいフィールドを追加する場合、フィールドの直前にOptionalFieldAttributeを付加します。


[Serializable]
class SimpleClass
{
    public int Num;
    public string Str;

    //新しく追加するフィールド
    [OptionalField]
    public int AddedNum;

    public SimpleClass(int n = 0, string s = null, int n2 = 0)
    {
        Num = n;
        Str = s;
        AddedNum = n2;
    }
}

OptionalFieldAttributeが付けられたフィールドは、デシリアライズの際に指定のフィールドが存在すればその値を復元します。
存在しない場合は既定値が割り当てられ、エラーになることなく読み込みが可能となります。


SimpleClass sc;

using (FileStream fs = new FileStream(path, FileMode.Open))
{
    BinaryFormatter bf = new BinaryFormatter();
    sc = (SimpleClass)bf.Deserialize(fs);
}

Console.WriteLine("{0} {1} {2}", sc.Num, sc.Str, sc.AddedNum);
123 abc 0