Ray Koopa 著
Conmajia 譯
2019 年 1 月 17 日已獲作者本人授權.
簡介
本文討論如何擴充套件 .NET 原生的 BinaryReader
和 BinaryWriter
類以支援更多新的常用的特性. 這些 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 自帶的 BinaryReader
和 BinaryWriter
類. 普通資料還好,如果是某些甲方爸爸的特殊格式資料,就有點力不從心了. 處理的資料格式越複雜,我越覺得 .NET 類裡還是少了一些常用又實用的東西,尤其是:
- 處理以不同於本機位元組順序儲存的資料
- 處理非 .NET格式的字串,比如以 0 結尾的字串
- 讀寫重複的資料型別而不用一遍又一遍地迴圈
- 臨時用不同編碼的字串讀寫資料流
- 檔案內高階定位,例如臨時定位新位置
一開始我只是寫點擴充套件方法,作為原生 BinaryReader
、BinaryWriter
的外掛. 但是使用中我發現,這還是不足以實現以不同於本機的位元組順序讀取資料這類問題. 於是我乾脆在原生類的基礎上建立了兩個新的派生類,我給它們起名叫 BinaryDataReader
和 BinaryDataWriter
. 接下來看看我是如何實現上面列出的各個特性的吧.
實現和用法
位元組順序
.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();
}
}
我分別重寫了 BinaryDataReader
和 BinaryDataWriter
的所有 Read
、Write
. 重寫的方法由 _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 裡,需要手動處理. 好在微軟的百科上有大神寫好了如何轉換的技術資料可以直接使用.
用法
BinaryDataReader
、BinaryDataWriter
預設用的本機位元組順序. 要改變位元組順序,可以修改它們的 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 結尾,也叫空結尾. 比如 C/C++ 裡用到的字串基本都是以