571 lines
19 KiB
Markdown
571 lines
19 KiB
Markdown
在火车上写 JSON 解析器是一个绝佳的选择,因为它不需要外部依赖,且逻辑非常严密。
|
||
|
||
解析 JSON 的核心思想是**递归下降解析(Recursive Descent Parsing)**。你可以把它想象成一个“吃字符串”的过程:根据当前看到的第一个字符,决定下一步怎么走。
|
||
|
||
---
|
||
|
||
### 1. 核心架构设计
|
||
|
||
首先,你需要定义一个类来存储 JSON 的各种类型。在现代 C++ 中,建议使用 `std::variant`。
|
||
|
||
```cpp
|
||
#include <iostream>
|
||
#include <variant>
|
||
#include <vector>
|
||
#include <map>
|
||
#include <string>
|
||
|
||
// 定义 JSON 的各种数据类型
|
||
struct JsonValue;
|
||
using JsonObject = std::map<std::string, JsonValue>;
|
||
using JsonArray = std::vector<JsonValue>;
|
||
|
||
struct JsonValue {
|
||
std::variant<std::nullptr_t, bool, double, std::string, JsonArray, JsonObject> data;
|
||
|
||
// 方便的构造函数
|
||
JsonValue() : data(nullptr) {}
|
||
// ... 其他构造函数
|
||
};
|
||
|
||
```
|
||
|
||
---
|
||
|
||
### 2. 解析逻辑:递归下降
|
||
|
||
你需要一个解析类,维护一个当前处理到的“指针”(或迭代器)。
|
||
|
||
#### 逻辑流程图:
|
||
|
||
**解析伪代码思路:**
|
||
|
||
1. **跳过空白符:** 忽略空格、换行、制表符。
|
||
2. **主解析函数 `parse_value()`:**
|
||
* 看到 `[` → 调用 `parse_array()`
|
||
* 看到 `{` → 调用 `parse_object()`
|
||
* 看到 `"` → 调用 `parse_string()`
|
||
* 看到 `t`/`f` → 调用 `parse_boolean()`
|
||
* 看到 `n` → 调用 `parse_null()`
|
||
* 看到数字或 `-` → 调用 `parse_number()`
|
||
|
||
|
||
|
||
#### 关键实现:解析对象(Object)
|
||
|
||
```cpp
|
||
JsonValue parse_object() {
|
||
JsonObject obj;
|
||
consume('{'); // 消耗掉左大括号
|
||
skip_whitespace();
|
||
|
||
if (peek() == '}') {
|
||
consume('}');
|
||
return JsonValue(obj);
|
||
}
|
||
|
||
while (true) {
|
||
std::string key = parse_string().as_string(); // 解析键
|
||
skip_whitespace();
|
||
consume(':'); // 消耗冒号
|
||
obj[key] = parse_value(); // 递归解析值
|
||
|
||
skip_whitespace();
|
||
if (peek() == ',') {
|
||
consume(',');
|
||
skip_whitespace();
|
||
} else if (peek() == '}') {
|
||
consume('}');
|
||
break;
|
||
}
|
||
}
|
||
return JsonValue(obj);
|
||
}
|
||
|
||
```
|
||
|
||
---
|
||
|
||
### 3. 火车上的挑战点(你可以深挖的地方)
|
||
|
||
在火车上,你可以重点攻克以下几个难点,这能体现你的 C++ 功底:
|
||
|
||
* **数值解析:** 不要简单地用 `atof`。尝试手动处理科学计数法(如 `1.23e-10`),处理负号、小数点。
|
||
* **转义字符:** 当字符串里出现 `\"`, `\\`, `\n` 时,如何正确存入内存?
|
||
* **错误处理:** 如果 JSON 格式写错了(比如少个逗号),你的解析器是直接崩溃,还是能抛出一个带行号的 `exception`?
|
||
* **性能优化(进阶):** 尝试使用 `std::string_view` 来减少解析过程中的字符串拷贝,这是现代 C++ 性能优化的核心。
|
||
|
||
---
|
||
|
||
### 🛠️ 开发建议
|
||
|
||
1. **先写一个 `peek()` 和 `consume()`:**
|
||
* `peek()`:看一眼当前的字符,但不移动指针。
|
||
* `consume(char c)`:检查当前字符是否为 `c`,如果是则移动到下一个,不是则报错。
|
||
|
||
|
||
2. **测试用例:** 准备一个最复杂的 `test.json`,包含嵌套的对象、数组、各种类型的混合。
|
||
|
||
**如果你准备动工,需要我为你提供一个完整的 `JsonValue` 类定义模板,还是你先尝试自己写一下基础框架?**
|
||
|
||
|
||
既然要手撸解析器,第一步确实得把 JSON 的“骨架”摸清楚。JSON(JavaScript Object Notation)本质上就是一种**嵌套的键值对结构**,它的美感在于极其有限的规则。
|
||
|
||
---
|
||
|
||
### 1. JSON 的 6 种基本数据类型
|
||
|
||
一个 JSON 文件里,无论多复杂,都逃不出这 6 种东西:
|
||
|
||
1. **Object(对象/映射):** 用 `{}` 包围,里面是 `key: value` 形式。
|
||
2. **Array(数组):** 用 `[]` 包围,里面是值的列表。
|
||
3. **String(字符串):** 必须用双引号 `"` 包围,支持转义(如 `\n`)。
|
||
4. **Number(数字):** 整数或浮点数,支持科学计数法。
|
||
5. **Boolean(布尔):** 只有 `true` 或 `false`。
|
||
6. **Null:** 只有一个值 `null`。
|
||
|
||
**举个例子:**
|
||
|
||
```json
|
||
{
|
||
"name": "Gemini",
|
||
"is_ai": true,
|
||
"version": 3.5,
|
||
"tags": ["fast", "helpful"],
|
||
"stats": { "cpu": 8, "ram": "16GB" }
|
||
}
|
||
|
||
```
|
||
|
||
---
|
||
|
||
### 2. 解析思路:递归下降(核心逻辑)
|
||
|
||
解析 JSON 最经典的方法是**“递归下降解析”**。它的核心逻辑就像是在走迷宫:你站在当前字符的位置,根据看到的符号决定走哪个房间。
|
||
|
||
#### 核心步骤流程:
|
||
|
||
**第一步:词法分析(Tokenize)——可选**
|
||
把整个字符串切成一个个有意义的单位(Token),比如 `{`、`[`、`"name"`、`123`。但在火车上为了省事,可以直接在原字符串上操作。
|
||
|
||
**第二步:语法解析(Parsing)——核心**
|
||
你需要写一个主函数 `parse_value()`,它的逻辑如下:
|
||
|
||
1. **跳过空白:** 循环跳过空格、`\n`、`\t`。
|
||
2. **看第一个字符:**
|
||
* 如果是 `{`:**递归**进入 `parse_object()`。
|
||
* 如果是 `[`:**递归**进入 `parse_array()`。
|
||
* 如果是 `"`:进入 `parse_string()`,直到遇到下一个不被转义的 `"`。
|
||
* 如果是 `t` 或 `f`:匹配是不是 `true` 或 `false`。
|
||
* 如果是 `n`:匹配是不是 `null`。
|
||
* 如果是 `-` 或 `0-9`:进入 `parse_number()`。
|
||
|
||
|
||
|
||
---
|
||
|
||
### 3. 如何处理递归?
|
||
|
||
这是最妙的地方。当你在解析一个 **Object** 时,遇到 `key: value` 中的 `value`,你并不需要知道它是数字还是另一个对象,你只需要再次调用 **`parse_value()`**。
|
||
|
||
**伪代码逻辑:**
|
||
|
||
```cpp
|
||
// 伪代码:解析数组
|
||
JsonArray parse_array() {
|
||
consume('['); // 吃掉左括号
|
||
JsonArray arr;
|
||
while (peek() != ']') {
|
||
// 关键点:数组里的元素可以是任何类型,所以递归调用
|
||
arr.push_back(parse_value());
|
||
|
||
if (peek() == ',') consume(','); // 吃掉逗号继续循环
|
||
}
|
||
consume(']'); // 吃掉右括号
|
||
return arr;
|
||
}
|
||
|
||
```
|
||
|
||
---
|
||
|
||
### 4. 火车上撸代码的实战建议
|
||
|
||
1. **数据结构设计:** 使用 `std::variant<nullptr_t, bool, double, string, vector<JsonValue>, map<string, JsonValue>>` 来表示 `JsonValue`。这是 C++17 的利器,能非常优雅地存储不同类型。
|
||
2. **错误捕获:**
|
||
如果解析到一半发现格式不对(比如 `{"key": }`),你可以抛出一个异常,打印出当前指针的位置。
|
||
3. **先从简单的写起:**
|
||
* 第一阶段:能解析 `true` / `false` / `null`。
|
||
* 第二阶段:能解析字符串和数字。
|
||
* 第三阶段:实现 `parse_array`。
|
||
* 第四阶段:实现最复杂的 `parse_object`。
|
||
|
||
|
||
|
||
**你想先从定义存储 JSON 的 `JsonValue` 类开始,还是直接写那个“吃字符串”的主循环逻辑?我可以给你一个起步的 C++ 框架。**
|
||
|
||
|
||
在火车上写项目,一个清晰、标准的目录结构能帮你理清思路,即使没有 IDE 的高级功能,靠 `g++` 或 `cmake` 也能轻松编译。
|
||
|
||
推荐一个**轻量级但专业**的 C++ 项目结构:
|
||
|
||
---
|
||
|
||
### 📂 项目文件结构:`TinyJSON`
|
||
|
||
```text
|
||
TinyJSON/
|
||
├── .gitignore # 忽略编译生成的 .exe, .o, build/ 文件夹
|
||
├── CMakeLists.txt # 项目构建脚本(如果用 CMake)
|
||
├── Makefile # 如果不用 CMake,手写编译命令也可以
|
||
├── README.md # 项目说明,记录你的解析思路
|
||
├── include/ # 头文件目录
|
||
│ └── tinyjson.hpp # 核心类 JsonValue 的定义
|
||
├── src/ # 源代码目录
|
||
│ ├── main.cpp # 程序入口(用于测试)
|
||
│ └── tinyjson.cpp # 解析逻辑的具体实现
|
||
└── tests/ # 测试用例目录
|
||
└── test_cases.json # 准备一些各种格式的 JSON 文件用来喂给程序
|
||
|
||
```
|
||
|
||
---
|
||
|
||
### 核心代码骨架预览
|
||
|
||
为了让你直接开工,我帮你把最关键的两个文件草图勾勒出来:
|
||
|
||
#### 1. `include/tinyjson.hpp`
|
||
|
||
这是你的“蓝图”。在火车上,建议把所有数据类型定义好。
|
||
|
||
```cpp
|
||
#pragma once
|
||
#include <string>
|
||
#include <vector>
|
||
#include <map>
|
||
#include <variant>
|
||
#include <memory>
|
||
|
||
namespace tinyjson {
|
||
|
||
struct JsonValue;
|
||
|
||
// 使用 C++17 的 variant 存储 6 种 JSON 类型
|
||
using Object = std::map<std::string, JsonValue>;
|
||
using Array = std::vector<JsonValue>;
|
||
using ValueData = std::variant<std::nullptr_t, bool, double, std::string, Array, Object>;
|
||
|
||
struct JsonValue {
|
||
ValueData data;
|
||
|
||
// 构造函数
|
||
JsonValue() : data(nullptr) {}
|
||
JsonValue(bool v) : data(v) {}
|
||
JsonValue(double v) : data(v) {}
|
||
JsonValue(std::string v) : data(std::move(v)) {}
|
||
// ... 其他构造函数
|
||
|
||
// 序列化:将内存中的对象转回字符串
|
||
std::string serialize() const;
|
||
};
|
||
|
||
// 解析器类
|
||
class Parser {
|
||
public:
|
||
explicit Parser(std::string source) : src(std::move(source)), pos(0) {}
|
||
JsonValue parse(); // 主入口
|
||
|
||
private:
|
||
std::string src;
|
||
size_t pos;
|
||
|
||
void skip_whitespace();
|
||
char peek();
|
||
char consume();
|
||
|
||
JsonValue parse_value();
|
||
JsonValue parse_string();
|
||
JsonValue parse_number();
|
||
// ... parse_object, parse_array
|
||
};
|
||
|
||
} // namespace tinyjson
|
||
|
||
```
|
||
|
||
#### 2. `src/main.cpp`
|
||
|
||
这是你验证成果的地方。
|
||
|
||
```cpp
|
||
#include "tinyjson.hpp"
|
||
#include <iostream>
|
||
|
||
int main() {
|
||
std::string test_json = "{\"name\": \"Gemini\", \"score\": 100, \"is_cool\": true}";
|
||
|
||
tinyjson::Parser parser(test_json);
|
||
try {
|
||
tinyjson::JsonValue val = parser.parse();
|
||
std::cout << "Successfully parsed!" << std::endl;
|
||
std::cout << val.serialize() << std::endl;
|
||
} catch (const std::exception& e) {
|
||
std::cerr << "Parse Error: " << e.what() << std::endl;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
```
|
||
|
||
---
|
||
|
||
### 💡 火车上的小技巧
|
||
|
||
* **极简编译命令:** 如果你没配置 CMake,直接在根目录执行:
|
||
`g++ -std=c++17 src/main.cpp src/tinyjson.cpp -I ./include -o json_parser`
|
||
* **断言测试:** 建议多用 `assert()`。写完 `parse_number` 马上测一下 `0`, `-1.5`, `2e10` 是否都能过,别等全部写完再调。
|
||
|
||
**这个结构对你来说合适吗?如果你已经建好了文件夹,我们可以先攻克 `parse_value` 这个“分拣中心”的逻辑。**
|
||
|
||
这是一个非常棒的工程化思考!**答案是:非常有必要,而且建议采用这种“数据与逻辑分离”的设计。**
|
||
|
||
在 C++ 中,这种做法不仅符合“单一职责原则”(Single Responsibility Principle),还能避免循环包含(Circular Dependency)的问题。
|
||
|
||
我们可以把它们拆分为以下两个部分:
|
||
|
||
---
|
||
|
||
### 1. `Json.h` (或 `JsonValue.h`):数据的容器
|
||
|
||
这个类负责**“存储”**。它代表了 JSON 在内存中的样子。
|
||
|
||
* **职责:** 定义数据结构(使用 `std::variant`)、提供访问数据的接口(比如 `as_int()`, `is_object()`)、以及将内存对象转回字符串(序列化)。
|
||
* **好处:** 你的业务代码只需要引用这个类,它不关心 JSON 是从网络下载的、从文件读的,还是手动构建的。
|
||
|
||
### 2. `JsonParser.h`:数据的加工厂
|
||
|
||
这个类负责**“解析”**。它像是一台扫描仪。
|
||
|
||
* **职责:** 接收原始字符串,通过内部的 `pos` 指针进行词法和语法分析,最后产出一个 `JsonValue` 对象。
|
||
* **好处:** 解析逻辑通常很复杂(处理空白符、递归、转义字符等),把它独立出来可以让代码更易读,也方便以后更换解析算法(比如从递归下降改为状态机)。
|
||
|
||
---
|
||
|
||
### 修改后的文件结构(更专业版)
|
||
|
||
```text
|
||
TinyJSON/
|
||
├── include/
|
||
│ ├── JsonValue.h # 定义 JSON 的数据模型 (The DOM)
|
||
│ └── JsonParser.h # 定义解析逻辑 (The Engine)
|
||
└── src/
|
||
├── JsonValue.cpp
|
||
└── JsonParser.cpp
|
||
|
||
```
|
||
|
||
### 为什么这样做在 C++ 中很重要?
|
||
|
||
当你处理**嵌套结构**(比如 JSON Object 里面又有一个 JSON Object)时,`JsonValue` 需要引用自身。
|
||
|
||
如果你把解析逻辑和数据模型混在一起,当你的解析函数 `parse_object` 需要返回一个 `JsonValue`,而 `JsonValue` 又包含一个调用 `parse_object` 的成员时,头文件包含关系会变得非常混乱。
|
||
|
||
**现在的分工应该是:**
|
||
|
||
1. `JsonValue.h`:先定义出“房子”长什么样。
|
||
2. `JsonParser.h`:引用 `JsonValue.h`,定义“如何盖房子”。
|
||
|
||
---
|
||
|
||
### 🛠️ 火车撸码小建议:
|
||
|
||
在实现 `JsonParser` 时,建议采用 **“流式处理”** 的思维:
|
||
|
||
* 给 `JsonParser` 一个私有成员 `std::string_view source;` 和 `size_t offset;`。
|
||
* `string_view` (C++17) 相比 `std::string` 在处理子串时**不产生内存拷贝**,在火车上写这种高性能代码,成就感会爆棚。
|
||
|
||
**你是打算先从 `JsonValue.h` 的底层结构定义开始写,还是想直接切入 `JsonParser.h` 的解析主循环?我可以帮你写出这两个类之间最关键的“解耦”代码片段。**
|
||
|
||
|
||
在火车上静下心来钻研 C++ 的新特性是非常棒的。这两个问题正好触及了 C++ 工程设计的核心:**数据表示**与**逻辑解耦**。
|
||
|
||
---
|
||
|
||
### 1. 为什么要用 `std::variant`?
|
||
|
||
在 JSON 中,一个“值”可能是数字、字符串、布尔值,也可能是嵌套的对象。
|
||
在 C 语言或老版本 C++ 中,我们通常用 `union`(联合体)来做,但 `union` 有个致命缺点:**它不知道自己当前存的是哪种类型**,且不支持像 `std::string` 这样复杂的 C++ 对象。
|
||
|
||
`std::variant` 是 C++17 引入的“类型安全联合体”:
|
||
|
||
* **多合一容器:** 它能同时定义多种可能的类型,但在某一时刻只存其中一种。
|
||
* **类型安全:** 它自带“标签”,知道自己现在存的是 `double` 还是 `std::string`。如果你存了 `bool` 却想按 `int` 取出来,它会抛出异常,而不是像 `union` 那样给你一堆乱码。
|
||
* **支持复杂对象:** 它能完美容纳 `std::map` 或 `std::vector`,这对处理 JSON 嵌套至关重要。
|
||
|
||
---
|
||
|
||
### 2. `JsonValue` 与 `JsonParser` 的角色分配
|
||
|
||
你可以把它们想象成 **“货架”** 与 **“搬运工”** 的关系:
|
||
|
||
#### **JsonValue(货架/数据模型)**
|
||
|
||
它的作用是**在内存中代表一个 JSON 元素**。
|
||
|
||
* **它是静止的:** 它只负责“存数据”和“给数据”。
|
||
* **它长什么样:** 它内部持有一个 `std::variant`。
|
||
* **它能干啥:** * 提供接口查询:`is_string()`,`is_number()`。
|
||
* 提供接口获取:`as_string()`,`as_double()`。
|
||
* 提供序列化:`to_string()`(把内存里的数据变回文本)。
|
||
|
||
|
||
|
||
#### **JsonParser(搬运工/解析引擎)**
|
||
|
||
它的作用是**把乱糟糟的字符串变成整齐的 `JsonValue**`。
|
||
|
||
* **它是动态的:** 它负责扫描字符串、处理逻辑、报错。
|
||
* **它长什么样:** 它内部持有一个字符串指针(或索引),记录读到了哪里。
|
||
* **它能干啥:**
|
||
* 跳过空格。
|
||
* 识别 `true` 是布尔值。
|
||
* 识别 `{` 后面紧跟着的是 `key:value` 结构。
|
||
* **最终产出:** 它辛苦工作半天,最后只给用户返回一个 `JsonValue` 对象。
|
||
|
||
|
||
|
||
---
|
||
|
||
### 3. 解析思路(How it works)
|
||
|
||
想象你要解析这段字符串:`{"age": 25}`
|
||
|
||
1. 用户创建 `JsonParser("{\"age\": 25}")`。
|
||
2. 调用 `parser.parse()`。
|
||
3. **解析器**看到 `{`,知道这是一个 **Object**。
|
||
4. 它继续往后走,看到 `"age"`,知道这是键。
|
||
5. 它看到 `:` 后面的 `25`,知道这是数字。
|
||
6. **解析器**在内部创建一个 `JsonValue` 对象,把数字 `25` 塞进它的 `std::variant` 里。
|
||
7. 最后,**解析器**把这个填满数据的 `JsonValue` 扔给用户。
|
||
|
||
---
|
||
|
||
### 🛠️ 火车撸码实践:
|
||
|
||
你可以先尝试定义这个“货架”:
|
||
|
||
```cpp
|
||
// JsonValue.h 核心片段
|
||
#include <variant>
|
||
#include <string>
|
||
#include <vector>
|
||
#include <map>
|
||
|
||
struct JsonValue; // 前置声明
|
||
|
||
using Object = std::map<std::string, JsonValue>;
|
||
using Array = std::vector<JsonValue>;
|
||
|
||
class JsonValue {
|
||
public:
|
||
// 这里就是 Variant 的威力
|
||
std::variant<std::nullptr_t, bool, double, std::string, Array, Object> data;
|
||
|
||
// 各种构造函数...
|
||
bool is_number() const { return std::holds_alternative<double>(data); }
|
||
double as_number() const { return std::get<double>(data); }
|
||
};
|
||
|
||
```
|
||
|
||
**现在你的电脑里建好这两个文件了吗?如果准备好了,我可以带你写出 `JsonParser` 里的第一个函数——如何识别并解析一个数字。**
|
||
|
||
|
||
当然可以!而且**返回 `JsonValue` 才是最正确、最标准的设计**。
|
||
|
||
你可以把 `JsonValue` 想象成一个**“无限嵌套的俄罗斯套娃”**。
|
||
|
||
### 1. 为什么返回 `JsonValue` 就够了?
|
||
|
||
因为 JSON 的根节点(Root)要么是一个 **对象(Object)** `{...}`,要么是一个 **数组(Array)** `[...]`。
|
||
|
||
由于我们在 `JsonValue` 的 `std::variant` 里已经包含了 `Object` 和 `Array`,所以一个 `JsonValue` 变量完全有能力代表整个 JSON 文件的内容。
|
||
|
||
### 2. 解析逻辑的返回值
|
||
|
||
在你的 `JsonParser` 类里,主入口函数应该是这样的:
|
||
|
||
```cpp
|
||
// JsonParser.h
|
||
class JsonParser {
|
||
public:
|
||
// 用户调用这个函数,传入整个 JSON 字符串,得到一个 JsonValue
|
||
static JsonValue parse(const std::string& content) {
|
||
JsonParser parser(content);
|
||
return parser.parse_value();
|
||
}
|
||
|
||
private:
|
||
// 内部递归解析函数
|
||
JsonValue parse_value() {
|
||
skip_whitespace();
|
||
char c = peek();
|
||
if (c == '{') return parse_object();
|
||
if (c == '[') return parse_array();
|
||
if (c == '"') return parse_string();
|
||
// ... 其他类型
|
||
}
|
||
};
|
||
|
||
```
|
||
|
||
### 3. 用户拿到结果后怎么用?
|
||
|
||
用户拿到返回的 `JsonValue` 后,只需要判断它内部存的是什么即可:
|
||
|
||
```cpp
|
||
// main.cpp
|
||
std::string content = load_file("config.json");
|
||
JsonValue root = JsonParser::parse(content);
|
||
|
||
// 如果我知道根节点是个对象
|
||
if (std::holds_alternative<Object>(root.data)) {
|
||
auto& obj = std::get<Object>(root.data);
|
||
// 像使用 map 一样访问里面的数据
|
||
std::cout << "Username: " << std::get<std::string>(obj["username"].data) << std::endl;
|
||
}
|
||
|
||
```
|
||
|
||
### 4. 火车上的进阶思路:如何优雅地“拿”数据?
|
||
|
||
如果你觉得 `std::get<...>(obj["..."].data)` 写起来太长、太痛苦,你可以在 `JsonValue` 类里增加一些辅助方法。这是 C++ 程序员最喜欢干的事:
|
||
|
||
```cpp
|
||
// 在 JsonValue 类内部增加
|
||
class JsonValue {
|
||
public:
|
||
// 运算符重载:让 JsonValue 像 map 一样好用
|
||
JsonValue& operator[](const std::string& key) {
|
||
return std::get<Object>(data)[key];
|
||
}
|
||
|
||
// 方便转换
|
||
std::string as_string() const { return std::get<std::string>(data); }
|
||
double as_double() const { return std::get<double>(data); }
|
||
};
|
||
|
||
// 这样用户代码就变成了:
|
||
std::cout << root["username"].as_string(); // 是不是优雅多了?
|
||
|
||
```
|
||
|
||
### 总结
|
||
|
||
* **JsonValue** 是“数据”,它是静止的结果。
|
||
* **JsonParser** 是“工具”,它是解析的动作。
|
||
* **整个 JSON 文件** 解析完后,就是一个**处于最顶层的 JsonValue**(它内部套着其他的 JsonValue)。
|
||
|
||
**你准备好写 `parse_value` 这个“分拣中心”了吗?它是整个解析器逻辑最核心的跳转枢纽。** |