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++ 里用到的字符串基本都是以