Administrator
发布于 2025-12-09 / 5 阅读
0

用rust写一个简单的存档修改器

本文主要记录一下遇到(还没学过或还没完全掌握)的问题,使用的是CodeX生成的JSON存档,仅供学习

一. 创建项目

这部分就带过一下

cargo new save_editor

二. 实现

1.安装依赖

首先,因为想要修改就必须先把存档反序列化成我自己的结构体,修改完之后再序列化成原本的save.json存档,所以需要在Cargo.toml里加入serdeserde_json依赖

这两个的功能是:

serde负责序列化和反序列化

serde_json负责处理JSON

[package]
name = "save_editor"
version = "0.1.0"
edition = "2021"

[dependencies]
# 告诉rust这个项目要用到serde(序列化/反序列化),以及serde_json(处理JSON)
serde = { version = "1", features = ["derive"] }
serde_json = "1"

2.实现功能

先看save.json存档里面都有啥玩意

{
  "player_name": "lvdl",
  "sun": 25,
  "hp": 90,
  "round": 2,
  "plants": 2
}

OK,知道有哪些部分以后就可以根据它写我们的结构体了

我这里用的笨方法,直接给游戏里的save.json存档从游戏目录拉到了我们的项目目录,方便修改

目前看来就是这样

接着我们来写如何实现修改

use std::error::Error;
use std::fs;

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct SaveData {
    player_name: String, //名字
    sun: u32,            //阳光
    hp: u32,             //血量
    round: u32,          //回合数
    plants: u32,         //植物数量
}
//读取存档解析成SaveData
fn load_save(path: &str) -> Result<SaveData, Box<dyn Error>> {
    let text = fs::read_to_string(path)?;
    //读取文件成字符串
    //这个问号是判断是否读到了,没读到直接error然后终止运行
    let save: SaveData = serde_json::from_str(&text)?;
    //把JSON字符串反序列化成SaveData结构体
    Ok(save)
    //返回结果
}
//把SaveData写回存档save.json
fn write_save(path: &str, save: &SaveData) -> Result<(), Box<dyn Error>> {
    let json = serde_json::to_string_pretty(save)?;
    //把SaveData序列化成JSON字符串
    fs::write(path, json)?;
    //写入文件,覆盖原内容
    Ok(())
}

fn main() -> Result<(), Box<dyn Error>> {
    //修改器主流程

    //这部分包括读取存档和显示目前的存档内容

    let path = "save.json";
    //路径是这个当前目录下的save.json
    let mut save = load_save(path)?;
    //调用上面的读取函数,读取存档
    println!("{:#?}", save);
    // println!("当前存档内容");
    // println!("玩家名:{}", save.player_name);
    // println!("阳光数:{}", save.sun);
    // println!("血量:{}", save.hp);
    // println!("回合数:{}", save.round);
    // println!("植物数量:{}", save.plants);
    //显示目前的存档内容

    //修改存档数据
    save.player_name = "张三".into();
    save.sun = 999;
    println!("修改后的内容");
    println!("玩家名:{}", save.player_name);
    println!("阳光数:{}", save.sun);

    write_save(path, &save)?;
    //调用上面的写回存档函数,把修改好的数据写回存档

    println!("\n{}存档文件已写回", path);

    Ok(())
}

其实逻辑都好理解:把这个JSON反序列化成我们的SaveData,然后在这个save里面修改数据

修改完成之后再把它序列化成save.json,怎么来的怎么回去,就这样

三. 问题

但是我的rust基础不太牢靠,很多地方会看不懂,我就当结合项目来学习了

在最顶上的三串use是啥

use std::error::Error; 
use std::fs; 
use serde::{Deserialize, Serialize};

这三句话其实都是干一件事:把别的地方定义好的东西拉到当前文件里用

use std::error::Error;

先看第一句

这里的std::error::Error是一个trait

完整来讲就是:标准库里的error模块里的一个trait,他叫Error

具体是干啥的:这个trait定义了错误类型应该是什么样子,就比如要能打印、要能返回错误信息等等

fn load_save(path: &str) -> Result<SaveData, Box<dyn Error>> { ... }

这一句里返回值有一个Box<dyn Error>,可以理解成:一个盒子,里面装着任意一种实现了Error这个trait的错误类型

use std::fs;

再看第二句

这里的std::fs是“文件系统"模块

fs的意思就是file system,它是标准库里负责读写文件的模块

我们的读取存档和修改存档都需要他

let text = fs::read_to_string(path)?;
fs::write(path, json)?;

就像这个写回存档部分使用的就是fs

这里再扩展一些std::fs里的常见函数

  • fs::read_to_string(path):把整个文件读成一个 String

  • fs::read(path):把整个文件读成 Vec<u8>(二进制)

  • fs::write(path, data):把数据写入文件(覆盖)

  • fs::create_dir / remove_file / rename 等……

use serde::{Deserialize, Serialize};

再看最后一句

这里的serde就不是std标准库了,他是一个外部库

之前再Cargo.toml里写的内容就是告诉Cargo我要用这两个库

serde 里有两个很重要的 trait:

  • serde::Serialize:表示“这个类型能被序列化”(转成 JSON、二进制等)

  • serde::Deserialize:表示“这个类型能从数据反序列化出来”

其实这部分就是实现序列化和反序列化的核心

总结

这三句都不是执行语句,是在给后面的代码做引用准备

#[derive(Debug, Serialize, Deserialize)] 这一串是干嘛的?

#[derive(...)]

这是一个“属性”,让编辑器帮忙自动生成代码,如果不用这个derive,那么给结构体实现某个trait就需要自己写很多的代码,非常麻烦:

impl std::fmt::Debug for SaveData {
    // 手动写怎么打印这个结构体……
}

每个trait的作用

Debug:这个很常见,就是让我们能用{:?}的格式打印这个结构体,适合调试

Serialize:和上面讲的一样,让SaveData可以被序列化,没有它哪些to_string都会编译不过

Deserialize:一样是来自外部库serde,是让SaveData可以被反序列化,从JSON或二进制里还原出来

读文档时Result是什么,Result里面的Box是什么

Result其实就是标准库里的一个枚举,定义差不多长这样

enum Result<T,E>{
  Ok(T)   //成功的值
  Err(E)  //失败的错误
}

意思是,要么成功了什么结果(就是那个Ok里的),要么失败了什么错误

Box<dyn Error>可以理解为一个盒子,里面装着某种错误,但是具体什么类型无所谓,只要它实现了上面的trait

为啥子要这么写?因为函数里可能会产生不同的错误

就比如fs::read_to_string(path)?;可能产生std::io::Error

serde_json::from_str(&text)?;可能产生serde_json::Error

如果返回类型写成Result<SaveData, std::io::Error>,那serde_json::Error就塞不进去,所以直接用一个盒子把所有实现了Error的都装起来,这样一个函数里可以用?来同时处理多种错误

写入存档时Result<(), Box<dyn Error>>为啥括号里是空的

其实这里就是没有返回值的意思,也就是这个函数成功时,不需要拿到任何返回值,就只失败时的错误

fs::write(path, json)?; 是怎么实现“覆盖”的?

这句实际上使用的是标准库里的函数

pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<(), Error>

这个函数的作用是:

  • 如果文件不存在,就创建一个新文件,把内容写进去

  • 如果文件已经存在,就先把元文件内容截断清空(truncate),再把新的内容写进去

反应到底层就是以 create(true) + truncate(true) + write(true) 的方式打开文件,所以旧的内容会被直接覆盖掉