本文主要记录一下遇到(还没学过或还没完全掌握)的问题,使用的是CodeX生成的JSON存档,仅供学习
一. 创建项目
这部分就带过一下
cargo new save_editor二. 实现
1.安装依赖
首先,因为想要修改就必须先把存档反序列化成我自己的结构体,修改完之后再序列化成原本的save.json存档,所以需要在Cargo.toml里加入serde和serde_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):把整个文件读成一个Stringfs::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) 的方式打开文件,所以旧的内容会被直接覆盖掉