BinaryFormatterクラス

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

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

セキュリティ上の問題から、.NET5以降ではBinaryFormatterクラスは非推奨となり、BinaryFormatterクラスが提供するメソッドは標準では使用できないようになっています。
設定の変更により使用することは可能ですが、.NET9以降は実装自体が削除されています。

古いバージョンの.NETを使用する方法以外ではもはや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