ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RapidJSON 教程实战:深入理解 DOM 风格的 Value 与 Document API

RapidJSON 教程实战:深入理解 DOM 风格的 Value 与 Document API RapidJSON 教程实战深入理解 DOM 风格的 Value 与 Document API【免费下载链接】rapidjsonA fast JSON parser/generator for C with both SAX/DOM style API项目地址: https://gitcode.com/GitHub_Trending/ra/rapidjson本文基于 RapidJSON 官方中文教程doc/tutorial.zh-cn.md展开系统讲解 DOM 风格 API 的核心用法将 JSON 解析为Document后如何按类型安全地查询Value如何以移动语义创建、修改、深复制和交换 DOM 树节点。读完本文你可以独立完成解析、遍历、增删改各类 JSON 节点并回写 JSON 的完整流程同时理解这些行为背后在 include/rapidjson/document.h 中的真实实现避免误用类型断言、拷贝字符串生命周期等常见陷阱。核心概念Value 与 DocumentRapidJSON 的 DOM API 只有两个核心类型Value每一个 JSON 值都储存为一个Value类实例。它是一个变体类型variant可承载 Null、False、True、Object、Array、String、Number 七种类型之一Document表示整棵 DOM它本身就是根Value并持有为整棵树分配内存的分配器。所有公开类型和函数都位于rapidjson命名空间中。Document默认模板参数是UTF8编码与MemoryPoolAllocatorCrtAllocator这一点可以从宏定义确认include/rapidjson/document.h#define RAPIDJSON_DEFAULT_ALLOCATOR ::RAPIDJSON_NAMESPACE::MemoryPoolAllocator::RAPIDJSON_NAMESPACE::CrtAllocatorGenericValue模板类本身Value是其 UTF8 默认实例化的 typedef见 include/rapidjson/document.htemplate typename Encoding, typename Allocator RAPIDJSON_DEFAULT_ALLOCATOR class GenericValue; // include/rapidjson/document.h#L667-L668查询 Value下面各节代码与可运行的完整示例 example/tutorial/tutorial.cpp 一一对应。假设我们用一个 C 字符串保存了一段 JSON{ hello: world, t: true , f: false, n: null, i: 123, pi: 3.1416, a: [1, 2, 3, 4] }把它解析至一个Document#include rapidjson/document.h using namespace rapidjson; // ... Document document; document.Parse(json);此时 JSON 已被解析至document中成为一棵 DOM 树。需要说明的是自 RFC 7159 起合法 JSON 文件的根可以是任何类型的 JSON 值而在较早的 RFC 4627 中根值只允许是 Object 或 Array。上述例子的根是一个 Objectassert(document.IsObject());按类型查询各成员由于一个Value可包含不同类型的值需要先验证类型再用对应的 API 取值// String assert(document.HasMember(hello)); assert(document[hello].IsString()); printf(hello %s\n, document[hello].GetString()); // 输出: world // BoolJSON True/False 值是以 bool 表示的 assert(document[t].IsBool()); printf(t %s\n, document[t].GetBool() ? true : false); // 输出: true // Null printf(n %s\n, document[n].IsNull() ? null : ?); // 输出: null查询 NumberJSON 只提供一种数值类型 Number但 C 需要更专门的类型。RapidJSON 的 DOM 在解析 Number 时会将其存为下列其中一种类型类型描述unsigned32 位无符号整数int32 位有符号整数uint64_t64 位无符号整数int64_t64 位有符号整数double64 位双精度浮点数查询时用检查函数确认能否以目标类型提取检查提取bool IsNumber()不适用bool IsUint()unsigned GetUint()bool IsInt()int GetInt()bool IsUint64()uint64_t GetUint64()bool IsInt64()int64_t GetInt64()bool IsDouble()double GetDouble()assert(document[i].IsNumber()); // 在此情况下IsUint()/IsInt64()/IsUint64() 也会返回 true assert(document[i].IsInt()); printf(i %d\n, document[i].GetInt()); // 另一种用法 (int)document[i] assert(document[pi].IsNumber()); assert(document[pi].IsDouble()); printf(pi %g\n, document[pi].GetDouble());从源码结构看“一个整数可以用多种类型提取而不必转换”并非魔法构造int值时RapidJSON 会根据数值范围一次性点亮所有可行的位标记include/rapidjson/document.hexplicit GenericValue(int i) RAPIDJSON_NOEXCEPT : data_() { data_.n.i64 i; data_.f.flags (i 0) ? (kNumberIntFlag | kUintFlag | kUint64Flag) : kNumberIntFlag; }因此值为 123 的x会令x.IsInt() x.IsUint() x.IsInt64() x.IsUint64() true而值为 -3000000000 的y只会令y.IsInt64() true。注意GetDouble()会把内部整数表示转换成doubleint/unsigned可以安全转换但int64_t与uint64_t可能丧失精度double尾数只有 52 位。查询 Array// 使用引用来连续访问方便之余还更高效。 const Value a document[a]; assert(a.IsArray()); for (SizeType i 0; i a.Size(); i) // 使用 SizeType 而不是 size_t printf(a[%d] %d\n, i, a[i].GetInt());输出a[0] 1 a[1] 2 a[2] 3 a[3] 4关于SizeType缺省情况下它是unsigned的 typedefinclude/rapidjson/rapidjson.h即 RapidJSON 在 64 位平台上也使用 32 位索引Array 最多存储 2^32-1 个元素。若你需要 64 位索引可以在包含头文件前定义RAPIDJSON_NO_SIZETYPEDEFINE并自行提供rapidjson::SizeTypeinclude/rapidjson/rapidjson.h。重要原则RapidJSON 不做隐式类型转换。对一个 String 类型的 Value 调用GetInt()是非法的——调试模式下会断言失败发布模式下行为未定义。用迭代器访问 ArrayArray 与std::vector相似除了下标访问也可用迭代器访问所有元素for (Value::ConstValueIterator itr a.Begin(); itr ! a.End(); itr) printf(%d , itr-GetInt());还有一些熟悉的查询函数SizeType Capacity() const、bool Empty() const。范围 for 循环v1.1.0 起的新功能启用 C11 后可以直接遍历GetArray()返回的代理对象for (auto v : a.GetArray()) printf(%d , v.GetInt());用迭代器访问 Object 成员与 Array 相似可以用迭代器访问 Object 的全部成员static const char* kTypeNames[] { Null, False, True, Object, Array, String, Number }; for (Value::ConstMemberIterator itr document.MemberBegin(); itr ! document.MemberEnd(); itr) { printf(Type of member %s is %s\n, itr-name.GetString(), kTypeNames[itr-value.GetType()]); }输出Type of member hello is String Type of member t is True Type of member f is False Type of member n is Null Type of member i is Number Type of member pi is Number Type of member a is ArrayC11 下同样可以for (auto m : document.GetObject()) printf(Type of member %s is %s\n, m.name.GetString(), kTypeNames[m.value.GetType()]);HasMember、operator[] 与 FindMember 的区别注意operator[](const char*)在找不到成员时会断言失败。若不确定成员是否存在先调用HasMember()再取operator[]会做两次查找更好的做法是直接调用FindMember()它一次查找既检查存在性又返回 ValueValue::ConstMemberIterator itr document.FindMember(hello); if (itr ! document.MemberEnd()) printf(%s\n, itr-value.GetString());源码中HasMember本身就是FindMember的封装include/rapidjson/document.h而FindMember在默认编译配置下是成员线性扫描include/rapidjson/document.h所以“先 HasMember 再 operator[]”确实会产生两次完整遍历FindMember是单遍查找。example/tutorial/tutorial.cpp 中也明确用FindMember做“一次查找检查存在性并取值”的演示。查询 String 与 GetStringLength除GetString()外Value还提供GetStringLength()。根据 RFC 4627JSON String 可包含 Unicode 字符U0000写作\u0000而 C/C 空字符结尾字符串把\0当作结束符。RapidJSON 支持包含U0000的 String此时必须用GetStringLength()获得正确长度。例如解析{ s : a\u0000b }后a\u0000b的正确长度是 3但strlen()只会返回 1。GetStringLength()同时提升性能避免了用户手动调用strlen()。转换到std::string时也应使用带长度的构造函数string(const char* s, size_t count);比较两个 Value与!比较两个 Value当且仅当类型与内容都相同才相等也可以直接与原生类型值比较if (document[hello] document[n]) /*...*/; // 比较两个值 if (document[hello] world) /*...*/; // 与字符串字面量比较 if (document[i] ! 123) /*...*/; // 与整数比较 if (document[pi] ! 3.14) /*...*/; // 与 double 比较Array/Object 按元素/成员的整棵子树递归比较。注意若一个 Object 含重复命名成员它与其他 Object 比较总返回false。创建修改值DOM 树被创建或修改后可使用Writer重新序列化为 JSON教程示例的最后一步见 example/tutorial/tutorial.cppStringBuffer sb; PrettyWriterStringBuffer writer(sb); document.Accept(writer); // Accept() 遍历 DOM 并产生 Handler 事件 puts(sb.GetString());改变 Value 类型默认构造的Value或Document类型为 Null。要改变类型调用SetXXX()或赋值操作Document d; // Null d.SetObject(); Value v; // Null v.SetInt(10); v 10; // 简写和上面相同部分类型提供重载构造函数Value b(true); // 调用 Value(bool) Value i(-123); // 调用 Value(int) Value u(123u); // 调用 Value(unsigned) Value d(1.5); // 调用 Value(double)重建空 Object/Array 可在默认构造后用SetObject()/SetArray()或一步到位Value o(kObjectType); Value a(kArrayType);移动语义Move SemanticsRapidJSON 一个特别的设计决定Value的赋值不是复制而是把来源 Value 移动move到目的 ValueValue a(123); Value b(456); b a; // a 变成 Nullb 变成数字 123。为什么这样设计答案是性能。对 Number、True、False、Null 这类定长类型复制很快但对 String、Array、Object 这类变长类型复制代价高昂且常被忽视——尤其当我们需要创建临时 Object、复制到另一变量、再析构它。若使用常规复制语义Value o(kObjectType); { Value contacts(kArrayType); // 把元素加进 contacts 数组。 // ... o.AddMember(contacts, contacts, d.GetAllocator()); // 深度复制 contacts可能大量内存分配 // 析构 contacts。 }此时o需要分配一块和 contacts 等大的缓冲区做深度复制随后还要析构 contacts带来大量无谓的内存分配/释放与拷贝。业界常见规避手段如引用计数、垃圾回收但为了保持 RapidJSON 简单且快速设计者选择了赋值即移动语义类似std::auto_ptr的拥有权转移Value o(kObjectType); { Value contacts(kArrayType); // adding elements to contacts array. o.AddMember(contacts, contacts, d.GetAllocator()); // 只需 memcpy() contacts 本身至新成员的 Value16 字节 // contacts 在这里变成 Null。它的析构是平凡的。 }这一语义在源码中非常直白——赋值操作符只是借助RawAssign交换数据块并置空来源include/rapidjson/document.hGenericValue operator(GenericValue rhs) RAPIDJSON_NOEXCEPT { if (RAPIDJSON_LIKELY(this ! rhs)) { GenericValue temp; temp.RawAssign(rhs); // data_ rhs.data_; rhs 置为 kNullFlag this-~GenericValue(); RawAssign(temp); } return *this; }而RawAssign本体只有三条语句include/rapidjson/document.hvoid RawAssign(GenericValue rhs) RAPIDJSON_NOEXCEPT { data_ rhs.data_; rhs.data_.f.flags kNullFlag; }在 C11 中这称为 move assignment operator由于 RapidJSON 支持 C03它让普通赋值就具备移动语义AddMember()、PushBack()等修改型函数同样如此。AddMember内部对 name/value 也正是调用RawAssign完成接管include/rapidjson/document.h。移动语义与临时值有时想直接构造一个临时 Value 传给移动函数如PushBack()、AddMember()。由于临时对象不能转换为普通Value引用RapidJSON 提供了Move()include/rapidjson/document.hValue a(kArrayType); Document::AllocatorType allocator document.GetAllocator(); // a.PushBack(Value(42), allocator); // 不能通过编译 a.PushBack(Value().SetInt(42), allocator); // fluent API a.PushBack(Value(42).Move(), allocator); // 和上一行相同创建 Stringcopy-string 与 const-string 两种策略RapidJSON 提供两种字符串存储策略copy-string分配缓冲区并把源数据复制进去总是安全的const-string只保存字符串指针适用于字符串字面量以及 DOM 章节讲到的原位in situ解析场景。为了让用户自定义内存分配方式任何可能分配内存的操作都要求显式传入 allocator 实例作为参数——这避免了每个Value内部保存 allocator或 document指针。copy-string 赋值使用带 allocator 的SetString()重载Document document; Value author; char buffer[10]; int len sprintf(buffer, %s %s, Milo, Yip); // 动态创建的字符串。 author.SetString(buffer, len, document.GetAllocator()); memset(buffer, 0, sizeof(buffer)); // 清空 buffer 后 author.GetString() 仍然包含 Milo Yip源码中这条路径最终落到SetStringRaw(StringRefType, Allocator)短字符串会放进 Value 内部的内联短串Short String区域只有超长字符串才真正走allocator.Malloc()分配include/rapidjson/document.h——因此短字符串的 copy-string 并不会触发堆分配。上面的SetString()需要长度参数可正确处理含空字符的字符串另一个无长度参数的重载假设输入是空字符结尾的会调用类似strlen()的函数取长度。const-string 版本没有 allocator 参数。对字符串字面量或生命周期安全的字符串直接传字面量即可安全且高效Value s; s.SetString(rapidjson); // 可包含空字符长度在编译期推导 s rapidjson; // 上行的缩写对普通字符指针需要用StringRef显式标记“不复制也是安全的”const char * cstr getenv(USER); size_t cstr_len ...; // 如果有长度 Value s; // s.SetString(cstr); // 这不能通过编译 s.SetString(StringRef(cstr)); // 可以假设其生命周期安全且以空字符结尾 s StringRef(cstr); // 上行的缩写 s.SetString(StringRef(cstr, cstr_len));// 更快可处理空字符 s StringRef(cstr, cstr_len); // 上行的缩写修改 ArrayArray 类型的 Value 提供与std::vector相似的 APIClear()Reserve(SizeType, Allocator)Value PushBack(Value, Allocator)template typename T GenericValue PushBack(T, Allocator)Value PopBack()ValueIterator Erase(ConstValueIterator pos)ValueIterator Erase(ConstValueIterator first, ConstValueIterator last)Reserve(...)与PushBack(...)可能为数组元素分配内存因此需要 allocator。示例Value a(kArrayType); Document::AllocatorType allocator document.GetAllocator(); for (int i 5; i 10; i) a.PushBack(i, allocator); // 可能需要调用 realloc()所以需要 allocator // 流畅接口Fluent interface a.PushBack(Lua, allocator).PushBack(Mio, allocator);与 STL 不同PushBack()/PopBack()返回 Array 本身的引用构成流畅接口fluent interface。若要在 Array 中加入非常量字符串或生命周期不足的字符串见“创建 String”必须用 copy-string API 创建 String为避免中间变量可就地使用临时值// 就地 Value 参数 contact.PushBack(Value(copy, document.GetAllocator()).Move(), // copy string document.GetAllocator()); // 显式 Value 参数 Value val(key, document.GetAllocator()); // copy string contact.PushBack(val, document.GetAllocator());修改 ObjectObject 是键值对的集合每个键必须为 String。增加成员的 APIValue AddMember(Value, Value, Allocator allocator)Value AddMember(StringRefType, Value, Allocator)template typename T Value AddMember(StringRefType, T value, Allocator)Value contact(kObject); contact.AddMember(name, Milo, document.GetAllocator()); contact.AddMember(married, true, document.GetAllocator());以StringRefType为 name 参数的重载与字符串SetString的接口相似其用意是避免复制 name 字符串——JSON Object 中经常使用常数键名。若键名来自非常量或生命周期不足的字符串需要 copy-string 版本// 就地 Value 参数 contact.AddMember(Value(copy, document.GetAllocator()).Move(), // copy string Value().Move(), // null value document.GetAllocator()); // 显式参数 Value key(key, document.GetAllocator()); // copy string name Value val(42); // 某 Value contact.AddMember(key, val, document.GetAllocator());移除成员的选择bool RemoveMember(const Ch* name)按键名移除线性时间复杂度bool RemoveMember(const Value name)同上name为 ValueMemberIterator RemoveMember(MemberIterator)用迭代器移除常数时间复杂度MemberIterator EraseMember(MemberIterator)类似但保持成员次序线性时间复杂度MemberIterator EraseMember(MemberIterator first, MemberIterator last)移除范围内成员保持次序线性时间复杂度。“常数时间”的实现在源码中可以验证DoRemoveMember采用“转移最后”手法——析构迭代器位置成员把最后一个成员移动到该位置因此成员次序会被打乱include/rapidjson/document.h而DoEraseMembers则通过memmove前移后续成员来保持次序include/rapidjson/document.h。深复制 Value如果确实需要复制一棵 DOM 树可用两个 API带 allocator 的构造函数和CopyFrom()。注意拷贝构造函数被显式声明为私有GenericValue(const GenericValue rhs);在 include/rapidjson/document.h 的 private 区因此Value v2(v1);这类普通拷贝编译不过必须显式提供 allocatorDocument d; Document::AllocatorType a d.GetAllocator(); Value v1(foo); // Value v2(v1); // 不容许 Value v2(v1, a); // 制造一个克隆 assert(v1.IsString()); // v1 不变 d.SetArray().PushBack(v1, a).PushBack(v2, a); assert(v1.IsNull() v2.IsNull()); // 两个都转移动 d v2.CopyFrom(d, a); // 把整个 document 复制至 v2 assert(d.IsArray() d.Size() 2); // d 不变 v1.SetObject().AddMember(array, v2, a); d.PushBack(v1, a);从源码看深复制构造函数对 Object/Array 会逐成员放置新Value到 allocator 内存include/rapidjson/document.h 及 DoCopyMembers对 const-string 则默认只共享指针可通过第三个参数copyConstStrings强制拷贝对引用 in-situ 缓冲区的字符串很有用。CopyFrom()同样是“析构 放置新对象”的实现include/rapidjson/document.h。交换 ValueRapidJSON 提供Swap()无论两棵 DOM 树多复杂交换都是常数时间的Value a(123); Value b(Hello); a.Swap(b); assert(a.IsString()); assert(b.IsInt());下一部分的学习路线本教程展示了如何查询及修改 DOM 树。RapidJSON 还有几个重要概念值得继续深入流读写 JSON 的通道可以是内存字符串、文件流等用户也可以自定义流编码定义流或内存中使用的字符编码RapidJSON 内部提供 Unicode 转换与校验DOM更高级功能如原位in situ解析、其他解析选项及高级用法本教程仅覆盖基础部分SAXRapidJSON 解析/生成功能的基础学习Reader/Writer实现更高性能的应用也可用PrettyWriter格式化 JSON性能官方及第三方性能测试技术内幕RapidJSON 内部设计与技术。另外可参考 常见问题、API 文档、示例程序如 example/tutorial/tutorial.cpp以及 单元测试 来交叉验证本文所述行为。【免费下载链接】rapidjsonA fast JSON parser/generator for C with both SAX/DOM style API项目地址: https://gitcode.com/GitHub_Trending/ra/rapidjson创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表