1. 为什么我们需要JSON助手?
如果你用过Delphi原生的JSON操作库,肯定对那一长串的AddPair、GetValue、TryGetValue记忆犹新。每次操作JSON都要写一堆重复代码,一个简单的赋值操作可能要写三四行。我在实际项目中就遇到过这种情况:一个包含嵌套结构的JSON对象,光是初始化代码就写了上百行,看得人头皮发麻。
Delphi从XE6版本开始原生支持JSON操作,这个设计确实很完善,但用起来实在太啰嗦。比如要给一个JSON对象添加字符串属性,原生写法是这样的:
var jo: TJSONObject; begin jo := TJSONObject.Create; try jo.AddPair('name', TJSONString.Create('张三')); // 更多操作... finally jo.Free; end; end;而用我们封装的JSON助手,同样的操作只需要一行:
jo.S['name'] := '张三';这种简洁不是偷懒,而是实实在在提升开发效率。我在维护一个老项目时,把原来用SuperObject写的JSON操作全部迁移到了原生库,代码量直接减少了40%。特别是处理复杂嵌套结构时,这种简洁语法的优势更加明显。
2. 助手的核心设计思路
2.1 类助手的魔法
这个JSON助手的核心是利用了Delphi的类助手(Class Helper)特性。类助手可以给现有类添加新的方法和属性,就像给这个类"打补丁"一样。我们给TJSONObject添加了一系列快捷访问属性:
type TJSONObjectHelper = class helper for TJSONObject public property S[PairName: string]: string read Get_ValueS write Set_ValueS; property I[PairName: string]: integer read Get_ValueI write Set_ValueI; // 其他属性... end;每个属性都对应着getter和setter方法,内部处理了类型转换、空值判断等细节。比如S属性对应的setter是这样的:
procedure TJSONObjectHelper.Set_ValueS(PairName, PairValue: string); var js: TJSONString; begin if Self.TryGetValue(PairName, js) then Self.RemovePair(PairName).Free; Self.AddPair(PairName, PairValue); end;这个设计有几个巧妙之处:
- 自动处理已有键值:如果键已存在,会先删除旧值
- 内存管理:记得Free被替换掉的对象,避免内存泄漏
- 空值安全:所有getter都处理了键不存在的情况
2.2 支持的数据类型
助手目前支持了开发中最常用的几种数据类型:
- S[]:字符串类型,对应TJSONString
- I[]:整数类型,对应TJSONNumber的整型值
- I64[]:Int64大整数
- D[]:日期时间,存储为TJSONNumber的Double值
- B[]:布尔值,对应TJSONBool
- A[]:数组,对应TJSONArray
- O[]:子对象,对应TJSONObject
这个设计覆盖了90%以上的JSON使用场景。我在实际项目中还扩展过一些特殊类型,比如专门处理GUID的G[]属性,原理都是一样的。
3. 实际应用场景示例
3.1 基本CRUD操作
让我们看一个完整的增删改查示例。假设我们要处理一个用户信息JSON:
var user: TJSONObject; begin user := TJSONObject.Create; try // 增 user.S['name'] := '李四'; user.I['age'] := 30; user.B['isVIP'] := True; // 改 user.I['age'] := 31; // 修改年龄 // 查 if user.Exists['name'] then ShowMessage(user.S['name']); // 删 user.Delete['isVIP']; // 输出最终JSON Memo1.Lines.Text := user.Format; finally user.Free; end; end;输出结果会是:
{ "name": "李四", "age": 31 }这种写法比原生API简洁多了,而且可读性更好。我在团队推广这个助手后,新人上手JSON操作的速度明显加快。
3.2 处理嵌套结构
JSON的嵌套结构用原生API写起来特别繁琐,用助手就简单多了。比如要构建这样的结构:
{ "user": { "name": "王五", "address": { "city": "北京", "street": "中关村" } } }用助手可以这样写:
var root: TJSONObject; begin root := TJSONObject.Create; try root.O['user'] := TJSONObject.Create; root.O['user'].S['name'] := '王五'; root.O['user'].O['address'] := TJSONObject.Create; root.O['user'].O['address'].S['city'] := '北京'; root.O['user'].O['address'].S['street'] := '中关村'; ShowMessage(root.Format); finally root.Free; end; end;虽然还是要创建每个嵌套对象,但至少属性赋值变得非常直观。我在处理复杂API返回数据时,这种写法节省了大量时间。
4. 高级技巧与注意事项
4.1 日期处理的坑
JSON标准没有专门的日期类型,我们通常用字符串或数字表示日期。助手提供了D[]属性来自动转换TDateTime:
// 写入日期 jo.D['birthday'] := Now; // 读取日期 birthday := jo.D['birthday'];但这里有个细节要注意:日期是以Double值存储的,相当于TDateTime的内部表示。如果你需要和其他系统交互,可能需要手动转换为字符串格式:
// 写入格式化日期 jo.S['birthday'] := FormatDateTime('yyyy-mm-dd', Now); // 读取时转换 birthday := StrToDate(jo.S['birthday']);我在一次跨系统集成中就踩过这个坑,前端期望的是"yyyy-mm-dd"格式的字符串,而我们直接传了Double值,导致解析出错。
4.2 内存管理最佳实践
虽然助手简化了操作,但内存管理还是要特别注意:
- 所有创建的TJSONObject和TJSONArray最终都要Free
- 使用try-finally块确保资源释放
- 修改属性值时,助手会自动Free旧值
- 数组和对象类型的属性不会自动Free传入的值
特别提醒最后一点,这样的代码会导致内存泄漏:
var arr: TJSONArray; begin arr := TJSONArray.Create; jo.A['items'] := arr; // 忘记Free arr end;正确做法是让JSON对象接管生命周期:
jo.A['items'] := TJSONArray.Create; // 由jo负责释放或者显式管理:
var arr: TJSONArray; begin arr := TJSONArray.Create; try jo.A['items'] := arr; except arr.Free; raise; end; end;我在代码审查中发现这是最常见的错误之一,建议团队统一采用第一种简写方式。
4.3 性能优化建议
在处理大型JSON数据时,有几个性能优化点:
- 预分配大数组:如果知道数组大概大小,可以先创建指定大小的TJSONArray
- 批量操作:多次修改可以考虑先转成字符串,用字符串操作,再转回JSON
- 避免频繁查询:如果需要多次访问同一属性,可以先用局部变量缓存
例如,处理包含1000个元素的数组:
// 不推荐 - 每次都要通过属性访问 for i := 0 to 999 do arr.Add(jo.A['items'].Items[i].Value); // 推荐 - 缓存数组引用 items := jo.A['items']; for i := 0 to 999 do arr.Add(items.Items[i].Value);在性能测试中,第二种写法能快20%左右。对于服务端高频处理的JSON数据,这种优化还是很可观的。