“文字地牢”小游戏通关 Rust 入门-存档与读档
·
存档与读档——文件 IO 与序列化
上文我们大致了解了"文字地牢"小游戏的错误处理与输入校验,那么我们本章的目标是掌握 serde 序列化;理解文本与二进制格式的权衡;处理版本兼容与健壮性。
基本概念
文件 I/O
Rust标准库提供了丰富的文件I/O操作功能,包括读取、写入、创建和删除文件等操作。
文件I/O操作的类型:
- 同步I/O:阻塞当前线程直到操作完成
- 异步I/O:非阻塞操作,适用于高并发场景
- 缓冲I/O:通过缓冲区减少系统调用次数
- 直接I/O:绕过操作系统缓存,直接与存储设备交互
文件操作模式:
- 只读:
File::open() - 写入:
File::create() - 追加:
OpenOptions::append() - 读写:
OpenOptions::read().write()
序列化与反序列化
序列化是将内存中的数据结构转换为可存储或传输的格式,反序列化则是将存储的数据转换回内存中的数据结构。
序列化格式的分类:
- 文本格式:JSON、XML、TOML、YAML等,人类可读
- 二进制格式:Bincode、Protocol Buffers、MessagePack等,紧凑高效
- 自描述格式:包含类型信息,无需外部模式定义
- 非自描述格式:需要外部模式定义才能正确解析
serde crate
serde是Rust生态系统中非常流行的序列化框架,支持多种数据格式,包括JSON、TOML、RON等。
serde的核心组件:
- Serialize trait:定义如何将类型序列化为数据格式
- Deserialize trait:定义如何从数据格式反序列化为类型
- Serializer/Deserializer:特定格式的实现
- derive宏:自动生成Serialize/Deserialize实现
如何使用
文件 I/O 操作详解
use std::fs::File;
use std::io::prelude::*;
use std::io::BufReader;
use std::io::BufWriter;
// 基本文件读取
fn read_file(path: &str) -> std::io::Result<String> {
std::fs::read_to_string(path)
}
// 基本文件写入
fn write_file(path: &str, content: &str) -> std::io::Result<()> {
std::fs::write(path, content)
}
// 使用File和缓冲区进行更精细的控制
fn read_file_buffered(path: &str) -> std::io::Result<String> {
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let mut contents = String::new();
reader.read_to_string(&mut contents)?;
Ok(contents)
}
// 流式处理大文件
fn process_large_file(path: &str) -> std::io::Result<()> {
let file = File::open(path)?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
// 处理每一行
println!("{}", line);
}
Ok(())
}
serde 序列化详解
use serde::{Serialize, Deserialize};
// 基本序列化
#[derive(Serialize, Deserialize, Debug)]
struct Person {
name: String,
age: u32,
email: Option<String>,
}
// 使用自定义序列化
#[derive(Serialize, Deserialize)]
struct Temperature {
#[serde(rename = "temp_celsius")]
celsius: f64,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
}
// 扁平化嵌套结构
#[derive(Serialize, Deserialize)]
struct Address {
street: String,
city: String,
}
#[derive(Serialize, Deserialize)]
struct User {
name: String,
#[serde(flatten)]
address: Address,
}
// 使用默认值处理缺失字段
#[derive(Serialize, Deserialize)]
struct Config {
#[serde(default = "default_timeout")]
timeout: u64,
#[serde(default)]
debug: bool,
}
fn default_timeout() -> u64 {
30
}
不同格式的使用
use serde::{Serialize, Deserialize};
use serde_json;
use serde_yaml;
use toml;
#[derive(Serialize, Deserialize, Debug)]
struct GameConfig {
difficulty: String,
volume: f64,
fullscreen: bool,
}
// JSON 格式
fn json_example() -> Result<(), Box<dyn std::error::Error>> {
let config = GameConfig {
difficulty: "hard".to_string(),
volume: 0.8,
fullscreen: true,
};
// 序列化
let json = serde_json::to_string_pretty(&config)?;
println!("JSON: {}", json);
// 反序列化
let parsed: GameConfig = serde_json::from_str(&json)?;
println!("Parsed: {:?}", parsed);
Ok(())
}
// TOML 格式
fn toml_example() -> Result<(), Box<dyn std::error::Error>> {
let config = GameConfig {
difficulty: "medium".to_string(),
volume: 0.7,
fullscreen: false,
};
// 序列化
let toml_string = toml::to_string(&config)?;
println!("TOML: {}", toml_string);
// 反序列化
let parsed: GameConfig = toml::from_str(&toml_string)?;
println!("Parsed: {:?}", parsed);
Ok(())
}
序列化与派生
- 在模型上派生
Serialize/Deserialize,即可与serde_json协作。
文件 IO 基础
- 写文件:
std::fs::write(path, content);读文件:std::fs::read_to_string(path)。 - 路径与权限:注意相对/绝对路径、工作目录与权限错误的处理。
版本兼容
- 为存档结构加入
version字段;读取时根据版本做兼容分支。
注意事项
- 处理文件I/O操作时,始终考虑错误情况并妥善处理。
- 在序列化和反序列化时,注意数据的兼容性和版本控制。
- 对于敏感数据,要考虑安全性,避免反序列化不受信任的数据。
- 在处理大文件时,考虑内存使用情况,避免一次性加载整个文件到内存。
- 注意文件路径的处理,特别是在跨平台应用中。
- 使用适当的缓冲策略来提高I/O性能。
- 考虑原子操作来确保数据一致性。
- 对于网络传输的数据,验证数据完整性和来源。
本项目中的使用
在我们的项目中,我们使用serde和serde_json来实现游戏状态的保存和加载功能:
pub fn save_state(path: &str, state: &GameState) -> Result<()> {
let s = serde_json::to_string_pretty(state)?;
std::fs::write(path, s)?;
Ok(())
}
pub fn load_state(path: &str) -> Result<GameState> {
let s = std::fs::read_to_string(path)?;
let state: GameState = serde_json::from_str(&s)?;
Ok(state)
}
我们的数据模型通过派生Serialize和Deserialize trait来支持序列化:
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Player {
pub position: Position,
pub hp: i32,
}
改进建议
在地牢探险游戏中,我们可以对存档系统进行以下改进:
- 版本控制:
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SaveGame {
version: u32,
game_state: GameState,
timestamp: u64,
}
impl SaveGame {
pub fn new(state: GameState) -> Self {
Self {
version: 1,
game_state: state,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
}
}
pub fn load(path: &str) -> Result<GameState> {
let content = std::fs::read_to_string(path)?;
let save_game: SaveGame = serde_json::from_str(&content)?;
match save_game.version {
1 => Ok(save_game.game_state),
_ => Err(AppError::InvalidSaveFormat("Unsupported version".to_string())),
}
}
}
- 原子写入:
pub fn save_state_atomic(path: &str, state: &GameState) -> Result<()> {
let temp_path = format!("{}.tmp", path);
let backup_path = format!("{}.bak", path);
// 序列化到临时文件
let s = serde_json::to_string_pretty(state)?;
std::fs::write(&temp_path, s)?;
// 如果原文件存在,创建备份
if std::path::Path::new(path).exists() {
std::fs::rename(path, &backup_path)?;
}
// 原子重命名
std::fs::rename(&temp_path, path)?;
// 删除备份(可选)
if std::path::Path::new(&backup_path).exists() {
let _ = std::fs::remove_file(&backup_path);
}
Ok(())
}
- 多格式支持:
pub enum SaveFormat {
Json,
Ron,
Toml,
}
pub fn save_state_with_format(
path: &str,
state: &GameState,
format: SaveFormat
) -> Result<()> {
let serialized = match format {
SaveFormat::Json => serde_json::to_string_pretty(state)?,
SaveFormat::Ron => ron::ser::to_string_pretty(state, Default::default())?,
SaveFormat::Toml => toml::to_string(state)?,
};
std::fs::write(path, serialized)?;
Ok(())
}
高级序列化技术
自定义序列化
use serde::{Serialize, Deserialize, Serializer, Deserializer};
#[derive(Debug)]
struct IpAddr {
octets: [u8; 4],
}
impl Serialize for IpAddr {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = format!("{}.{}.{}.{}",
self.octets[0], self.octets[1], self.octets[2], self.octets[3]);
serializer.serialize_str(&s)
}
}
impl<'de> Deserialize<'de> for IpAddr {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let parts: Vec<u8> = s.split('.')
.map(|part| part.parse().map_err(serde::de::Error::custom))
.collect::<Result<Vec<u8>, _>>()?;
if parts.len() != 4 {
return Err(serde::de::Error::custom("Invalid IP address format"));
}
Ok(IpAddr {
octets: [parts[0], parts[1], parts[2], parts[3]],
})
}
}
条件序列化
#[derive(Serialize, Deserialize)]
struct UserPreferences {
#[serde(default)]
theme: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
avatar: Option<String>,
#[serde(default = "default_volume", skip_serializing_if = "is_default_volume")]
volume: f64,
}
fn default_volume() -> f64 {
1.0
}
fn is_default_volume(volume: &f64) -> bool {
*volume == 1.0
}
练习:
- 将存档格式替换为
ron或toml并实现双向读写。 - 在读档时对缺失字段提供默认值以保证向后兼容。
概念补充
- 文本 vs 二进制:文本(JSON/TOML/RON)可读性好、体积较大;二进制(bincode)更紧凑、速度快但难调试。
- 兼容策略:为新字段提供默认值(
#[serde(default)])、使用Option<T>包容老版本;避免删除或重命名已发布字段。 - 精度与平台:注意整数溢出、浮点精度与端序问题(跨平台二进制时尤需关注)。
- I/O 健壮性:持久化采用"写临时文件→原子替换";写入时
fsync确保落盘(对崩溃敏感场景)。 - 安全考量:反序列化尽量采用受信格式与类型边界;避免对不受信输入使用
deserialize_any风格的宽松解析。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)