docs: 补充 soulsoft 参考实现分析与路线对比

- §1.5 读源码总结 soulsoft_serialization 的做法:宏给每个类生成 4 个序列化方法
  + superType 参数,继承靠编译期 super/静态方法调用链,零反射读字段
- §2 方案总览明确路线 A(入口不变+父类宏)为主方案
- §8 新增路线 A/B 取舍对比表,明确推荐路线 A 的理由:
  保留 Serialize(obj)/Deserialize<T>(json) 免标注 API,只借 soulsoft
  「宏生成静态类型字段代码」技巧
This commit is contained in:
2026-08-17 12:48:03 +08:00
parent 85b17ed8ab
commit a7ed2902f5
+64 -2
View File
@@ -91,10 +91,49 @@ let result = f.apply(TypeInfo.of<BaseUser>(), args) // ✓ 调用成功
原理:`exportJsonFields` 内部用**静态类型** `BaseUser` 访问 `_id`,走的是普通字段访问(非反射),完全合法;外部通过静态方法反射调用,`checkStaticDeclaringType` 只校验声明的类(用 `thisType` 参数),不校验实例的运行时类型。
### 1.5 参考实现:soulsoft_serialization 是怎么做的(已读源码)
soulsoft 走的路线与本方案不同——**它完全不用反射读字段**,而是宏给**每个类**生成序列化方法,运行时是普通方法调用链:
```cangjie
@Serialization[superType: BaseUser] // 子类显式告诉宏:我的父类是 BaseUser
public class User <: BaseUser { ... }
```
宏在编译期(AST 层)为类生成 4 个方法 + 实现 `ISerialization<T>` 接口:
| 方法 | 可见性 | 作用 |
|---|---|---|
| `serializeObject(options): DataModel` | public | 序列化入口,构造 DataModelStruct 后调追加方法 |
| `serializeObject(dms, options): Unit` | protected | 追加本类字段(`dms.add(Field("_id", this._id.serializeObject(options)))`**编译期静态访问** |
| `deserializeObject(dm, options): T` | public static | 反序列化入口,构造实例后调追加方法 |
| `deserializeObject(dms, data, options): Unit` | protected static | 回填本类字段(`data._id = String.deserializeObject(...)`**编译期静态赋值** |
**继承串联靠编译期方法调用,不是反射**
```cangjie
// 子类生成的方法,方法体第一行:
super.serializeObject(dms, options) // 序列化:父类字段由父类方法追加
BaseUser.deserializeObject(dms, data, options) // 反序列化:先回填父类字段
```
关键点:
- 父类字段访问是宏生成的 `this._id` / `data._id = ...`,**静态类型访问**,天然绕开反射的声明类检查(与本方案 §2.2 同理);
- 运行时 `data.serializeObject(options)` 是接口虚方法调用、`T.deserializeObject(...)` 是泛型静态调用,全部编译期绑定,**零反射、零类型转换**;
- 宏怎么知道父类是谁?**用户显式传 `superType` 参数**——印证了「宏无法查询类型信息」的限制,soulsoft 用「让用户声明」解决;
- 代价:**每个类都要标宏**,且 API 带泛型约束 `where T <: ISerializable`——不标宏的类编译期就拒绝,「任意类免标注序列化」能力不存在。
**与本方案的关系**:soulsoft 的「宏生成静态类型字段代码」思路与本方案一致,可以借鉴;但它的「每个类标宏 + 泛型约束」形态会破坏 simapi_serialization「任意类免标注、`Serialize(obj)`/`Deserialize<T>(json)`」的 API 契约。因此本方案**只借其「宏生成字段代码」的技巧**,保留自己的反射入口(详见 §8 路线对比)。
---
## 2. 方案总览
> 本方案 = **路线 A**(推荐):保留 simapi_serialization 现有的反射入口 API
> 仅对「想暴露父类字段」的父类用宏生成静态导出/导入方法,库沿继承链调用。
> 路线 B(soulsoft 全宏风格)见 §8 对比,作为备选。
```
┌─────────────────────────────────────────────────────────┐
│ 宏 @JsonExportFields(标注在父类上) │
@@ -121,6 +160,10 @@ let result = f.apply(TypeInfo.of<BaseUser>(), args) // ✓ 调用成功
- 导出方法内部:`instance._id` 是静态类型字段访问(编译期解析),不是反射,无任何运行时检查;
- 导出方法外部:通过**静态方法反射**调用,静态方法没有「实例声明类」校验,只校验声明类(用 `thisType` 参数),子类实例天然是父类类型的合法值。
### 2.3 为什么不用 soulsoft 的「每个类标宏」形态
soulsoft 的 API 带泛型约束 `where T <: ISerializable`,不标宏的类在编译期就被拒绝——这要求**所有**序列化类都标注宏,与 simapi_serialization「任意类免标注、`Serialize(obj)`/`Deserialize<T>(json)`」的契约冲突。本方案把「宏生成字段代码」与「反射入口」分层:宏只在父类上补「父类字段」这一块,其余照旧反射,二者互不干扰。
---
## 3. 宏设计
@@ -386,11 +429,30 @@ main() {
---
## 8. 备选方案对比
## 8. 路线对比与备选方案
### 8.1 两条可行路线的取舍
| | 路线 A:入口不变 + 父类宏(本方案) | 路线 B:soulsoft 全宏风格 |
|---|---|---|
| 宏标在哪 | **仅父类**标 `@JsonExportFields` | **每个类**标 `@Serialization[superType: 父类]` |
| 继承串联 | 库运行时反射 `superClass` 链 + 静态方法调用 | 编译期 `super.serializeObject(...)` / `Parent.deserializeObject(...)` 调用链 |
| `Serialize(obj)` / `Deserialize<T>(json)` | ✅ **完全不变**(无泛型约束,任意类) | 需保留反射回退,否则无宏类编译期拒绝 |
| 无宏的类 | ✅ 照旧全反射(向后兼容) | 无法序列化(除非入口加反射回退) |
| 调用方改动 | 无 | 每个类要加标注 + `superType` |
| 运行时开销 | 一次静态方法反射查找(可缓存) | 零反射,纯方法调用 |
| 类型安全 | 导出方法内静态访问(安全),返回值 `as` 转换(需兜底) | 全程编译期强类型 |
| 宏实现复杂度 | 中(解析字段生成 2 个静态方法) | 高(生成 4 方法 + 接口实现 + 构造器 + prop |
| 字段约定 | 无(所有 public var | 必须 `_` 前缀 + var + 类型注解(`isAllowedMemberVarDecl` |
**结论**:路线 B 的运行时性能更优,但要求每个类标宏、约束字段命名、改变 API 形态——与 simapi_serialization「任意类免标注」的核心价值冲突。**推荐路线 A**:只借 soulsoft「宏生成静态类型字段代码」的技巧,入口 API、调用方、无宏类行为全部保持现状。
### 8.2 备选方案总表
| 方案 | 可行性 | 改动量 | 说明 |
|---|---|---|---|
| **宏标注父类 + 静态导出方法(本方案** | ✅ 已验证 | 中 | 自动、向后兼容、无反射限制 |
| **路线 A宏标注父类 + 静态导出方法(推荐** | ✅ 已验证 | 中 | 自动、向后兼容、无反射限制、API 不变 |
| 路线 B:soulsoft 全宏风格(每类标宏 + superType) | ✅ 可参考 | 高 | 零反射,但破坏免标注 API、约束字段命名 |
| 序列化库运行时遍历继承链 + 反射读父类字段 | ❌ | — | `getValue` 声明类严格校验,直接抛异常 |
| 宏把父类字段复制到子类 | ❌ | — | 编译器禁止字段遮蔽 |
| 用户在子类手写父类字段(复制粘贴) | ✅ | 小但人工 | 破坏继承语义、易漏改;仅作为临时 workaround |