从 cmets 看来,这是一个相当直接的序列化问题。我们可以通过几种方式做到这一点,而不必担心数组操作,除非你真的想这样做。
我将使用以下类:
class Map
{
public int Height { get; set; }
public int Width { get; set; }
public string Name { get; set; }
public Tile[] Tiles { get; set; }
}
class Tile
{
public byte Terrain { get; set; }
public byte Elevation { get; set; }
public byte Illumination { get; set; }
public int ActorID { get; set; }
}
现在,使用快速制造方法和一些(可重复的)随机数,我创建了一个地图,在所有图块中都有一些可怕的随机数据。将 Width 和 Height 都设置为 512,我们有 262,144 个充满随机垃圾的图块。
现在我们可以进行一些序列化...
内置二进制序列化
在为Map 和Title 类添加一些[Serializable] 属性后,我们可以使用内置的.NET 方法对其进行序列化。 Map 类现在看起来像这样:
[Serializable]
class Map
{
// ...all the same members here...
}
序列化的代码如下:
// Usings:
// using System.Runtime.Serialization;
// using System.Runtime.Serialization.Formatters.Binary;
var map = GenerateMap();
IFormatter formatter = new BinaryFormatter();
byte[] serbytes;
using (var ms = new MemoryStream())
{
fmt.Serialize(ms, map);
serbytes = ms.ToArray();
}
运行它会得到一个 5.5MB 大小的字节数组,这并不是非常好。我们可以压缩它,但由于我有随机数据,它不能很好地压缩——Brotli 压缩为 1.5MB,GZip 压缩为 2.1MB。实际数据可能会更好。这是带有压缩的序列化部分:
// Usings:
// using System.IO.Compression;
// using System.Runtime.Serialization;
// using System.Runtime.Serialization.Formatters.Binary;
var map = GenerateMap();
IFormatter formatter = new BinaryFormatter();
byte[] serbytes;
using (var ms = new MemoryStream)
{
using (var cmp = new GZipStream(ms, CompressionLevel.Optimal, true))
{
fmt.Serialize(ms, map);
}
serbytes = ms.ToArray();
}
从字节数组解压反序列化:
Map deser;
using (var ms = new MemoryStream(serbytes))
using (var dec = new GZipStream(ms, CompressionMode.Decompress, true))
{
deser = (Map)formatter.Deserialize(dec);
}
仅此而已。
使用 ProtoBuf-net
内置序列化程序不一定是一般情况下使用的最佳选择。另一种选择是使用protobuf-net,它的属性更重一些。您需要为要序列化的类和所有成员添加一个属性,如下所示:
[ProtoContract(SkipConstructor = true)]
public class Tile
{
[ProtoMember(1)]
public byte Terrain { get; set; }
[ProtoMember(2)]
public byte Elevation { get; set; }
[ProtoMember(3)]
public byte Illumination { get; set; }
[ProtoMember(4)]
public byte Foo { get; set; }
[ProtoMember(5)]
public int ActorID { get; set; }
}
[ProtoContract(SkipConstructor = true)]
public class Map
{
[ProtoMember(1)]
public int Height { get; set; }
[ProtoMember(2)]
public int Width { get; set; }
[ProtoMember(3)]
public string Name { get; set; }
[ProtoMember(4)]
public Tile[] Tiles { get; set; }
}
现在我们可以像使用内部序列化器一样做同样的事情,在序列化和反序列化方面只有一行不同:
// Usings:
// using ProtoBuf;
var map = GenerateMap();
byte[] serbytes;
using (var ms = new MemoryStream())
{
using (var cmp = new GZipStream(ms, CompressionLevel.Optimal, true))
{
Serializer.Serialize(ms, map);
}
serbytes = ms.ToArray();
}
Map deser;
using (var ms = new MemoryStream(serbyte))
using (var dec = new GZipStream(ms, CompressionMode.Decompress, true))
{
deser = Serializer.Deserialize<Map>(ms);
}
非常简单,根据我的测试数据,它使用 GZip 压缩得更好 - 1.4MB 与 2.1MB 对于本机序列化程序。
或者自己动手...
如果你真的,真的希望你可以使用BinaryWriter 和BinaryReader 在每个类上手动创建Serialize 和Deserialize 方法。优点是文件格式完全在您的控制之下。缺点是你必须自己做所有事情。
例如:
class Map
{
public int Height { get; set; }
public int Width { get; set; }
public string Name { get; set; }
public Tile[] Tiles { get; set; }
public void Serialize(Stream stream)
{
using (var writer = new BinaryWriter(stream, Encoding.UTF8, true))
{
writer.Write(Height);
writer.Write(Width);
writer.Write(Name);
foreach (var tile in Tiles)
{
tile.Serialize(stream);
}
}
}
public static Map Deserialize(Stream stream)
{
var res = new Map();
using (var reader = new BinaryReader(stream, Encoding.UTF8, true))
{
res.Height = reader.ReadInt32();
res.Width = reader.ReadInt32();
res.Name = reader.ReadString();
int tileCount = res.Height * res.Width;
res.Tiles = new Tile[tileCount];
for (int i = 0; i < tileCount; i++)
{
res.Tiles[i] = Tile.Deserialize(stream);
}
}
}
}
使用我一直在使用的测试数据,生成的 GZip 压缩二进制数据略低于 1.1MB,但无论如何这是高熵输入。
结论
二进制序列化可以是简单的也可以是困难的,这取决于您尝试序列化的是什么。压缩只是在开始将序列化数据填充到其中之前在流组合中添加另一个阶段的问题。如果您不想这样做,则不需要您弄乱数组……除了您自己数据中的数组。
注意:在上述所有示例中,您当然可以将 MemoryStream 替换为 FileStream 并直接处理磁盘上的文件。