C# [シリアライズ]

シリアライズとは作成したインスタンスをバイナリデータとしてディスクに保存したり復元できたりする機能です。
更新日 2016-02-13

シリアライズについて

通常のファイル入出力の場合、インスタンスからバイナリ・テキストデータへの変換の実装が必要ですが、シリアライズ(Serialize)はこの辺を自動でやってくれます。 デ・シリアライズ(Deserialize) とはバイナリ・テキストデータからインスタンスを復元する機能です。
どんな時に使うのかというと、オリジナルのデータとは別に加工処理途中で一時的に保存して、アプリケーションの再起動後に速やかに復元、続きの処理を行うといった例が あります。メモリを使いすぎる場合、一時的にストレージに退避させたりといった目的にも使えます。

一般的なシリアライズ

ファイルに保存する

Save() で保存(シリアライズ)、getInstance() で復元(デシリアライズ)を行います。
[Serialize]
public class MyCache
{
	public const String SERIALIZE_FILE_PATH = "e:\\MyCache.bin";

	public int m_nValue;

	public static MyCache getInstance()
	{
		MyCache refMyCache = null;
		{
			if (File.Exists(SERIALIZE_FILE_PATH))
			{
				IFormatter formatter = null;
				Stream stream = null;

				try
				{
					stream = new FileStream(SERIALIZE_FILE_PATH,
								 	FileMode.Open, FileAccess.Read, FileShare.Read);

					formatter = new BinaryFormatter();
					refMyCache = (MyCache)formatter.Deserialize(stream);
				}
				catch (Exception)
				{
				}
				finally
				{
					if (stream != null)
					{
						stream.Close();
						stream = null;
					}
				}
			}
		}

		// New instance and initialize value.
		if (refMyCache == null)
		{
			refMyCache = new MyCache();
			refMyCache.m_nValue = 3;
		}

		return refMyCache;
	}

	public void Save()
	{
		IFormatter formatter = null;
		Stream stream = null;
	
		try
		{
			formatter = new BinaryFormatter();
			stream = new FileStream(SERIALIZE_FILE_PATH,
							 FileMode.Create, FileAccess.Write, FileShare.None);
			formatter.Serialize(stream, this);
		}
		catch (Exception ex)
		{
		}
		finally
		{
			if (stream != null)
			{
				stream.Close();
				stream = null;
            }
        }
	}
}
実際にインスタンスを作ってみます。
public static MyCache g_MyCache = MyCache.getInstance();

void main()
{ 
	// refMyCache.m_nValue の値をデバッグ出力
	Debug.WriteLine("g_MyCache.m_nValue = " + g_MyCache.m_nValue); 
               
	// 新しい値を設定
	g_MyCache.m_nValue += 3;

    // ここでシリアライズ
	g_MyCache.Save();
}
インスタンスの生成にはgetInstance() を使います。最初のgetInstance() ではシリアライズされたデータが無いため、内部ではnew による生成が 行われます。したがってg_MyCache.m_nValue の値は初期値の 3 です。
次回の実行時にはシリアライズデータから復元されるため、g_MyCache.m_nValue の値は 6 になるはずです。
このクラスに作成するプロパティは全てシリアライズの対象になります。アプリケーション毎に一つあると大変便利でしょう。

コンストラクタや宣言では初期化はダメ

シリアライズではオブジェクト固有の初期値(null や0)が省略される事があります。コンストラクタや宣言でその値以外の初期化を設定している場合、 デシリアライズ時に正しく反映されない事になります。非常に面倒なトラブルになります。宣言時に初期化するのはC++ 時代からの定石でしたが、 これを機に改めます。

Protocol Buffers を使う

Google から提供されているシリアライズの実装です。.Net 対応版もあり、非常に高速かつコンパクトな設計です。

protobuf-net r480 をダウンロードする

Google の protobuf-net からprotobuf-net r668 をダウンロードします。利用するプロジェクトの参照設定に、解凍したnet30 フォルダの中のprotobuf-net.dll を追加します。net30 は.Net3.0 以降に対応しています。

属性を変える・付ける

class にはProtoContract 属性を、メンバ変数にはProtoMember 属性をつけます。ProtoMember 属性には1 から続く数値をつけます。 const メンバ変数や関数はシリアライズには関係しないので付ける必要はありません。
[ProtoContract]
public class SerializeTestClass
{
	const String SERIALIZE_FILE_PATH = "e:\\SerializeTestClass.bin";

	[ProtoMember(1)]
	public int m_nValue = 3;

	public static SerializeTestClass getInstance()
	{
		SerializeTestClass refSerializeTestClass = null;
		{
			if (File.Exists(SERIALIZE_FILE_PATH))
			{
				Stream stream = null;

				try
				{
					stream = new FileStream(SERIALIZE_FILE_PATH,
									 FileMode.Open, FileAccess.Read, FileShare.Read);

					refSerializeTestClass = 
							Serializer.Deserialize<SerializeTestClass>(stream);
				}
				catch (Exception)
				{
				}
				finally
				{
					if (stream != null)
					{
						stream.Close();
						stream = null;
					}
				}
			}
		}

		if (refSerializeTestClass == null)
		{
			refSerializeTestClass = new SerializeTestClass();
		}
		return refSerializeTestClass;
	}

	public void Save()
	{
		Stream stream = null;
	
		try
		{
			stream = new FileStream(SERIALIZE_FILE_PATH,
							 FileMode.Create, FileAccess.Write, FileShare.None);
			Serializer.Serialize(stream, this);
		}
		catch (Exception ex)
		{
		}
		finally
		{
			if (stream != null)
			{
				stream.Close();
				stream = null;
            }
        }
	}
}
BinaryFormatter を使っていないのでよりシンプルです。

使用感

メモリ上の膨大なList データ(400万件、6GB 程度)のシリアライズはBinaryFormatter では遅すぎて使い物になりません。proto-buf はgoogle 製という事ですが、 バツグンの速さです。デシリアライズ(復元)にはファイル入力よりもCPU 負荷のウェイトが大きくかかります。したがって複数に分割してシリアライズしておき、 デシリアライズには複数のスレッドで行うと良いでしょう。ちなみにCore-i7 3770 32GB メモリ環境下でHDD からの復元に16秒前後です。ファイルは40MB が100個ほどで サイズもそこそこ抑えられています。

ProtoMember 属性のインデックス数値を自動補正する

ProtoMember の数値は通し番号でないといけないので(たぶんキー?)、ビルド前イベントで自動的に補正する処理を行うと便利です。でもそんな機能は無いので 簡単なコマンドラインプログラムを作ってファイルをノーマライズする事にします。

[VisualStudio2012 C# プロジェクト一式]CorrectProtoMember.zip
[VisualStudio2012 C# main() ソース]program.cs
// ビルド前に実行するコマンドライン
d:\CorrectProtoMember $(ProjectDir)SeekPos.cs
引数1 に読み込みファイルパス、引数2 に保存ファイルパス(省略可)を合わせて呼び出すと、ProtoMemer の数値を順に補正します。単純なマッチングで行なっているので、 うまく動かない時はRegex.match 周りを確認してね。