Skip to content

Day 60: std.serialization模块:序列化基础

序列化是将数据结构(如结构体)转换为可以存储(例如写入文件)或传输(例如通过网络发送)的格式(如字节流)的过程。反序列化则是相反的过程,即从字节流中重建原始数据结构。

这对于保存应用程序状态、配置文件或在不同系统之间进行通信至关重要。Zig的std.serialization模块(以及更常用的std.json和std.binary等)提供了执行这些任务的工具。本节将重点介绍二进制序列化的基础。

要将数据序列化为二进制格式,我们通常使用一个Writer。std.io.Writer是一个接口,可以代表文件、内存缓冲区等任何可以写入字节的地方。std.binary.Writer则包装了一个std.io.Writer,并提供了写入特定二进制数据类型(如整数、浮点数)的方法。

  • writeInt: 写入一个整数,可以指定端序(Endianness)。
  • writeFloat: 写入一个浮点数。
  • writeStruct: 使用@sizeOf和@ptrCast直接将一个packed struct的内存内容写入流中。这是最快但最不灵活的方法。
const std = @import("std");
const Config = packed struct {
version: u8,
is_fullscreen: bool,
volume: f32,
};
pub fn main() !void {
var file = try std.fs.cwd().createFile("config.bin", .{});
defer file.close();
const writer = file.writer();
const my_config = Config{
.version = 2,
.is_fullscreen = true,
.volume = 0.75,
};
// 直接写入整个结构体的字节
try writer.writeAll(@bytesOf(my_config));
}

与Writer相对应,std.binary.Reader包装了一个std.io.Reader,并提供了读取二进制数据的方法。

  • readInt: 读取一个整数。
  • readFloat: 读取一个浮点数。
  • readStruct: 直接将字节读入一个packed struct的内存中。
// ... Config 定义 ...
pub fn main() !void {
var file = try std.fs.cwd().openFile("config.bin", .{});
defer file.close();
const reader = file.reader();
var my_config: Config = undefined;
// 将文件内容直接读入结构体
try reader.readExactly(@bytesOf(&my_config));
std.debug.print("Config Version: {d}\n", .{my_config.version});
std.debug.print("Is Fullscreen: {}\n", .{my_config.is_fullscreen});
std.debug.print("Volume: {d}\n", .{my_config.volume});
}

当结构体包含指针或切片等变长字段时,直接内存复制就行不通了。你需要定义一个明确的序列化和反序列化协议。

const std = @import("std");
const User = struct {
id: u64,
name: []const u8,
pub fn serialize(self: User, writer: anytype) !void {
try writer.writeInt(u64, self.id, .big);
try writer.writeInt(u32, @intCast(self.name.len), .big);
try writer.writeAll(self.name);
}
pub fn deserialize(allocator: std.mem.Allocator, reader: anytype) !User {
const id = try reader.readInt(u64, .big);
const name_len = try reader.readInt(u32, .big);
const name = try allocator.alloc(u8, name_len);
try reader.readExactly(name);
return User{ .id = id, .name = name };
}
};

5. 实践练习:保存和加载游戏状态

Section titled “5. 实践练习:保存和加载游戏状态”

创建一个GameState结构体,包含玩家分数(u32)、生命值(u8)和玩家名称([]const u8)。实现serialize和deserialize方法,将游戏状态保存到文件gamestate.sav中,并能从中加载回来。

  • 版本兼容性 直接的二进制序列化非常脆弱。如果你在结构体中添加、删除或重新排序字段,旧的序列化数据将无法再被正确读取。为了解决这个问题,你需要在序列化数据中包含一个版本号,并编写能够处理不同版本数据的代码。

  • 变长字段 如示例所示,对于指针、切片或任何动态大小的字段,你不能只复制内存。你必须先序列化其长度,然后再序列化其内容。

  • 端序(Endianness) 当在不同架构的机器之间交换二进制数据时(例如x86和ARM),字节顺序(端序)就变得很重要。x86是小端序(Little Endian),而许多网络协议和ARM处理器使用大端序(Big Endian)。在序列化时明确指定端序(如.big或.little)是一种良好的实践。

二进制序列化提供了最高的性能和最小的体积,但代价是牺牲了灵活性和人类可读性。对于配置文件或需要跨语言、跨平台轻松交换数据的场景,使用std.json进行JSON序列化通常是更好的选择。

Zig的序列化工具体现了其核心哲学:它为你提供了最高性能的底层工具(如直接内存复制),同时也允许你通过自定义serialize/deserialize方法构建更健壮、更灵活的高级抽象。理解这两者之间的权衡是设计高效、可维护系统的关键。