加強版二進位制讀寫器

Conmajia發表於2019-01-19

Ray Koopa 著
Conmajia 譯
2019 年 1 月 17 日

已獲作者本人授權.

簡介

本文討論如何擴充套件 .NET 原生的 BinaryReaderBinaryWriter 類以支援更多新的常用的特性. 這些 API 可以通過 NuGet > Syroot.IO.BinaryData 安裝:

PM> Install-Package Syroot.IO.BinaryData -Version 4.0.4
> dotnet add package Syroot.IO.BinaryData --version 4.0.4
> paket add Syroot.IO.BinaryData --version 4.0.4

GitHub 上的百科主要關注實現方面,不過也提到了它的演化過程和編寫實現時需要注意的東西.

背景

每次我要用到二進位制資料載入、解析、儲存這類功能的時候,我都用的 .NET 自帶的 BinaryReaderBinaryWriter 類. 普通資料還好,如果是某些甲方爸爸的特殊格式資料,就有點力不從心了. 處理的資料格式越複雜,我越覺得 .NET 類裡還是少了一些常用又實用的東西,尤其是:

  • 處理以不同於本機位元組順序儲存的資料
  • 處理非 .NET格式的字串,比如以 0 結尾的字串
  • 讀寫重複的資料型別而不用一遍又一遍地迴圈
  • 臨時用不同編碼的字串讀寫資料流
  • 檔案內高階定位,例如臨時定位新位置

本機指的是執行 .NET 的計算機. 位元組順序指的是資料按位元位從低到高從高到低儲存,也叫小端格式(little-endian)或大端格式(big-endian).

一開始我只是寫點擴充套件方法,作為原生 BinaryReaderBinaryWriter 的外掛. 但是使用中我發現,這還是不足以實現以不同於本機的位元組順序讀取資料這類問題. 於是我乾脆在原生類的基礎上建立了兩個新的派生類,我給它們起名叫 BinaryDataReaderBinaryDataWriter. 接下來看看我是如何實現上面列出的各個特性的吧.

實現和用法

位元組順序

.NET 本身沒有規定資料的位元組順序,直接用的本機順序. 要支援跟本機不同的位元組順序,要對原生讀寫類做一些改動. 首先檢測當前系統用到的位元組順序,這很簡單,有現成的 System.BitConverter.IsLittleEndian 欄位可用:

ByteOrder systemByteOrder = BitConverter.IsLittleEndian ? ByteOrder.LittleEndian : ByteOrder.BigEndian;

這裡我引入了一個列舉型別 ByteOrder 區分大小端位元組順序:

public enum ByteOrder : ushort
{
    BigEndian = 0xFEFF,
    LittleEndian = 0xFFFE
}

ByteOrder 屬性則用來指定讀寫類的位元組順序:

public ByteOrder ByteOrder
{
    get
    {
        return _byteOrder;
    }
    set
    {
        _byteOrder = value;
        _needsReversion = _byteOrder != ByteOrder.GetSystemByteOrder();
    }
}

我分別重寫了 BinaryDataReaderBinaryDataWriter 的所有 ReadWrite. 重寫的方法由 _needsReversion 決定要不要改變位元組順序(反向輸出資料):

public override Int32 ReadInt32()
{
    if (_needsReversion)
    {
        byte[] bytes = base.ReadBytes(sizeof(int));
        Array.Reverse(bytes);
        return BitConverter.ToInt32(bytes, 0);
    }
    else
    {
        return base.ReadInt32();
    }
}

BitConverter.ToXXX() 這系列方法能輕鬆實現位元組陣列和多位元組資料的互相轉換. 不過 Decimal 型別有點怪,它的轉換沒有內建在 .NET 裡,需要手動處理. 好在微軟的百科上有大神寫好了如何轉換的技術資料可以直接使用.

用法

BinaryDataReaderBinaryDataWriter 預設用的本機位元組順序. 要改變位元組順序,可以修改它們的 ByteOrder 屬性. 任何時候都可以修改這個屬性,讀/寫語句之間也可以:

using (MemoryStream stream = new MemoryStream(...))
using (BinaryDataReader reader = new BinaryDataReader(stream))
{
    int intInSystemOrder = reader.ReadInt32();

    reader.ByteOrder = ByteOrder.BigEndian;
    int intInBigEndian = reader.ReadInt32();

    reader.ByteOrder = ByteOrder.LittleEndian;
    int intInLittleEndian = reader.ReadInt32();
}

重複的資料型別

處理 3D 格式檔案的時候,經常要讀入很多變換矩陣,一串 16 個浮點數那種,一個接一個的讀. 我可以寫個專門的 ReadMatrix,沒毛病. 不過呢,既然要寫,就寫一個通用一點的,就像 ReadSingles(T[]) 這種,傳入要讀的數量,for 之類的迴圈它在內部處理好,然後返回讀出來的陣列.

public Int32[] ReadInt32s(int count)
{
    return ReadMultiple(count, ReadInt32);
}

private T[] ReadMultiple<T>(int count, Func<T> readFunc)
{
    T[] values = new T[count];
    for (int i = 0; i < values.Length; i++)
    {
        values[i] = readFunc.Invoke();
    }
    return values;
}

用法

呼叫對應資料型別的 Read,傳入要讀取的數量,得到的返回值就是讀取到的資料陣列. Write 則是把陣列寫到資料流.

using (MemoryStream stream = new MemoryStream(...))
using (BinaryDataReader reader = new BinaryDataReader(stream))
{
    int[] fortyFatInts = reader.ReadInt32s(40);
}

不同的字串格式

字串可以儲存為不同的二進位制格式. 預設的讀寫器類只支援帶無符號整數字首的字串. 工作中我處理的多數字符串都是 0 結尾Zero-Terminated,也叫空結尾Null-Terminated. 比如 C/C++ 裡用到的字串基本都是以

相關文章