[C#]クラスのインスタンスを byte[] 配列にする方法3選

クラスのインスタンス(中身全て)をセーブ・ロードしたり、ネットを介したデータのやりとりをしたい……そんな時、インスタンスを転送用に byte 配列にする方法をいくつか紹介します。

なお、素直に Json でよくない? というのもごもっとも。今回はインスタンス値の CRC を作ったり、個人的な要件があって byte[] の変換を調査しました。

byte[] に変換するサンプルインスタンスは次の通りです。

    public class Sample
    {
        public const int    SLOT_AUTOSAVE = 0;

        public bool         Used;
        public string       DisplayNo;
        public int          SlotNo;
        public DateTime     Time;
        public string       Description;
        public int          Crc;
    }

    Sample sample = new Sample()
    {
        Used        = true,
        DisplayNo   = "99",
        SlotNo      = Sample.SLOT_AUTOSAVE,
        Time        = DateTime.Now,
        Description = "テストデータです",
    };

1.BinaryFormatter

名前からして思い浮かびやすいのは、まずこれでしょうか。
クラスのインスタンスから byte[] の変換、byte[] からインスタンスに変換、それぞれのコードを示します。

注意点として、変換するクラスは [Serializable] という属性をつけなければならないことです。
そのため、クラスの中にクラス変数を宣言している(そちらはライブラリのため、気軽に自分でコード追加できない)といった場合は難しいかもしれません。

byte[] のサイズは 217 バイトでした。(マシン環境によって異なるかもしれません)

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

[Serializable]
public class Sample
{
    public const int    SLOT_AUTOSAVE = 0;

    public bool         Used;
    public string       DisplayNo;
    public int          SlotNo;
    public DateTime     Time;
    public string       Description;
    public int          Crc;
}

Sample sample = new Sample()
{
    Used        = true,
    DisplayNo   = "99",
    SlotNo      = Sample.SLOT_AUTOSAVE,
    Time        = DateTime.Now,
    Description = "テストデータです",
};

void binaryFormatter()
{
    // instance → byte[]
    byte[] bytes;
    using (MemoryStream ms = new MemoryStream())
    {
        BinaryFormatter writer = new BinaryFormatter();
        writer.Serialize(ms, sample);

        bytes = ms.ToArray();
    }

    // byte[] → instance
    Sample output;

    using (MemoryStream ms = new MemoryStream(bytes))
    {
        BinaryFormatter reader = new BinaryFormatter();

        output = (Sample)reader.Deserialize(ms);
    }
}

2.Marshal

4 行目、StructLayout の属性が必要なことに注意してください。
コードも少し多めです。この中で、Free 系は絶対忘れないようにしてください(メモリーリークします)。

また、今回のサンプルクラスは残念ながら以下のエラーが出てしまいます。

MarshalDirectiveException: Type System.DateTime which is passed to unmanaged code must have a StructLayout attribute.

System.DateTime はマーシャリングをサポートしていないようです。
これを解決する方法もなくはないですが、楽をして相互変換をしたいのに手間が増えるのは御免ですよね。

ちなみに、DateTime がなければ実行は可能です。
byte[] のサイズは 28 バイトと結構小さめにできますが、そもそも DateTime は含められませんし、普段使いするには敷居が高い印象です。

using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential, Pack = 4, CharSet = CharSet.Ansi)]
public class Sample
{
    public const int    SLOT_AUTOSAVE = 0;

    public bool         Used;
    public string       DisplayNo;
    public int          SlotNo;
    public DateTime     Time;          //← これがあるとエラー
    public string       Description;
    public int          Crc;
}

Sample sample = new Sample()
{
    Used        = true,
    DisplayNo   = "99",
    SlotNo      = Sample.SLOT_AUTOSAVE,
    Time        = DateTime.Now,        //← これがあるとエラー
    Description = "テストデータです",
};

void marshal()
{
    // instance → byte[]
    var size  = Marshal.SizeOf(sample);
    var bytes = new byte[size];
    var ptr   = Marshal.AllocHGlobal(size);
    Marshal.StructureToPtr(sample, ptr, false);
    Marshal.Copy(ptr, bytes, 0, size);
    Marshal.FreeHGlobal(ptr);

    // byte[] → instance
    var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
    Sample output = (Sample)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(Sample));
    handle.Free();
}

3.Json を経由

コードはシンプル。byte[] のサイズも 133 バイトと、BinaryFormatter よりは小さめです。
UTF8GetEncoding("Shift-JIS") にするともっと小さくなるものの、日本語・英語以外の言語対応を考えるなら UTF8 がよさそうですね。
また、NewtonSoft.Json を使う限りにおいて、クラスに属性をつける必要もありません。

扱いやすさでは3つの中で間違いなくトップ。
ただ、なんとなくイメージ的に遅いんじゃないか……? と懸念を感じたので、3種それぞれの実行速度を比べてみることにします。

using System.Text;
using Newtonsoft.Json;

public class Sample
{
    public const int    SLOT_AUTOSAVE = 0;

    public bool         Used;
    public string       DisplayNo;
    public int          SlotNo;
    public DateTime     Time;
    public string       Description;
    public int          Crc;
}

Sample sample = new Sample()
{
    Used        = true,
    DisplayNo   = "99",
    SlotNo      = Sample.SLOT_AUTOSAVE,
    Time        = DateTime.Now,
    Description = "テストデータです",
};

void toJson()
{
    // instance → byte[]
    byte[] bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(sample));
    
    // byte[] → instance
    Sample output = JsonConvert.DeserializeObject<Sample>(Encoding.UTF8.GetString(bytes));
}

3種類の速度を比較する

それぞれを1万回繰り返した速度比較を示します。
なお、Marshal は DateTime が使えないため、テストでは DateTime を除いたクラスで行いました。

速度だけでみると圧倒的に Marshal が速いようです。
また、一番遅いと予想していた ToJson が2番手だったのが意外でした。
BinaryFormatter はシリアライズより、デシリアライズが重そうです。シリアライズだけなら 278ms でした(ToJson は 220ms)。

以上から、普段使いには ToJson、速度を求めるのであれば Marshal(ただしマーシャリング出来ないクラスもある) がよさそうです。

BinaryFormatter は利点がないので、今後は控えようと思います☺

返信を残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA