存档与读档——文件 IO 与序列化

上文我们大致了解了"文字地牢"小游戏的错误处理与输入校验,那么我们本章的目标是掌握 serde 序列化;理解文本与二进制格式的权衡;处理版本兼容与健壮性。

基本概念

文件 I/O

Rust标准库提供了丰富的文件I/O操作功能,包括读取、写入、创建和删除文件等操作。

文件I/O操作的类型:

  1. 同步I/O:阻塞当前线程直到操作完成
  2. 异步I/O:非阻塞操作,适用于高并发场景
  3. 缓冲I/O:通过缓冲区减少系统调用次数
  4. 直接I/O:绕过操作系统缓存,直接与存储设备交互

文件操作模式:

  1. 只读File::open()
  2. 写入File::create()
  3. 追加OpenOptions::append()
  4. 读写OpenOptions::read().write()

序列化与反序列化

序列化是将内存中的数据结构转换为可存储或传输的格式,反序列化则是将存储的数据转换回内存中的数据结构。

序列化格式的分类:

  1. 文本格式:JSON、XML、TOML、YAML等,人类可读
  2. 二进制格式:Bincode、Protocol Buffers、MessagePack等,紧凑高效
  3. 自描述格式:包含类型信息,无需外部模式定义
  4. 非自描述格式:需要外部模式定义才能正确解析

serde crate

serde是Rust生态系统中非常流行的序列化框架,支持多种数据格式,包括JSON、TOML、RON等。

serde的核心组件:

  1. Serialize trait:定义如何将类型序列化为数据格式
  2. Deserialize trait:定义如何从数据格式反序列化为类型
  3. Serializer/Deserializer:特定格式的实现
  4. 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 字段;读取时根据版本做兼容分支。

注意事项

  1. 处理文件I/O操作时,始终考虑错误情况并妥善处理。
  2. 在序列化和反序列化时,注意数据的兼容性和版本控制。
  3. 对于敏感数据,要考虑安全性,避免反序列化不受信任的数据。
  4. 在处理大文件时,考虑内存使用情况,避免一次性加载整个文件到内存。
  5. 注意文件路径的处理,特别是在跨平台应用中。
  6. 使用适当的缓冲策略来提高I/O性能。
  7. 考虑原子操作来确保数据一致性。
  8. 对于网络传输的数据,验证数据完整性和来源。

本项目中的使用

在我们的项目中,我们使用serdeserde_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)
}

我们的数据模型通过派生SerializeDeserialize trait来支持序列化:

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Player {
    pub position: Position,
    pub hp: i32,
}

改进建议

在地牢探险游戏中,我们可以对存档系统进行以下改进:

  1. 版本控制
#[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())),
        }
    }
}
  1. 原子写入
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(())
}
  1. 多格式支持
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
}

练习:

  1. 将存档格式替换为 rontoml 并实现双向读写。
  2. 在读档时对缺失字段提供默认值以保证向后兼容。

概念补充

  • 文本 vs 二进制:文本(JSON/TOML/RON)可读性好、体积较大;二进制(bincode)更紧凑、速度快但难调试。
  • 兼容策略:为新字段提供默认值(#[serde(default)])、使用 Option<T> 包容老版本;避免删除或重命名已发布字段。
  • 精度与平台:注意整数溢出、浮点精度与端序问题(跨平台二进制时尤需关注)。
  • I/O 健壮性:持久化采用"写临时文件→原子替换";写入时 fsync 确保落盘(对崩溃敏感场景)。
  • 安全考量:反序列化尽量采用受信格式与类型边界;避免对不受信输入使用 deserialize_any 风格的宽松解析。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐