]>
Commit | Line | Data |
---|---|---|
31f18b77 FG |
1 | # 教程 |
2 | ||
3 | 本教程简介文件对象模型(Document Object Model, DOM)API。 | |
4 | ||
5 | 如 [用法一览](../readme.zh-cn.md#用法一览) 中所示,可以解析一个 JSON 至 DOM,然后就可以轻松查询及修改 DOM,并最终转换回 JSON。 | |
6 | ||
7 | [TOC] | |
8 | ||
9 | # Value 及 Document {#ValueDocument} | |
10 | ||
11 | 每个 JSON 值都储存为 `Value` 类,而 `Document` 类则表示整个 DOM,它存储了一个 DOM 树的根 `Value`。RapidJSON 的所有公开类型及函数都在 `rapidjson` 命名空间中。 | |
12 | ||
13 | # 查询 Value {#QueryValue} | |
14 | ||
15 | 在本节中,我们会使用到 `example/tutorial/tutorial.cpp` 中的代码片段。 | |
16 | ||
17 | 假设我们用 C 语言的字符串储存一个 JSON(`const char* json`): | |
18 | ~~~~~~~~~~js | |
19 | { | |
20 | "hello": "world", | |
21 | "t": true , | |
22 | "f": false, | |
23 | "n": null, | |
24 | "i": 123, | |
25 | "pi": 3.1416, | |
26 | "a": [1, 2, 3, 4] | |
27 | } | |
28 | ~~~~~~~~~~ | |
29 | ||
30 | 把它解析至一个 `Document`: | |
31 | ~~~~~~~~~~cpp | |
32 | #include "rapidjson/document.h" | |
33 | ||
34 | using namespace rapidjson; | |
35 | ||
36 | // ... | |
37 | Document document; | |
38 | document.Parse(json); | |
39 | ~~~~~~~~~~ | |
40 | ||
41 | 那么现在该 JSON 就会被解析至 `document` 中,成为一棵 *DOM 树 *: | |
42 | ||
43 | ![教程中的 DOM](diagram/tutorial.png) | |
44 | ||
45 | 自从 RFC 7159 作出更新,合法 JSON 文件的根可以是任何类型的 JSON 值。而在较早的 RFC 4627 中,根值只允许是 Object 或 Array。而在上述例子中,根是一个 Object。 | |
46 | ~~~~~~~~~~cpp | |
47 | assert(document.IsObject()); | |
48 | ~~~~~~~~~~ | |
49 | ||
50 | 让我们查询一下根 Object 中有没有 `"hello"` 成员。由于一个 `Value` 可包含不同类型的值,我们可能需要验证它的类型,并使用合适的 API 去获取其值。在此例中,`"hello"` 成员关联到一个 JSON String。 | |
51 | ~~~~~~~~~~cpp | |
52 | assert(document.HasMember("hello")); | |
53 | assert(document["hello"].IsString()); | |
54 | printf("hello = %s\n", document["hello"].GetString()); | |
55 | ~~~~~~~~~~ | |
56 | ||
57 | ~~~~~~~~~~ | |
58 | world | |
59 | ~~~~~~~~~~ | |
60 | ||
61 | JSON True/False 值是以 `bool` 表示的。 | |
62 | ~~~~~~~~~~cpp | |
63 | assert(document["t"].IsBool()); | |
64 | printf("t = %s\n", document["t"].GetBool() ? "true" : "false"); | |
65 | ~~~~~~~~~~ | |
66 | ||
67 | ~~~~~~~~~~ | |
68 | true | |
69 | ~~~~~~~~~~ | |
70 | ||
71 | JSON Null 值可用 `IsNull()` 查询。 | |
72 | ~~~~~~~~~~cpp | |
73 | printf("n = %s\n", document["n"].IsNull() ? "null" : "?"); | |
74 | ~~~~~~~~~~ | |
75 | ||
76 | ~~~~~~~~~~ | |
77 | null | |
78 | ~~~~~~~~~~ | |
79 | ||
80 | JSON Number 类型表示所有数值。然而,C++ 需要使用更专门的类型。 | |
81 | ||
82 | ~~~~~~~~~~cpp | |
83 | assert(document["i"].IsNumber()); | |
84 | ||
85 | // 在此情况下,IsUint()/IsInt64()/IsUInt64() 也会返回 true | |
86 | assert(document["i"].IsInt()); | |
87 | printf("i = %d\n", document["i"].GetInt()); | |
88 | // 另一种用法: (int)document["i"] | |
89 | ||
90 | assert(document["pi"].IsNumber()); | |
91 | assert(document["pi"].IsDouble()); | |
92 | printf("pi = %g\n", document["pi"].GetDouble()); | |
93 | ~~~~~~~~~~ | |
94 | ||
95 | ~~~~~~~~~~ | |
96 | i = 123 | |
97 | pi = 3.1416 | |
98 | ~~~~~~~~~~ | |
99 | ||
100 | JSON Array 包含一些元素。 | |
101 | ~~~~~~~~~~cpp | |
102 | // 使用引用来连续访问,方便之余还更高效。 | |
103 | const Value& a = document["a"]; | |
104 | assert(a.IsArray()); | |
105 | for (SizeType i = 0; i < a.Size(); i++) // 使用 SizeType 而不是 size_t | |
106 | printf("a[%d] = %d\n", i, a[i].GetInt()); | |
107 | ~~~~~~~~~~ | |
108 | ||
109 | ~~~~~~~~~~ | |
110 | a[0] = 1 | |
111 | a[1] = 2 | |
112 | a[2] = 3 | |
113 | a[3] = 4 | |
114 | ~~~~~~~~~~ | |
115 | ||
116 | 注意,RapidJSON 并不自动转换各种 JSON 类型。例如,对一个 String 的 Value 调用 `GetInt()` 是非法的。在调试模式下,它会被断言失败。在发布模式下,其行为是未定义的。 | |
117 | ||
118 | 以下将会讨论有关查询各类型的细节。 | |
119 | ||
120 | ## 查询 Array {#QueryArray} | |
121 | ||
122 | 缺省情况下,`SizeType` 是 `unsigned` 的 typedef。在多数系统中,Array 最多能存储 2^32-1 个元素。 | |
123 | ||
124 | 你可以用整数字面量访问元素,如 `a[0]`、`a[1]`、`a[2]`。 | |
125 | ||
126 | Array 与 `std::vector` 相似,除了使用索引,也可使用迭代器来访问所有元素。 | |
127 | ~~~~~~~~~~cpp | |
128 | for (Value::ConstValueIterator itr = a.Begin(); itr != a.End(); ++itr) | |
129 | printf("%d ", itr->GetInt()); | |
130 | ~~~~~~~~~~ | |
131 | ||
132 | 还有一些熟悉的查询函数: | |
133 | * `SizeType Capacity() const` | |
134 | * `bool Empty() const` | |
135 | ||
136 | ### 范围 for 循环 (v1.1.0 中的新功能) | |
137 | ||
138 | 当使用 C++11 功能时,你可使用范围 for 循环去访问 Array 内的所有元素。 | |
139 | ||
140 | ~~~~~~~~~~cpp | |
141 | for (auto& v : a.GetArray()) | |
142 | printf("%d ", v.GetInt()); | |
143 | ~~~~~~~~~~ | |
144 | ||
145 | ## 查询 Object {#QueryObject} | |
146 | ||
147 | 和 Array 相似,我们可以用迭代器去访问所有 Object 成员: | |
148 | ||
149 | ~~~~~~~~~~cpp | |
150 | static const char* kTypeNames[] = | |
151 | { "Null", "False", "True", "Object", "Array", "String", "Number" }; | |
152 | ||
153 | for (Value::ConstMemberIterator itr = document.MemberBegin(); | |
154 | itr != document.MemberEnd(); ++itr) | |
155 | { | |
156 | printf("Type of member %s is %s\n", | |
157 | itr->name.GetString(), kTypeNames[itr->value.GetType()]); | |
158 | } | |
159 | ~~~~~~~~~~ | |
160 | ||
161 | ~~~~~~~~~~ | |
162 | Type of member hello is String | |
163 | Type of member t is True | |
164 | Type of member f is False | |
165 | Type of member n is Null | |
166 | Type of member i is Number | |
167 | Type of member pi is Number | |
168 | Type of member a is Array | |
169 | ~~~~~~~~~~ | |
170 | ||
171 | 注意,当 `operator[](const char*)` 找不到成员,它会断言失败。 | |
172 | ||
173 | 若我们不确定一个成员是否存在,便需要在调用 `operator[](const char*)` 前先调用 `HasMember()`。然而,这会导致两次查找。更好的做法是调用 `FindMember()`,它能同时检查成员是否存在并返回它的 Value: | |
174 | ||
175 | ~~~~~~~~~~cpp | |
176 | Value::ConstMemberIterator itr = document.FindMember("hello"); | |
177 | if (itr != document.MemberEnd()) | |
178 | printf("%s\n", itr->value.GetString()); | |
179 | ~~~~~~~~~~ | |
180 | ||
181 | ### 范围 for 循环 (v1.1.0 中的新功能) | |
182 | ||
183 | 当使用 C++11 功能时,你可使用范围 for 循环去访问 Object 内的所有成员。 | |
184 | ||
185 | ~~~~~~~~~~cpp | |
186 | for (auto& m : document.GetObject()) | |
187 | printf("Type of member %s is %s\n", | |
188 | m.name.GetString(), kTypeNames[m.value.GetType()]); | |
189 | ~~~~~~~~~~ | |
190 | ||
191 | ## 查询 Number {#QueryNumber} | |
192 | ||
193 | JSON 只提供一种数值类型──Number。数字可以是整数或实数。RFC 4627 规定数字的范围由解析器指定。 | |
194 | ||
195 | 由于 C++ 提供多种整数及浮点数类型,DOM 尝试尽量提供最广的范围及良好性能。 | |
196 | ||
197 | 当解析一个 Number 时, 它会被存储在 DOM 之中,成为下列其中一个类型: | |
198 | ||
199 | 类型 | 描述 | |
200 | -----------|--------------------------------------- | |
201 | `unsigned` | 32 位无号整数 | |
202 | `int` | 32 位有号整数 | |
203 | `uint64_t` | 64 位无号整数 | |
204 | `int64_t` | 64 位有号整数 | |
205 | `double` | 64 位双精度浮点数 | |
206 | ||
207 | 当查询一个 Number 时, 你可以检查该数字是否能以目标类型来提取: | |
208 | ||
209 | 查检 | 提取 | |
210 | ------------------|--------------------- | |
211 | `bool IsNumber()` | 不适用 | |
212 | `bool IsUint()` | `unsigned GetUint()` | |
213 | `bool IsInt()` | `int GetInt()` | |
214 | `bool IsUint64()` | `uint64_t GetUint64()` | |
215 | `bool IsInt64()` | `int64_t GetInt64()` | |
216 | `bool IsDouble()` | `double GetDouble()` | |
217 | ||
218 | 注意,一个整数可能用几种类型来提取,而无需转换。例如,一个名为 `x` 的 Value 包含 123,那么 `x.IsInt() == x.IsUint() == x.IsInt64() == x.IsUint64() == true`。但如果一个名为 `y` 的 Value 包含 -3000000000,那么仅会令 `x.IsInt64() == true`。 | |
219 | ||
220 | 当要提取 Number 类型,`GetDouble()` 是会把内部整数的表示转换成 `double`。注意 `int` 和 `unsigned` 可以安全地转换至 `double`,但 `int64_t` 及 `uint64_t` 可能会丧失精度(因为 `double` 的尾数只有 52 位)。 | |
221 | ||
222 | ## 查询 String {#QueryString} | |
223 | ||
224 | 除了 `GetString()`,`Value` 类也有一个 `GetStringLength()`。这里会解释个中原因。 | |
225 | ||
226 | 根据 RFC 4627,JSON String 可包含 Unicode 字符 `U+0000`,在 JSON 中会表示为 `"\u0000"`。问题是,C/C++ 通常使用空字符结尾字符串(null-terminated string),这种字符串把 ``\0'` 作为结束符号。 | |
227 | ||
228 | 为了符合 RFC 4627,RapidJSON 支持包含 `U+0000` 的 String。若你需要处理这些 String,便可使用 `GetStringLength()` 去获得正确的字符串长度。 | |
229 | ||
230 | 例如,当解析以下的 JSON 至 `Document d` 之后: | |
231 | ||
232 | ~~~~~~~~~~js | |
233 | { "s" : "a\u0000b" } | |
234 | ~~~~~~~~~~ | |
235 | `"a\u0000b"` 值的正确长度应该是 3。但 `strlen()` 会返回 1。 | |
236 | ||
237 | `GetStringLength()` 也可以提高性能,因为用户可能需要调用 `strlen()` 去分配缓冲。 | |
238 | ||
239 | 此外,`std::string` 也支持这个构造函数: | |
240 | ||
241 | ~~~~~~~~~~cpp | |
242 | string(const char* s, size_t count); | |
243 | ~~~~~~~~~~ | |
244 | ||
245 | 此构造函数接受字符串长度作为参数。它支持在字符串中存储空字符,也应该会有更好的性能。 | |
246 | ||
247 | ## 比较两个 Value | |
248 | ||
249 | 你可使用 `==` 及 `!=` 去比较两个 Value。当且仅当两个 Value 的类型及内容相同,它们才当作相等。你也可以比较 Value 和它的原生类型值。以下是一个例子。 | |
250 | ||
251 | ~~~~~~~~~~cpp | |
252 | if (document["hello"] == document["n"]) /*...*/; // 比较两个值 | |
253 | if (document["hello"] == "world") /*...*/; // 与字符串家面量作比较 | |
254 | if (document["i"] != 123) /*...*/; // 与整数作比较 | |
255 | if (document["pi"] != 3.14) /*...*/; // 与 double 作比较 | |
256 | ~~~~~~~~~~ | |
257 | ||
258 | Array/Object 顺序以它们的元素/成员作比较。当且仅当它们的整个子树相等,它们才当作相等。 | |
259 | ||
260 | 注意,现时若一个 Object 含有重复命名的成员,它与任何 Object 作比较都总会返回 `false`。 | |
261 | ||
262 | # 创建/修改值 {#CreateModifyValues} | |
263 | ||
264 | 有多种方法去创建值。 当一个 DOM 树被创建或修改后,可使用 `Writer` 再次存储为 JSON。 | |
265 | ||
266 | ## 改变 Value 类型 {#ChangeValueType} | |
267 | 当使用默认构造函数创建一个 Value 或 Document,它的类型便会是 Null。要改变其类型,需调用 `SetXXX()` 或赋值操作,例如: | |
268 | ||
269 | ~~~~~~~~~~cpp | |
270 | Document d; // Null | |
271 | d.SetObject(); | |
272 | ||
273 | Value v; // Null | |
274 | v.SetInt(10); | |
275 | v = 10; // 简写,和上面的相同 | |
276 | ~~~~~~~~~~ | |
277 | ||
278 | ### 构造函数的各个重载 | |
279 | 几个类型也有重载构造函数: | |
280 | ||
281 | ~~~~~~~~~~cpp | |
282 | Value b(true); // 调用 Value(bool) | |
283 | Value i(-123); // 调用 Value(int) | |
284 | Value u(123u); // 调用 Value(unsigned) | |
285 | Value d(1.5); // 调用 Value(double) | |
286 | ~~~~~~~~~~ | |
287 | ||
288 | 要重建空 Object 或 Array,可在默认构造函数后使用 `SetObject()`/`SetArray()`,或一次性使用 `Value(Type)`: | |
289 | ||
290 | ~~~~~~~~~~cpp | |
291 | Value o(kObjectType); | |
292 | Value a(kArrayType); | |
293 | ~~~~~~~~~~ | |
294 | ||
295 | ## 转移语意(Move Semantics) {#MoveSemantics} | |
296 | ||
297 | 在设计 RapidJSON 时有一个非常特别的决定,就是 Value 赋值并不是把来源 Value 复制至目的 Value,而是把把来源 Value 转移(move)至目的 Value。例如: | |
298 | ||
299 | ~~~~~~~~~~cpp | |
300 | Value a(123); | |
301 | Value b(456); | |
302 | b = a; // a 变成 Null,b 变成数字 123。 | |
303 | ~~~~~~~~~~ | |
304 | ||
305 | ![使用移动语意赋值。](diagram/move1.png) | |
306 | ||
307 | 为什么?此语意有何优点? | |
308 | ||
309 | 最简单的答案就是性能。对于固定大小的 JSON 类型(Number、True、False、Null),复制它们是简单快捷。然而,对于可变大小的 JSON 类型(String、Array、Object),复制它们会产生大量开销,而且这些开销常常不被察觉。尤其是当我们需要创建临时 Object,把它复制至另一变量,然后再析构它。 | |
310 | ||
311 | 例如,若使用正常 * 复制 * 语意: | |
312 | ||
313 | ~~~~~~~~~~cpp | |
314 | Value o(kObjectType); | |
315 | { | |
316 | Value contacts(kArrayType); | |
317 | // 把元素加进 contacts 数组。 | |
318 | // ... | |
319 | o.AddMember("contacts", contacts, d.GetAllocator()); // 深度复制 contacts (可能有大量内存分配) | |
320 | // 析构 contacts。 | |
321 | } | |
322 | ~~~~~~~~~~ | |
323 | ||
324 | ![复制语意产生大量的复制操作。](diagram/move2.png) | |
325 | ||
326 | 那个 `o` Object 需要分配一个和 contacts 相同大小的缓冲区,对 conacts 做深度复制,并最终要析构 contacts。这样会产生大量无必要的内存分配/释放,以及内存复制。 | |
327 | ||
328 | 有一些方案可避免实质地复制这些数据,例如引用计数(reference counting)、垃圾回收(garbage collection, GC)。 | |
329 | ||
330 | 为了使 RapidJSON 简单及快速,我们选择了对赋值采用 * 转移 * 语意。这方法与 `std::auto_ptr` 相似,都是在赋值时转移拥有权。转移快得多简单得多,只需要析构原来的 Value,把来源 `memcpy()` 至目标,最后把来源设置为 Null 类型。 | |
331 | ||
332 | 因此,使用转移语意后,上面的例子变成: | |
333 | ||
334 | ~~~~~~~~~~cpp | |
335 | Value o(kObjectType); | |
336 | { | |
337 | Value contacts(kArrayType); | |
338 | // adding elements to contacts array. | |
339 | o.AddMember("contacts", contacts, d.GetAllocator()); // 只需 memcpy() contacts 本身至新成员的 Value(16 字节) | |
340 | // contacts 在这里变成 Null。它的析构是平凡的。 | |
341 | } | |
342 | ~~~~~~~~~~ | |
343 | ||
344 | ![转移语意不需复制。](diagram/move3.png) | |
345 | ||
346 | 在 C++11 中这称为转移赋值操作(move assignment operator)。由于 RapidJSON 支持 C++03,它在赋值操作采用转移语意,其它修改形函数如 `AddMember()`, `PushBack()` 也采用转移语意。 | |
347 | ||
348 | ### 转移语意及临时值 {#TemporaryValues} | |
349 | ||
350 | 有时候,我们想直接构造一个 Value 并传递给一个“转移”函数(如 `PushBack()`、`AddMember()`)。由于临时对象是不能转换为正常的 Value 引用,我们加入了一个方便的 `Move()` 函数: | |
351 | ||
352 | ~~~~~~~~~~cpp | |
353 | Value a(kArrayType); | |
354 | Document::AllocatorType& allocator = document.GetAllocator(); | |
355 | // a.PushBack(Value(42), allocator); // 不能通过编译 | |
356 | a.PushBack(Value().SetInt(42), allocator); // fluent API | |
357 | a.PushBack(Value(42).Move(), allocator); // 和上一行相同 | |
358 | ~~~~~~~~~~ | |
359 | ||
360 | ## 创建 String {#CreateString} | |
361 | RapidJSON 提供两个 String 的存储策略。 | |
362 | ||
363 | 1. copy-string: 分配缓冲区,然后把来源数据复制至它。 | |
364 | 2. const-string: 简单地储存字符串的指针。 | |
365 | ||
366 | Copy-string 总是安全的,因为它拥有数据的克隆。Const-string 可用于存储字符串字面量,以及用于在 DOM 一节中将会提到的 in-situ 解析中。 | |
367 | ||
368 | 为了让用户自定义内存分配方式,当一个操作可能需要内存分配时,RapidJSON 要求用户传递一个 allocator 实例作为 API 参数。此设计避免了在每个 Value 存储 allocator(或 document)的指针。 | |
369 | ||
370 | 因此,当我们把一个 copy-string 赋值时, 调用含有 allocator 的 `SetString()` 重载函数: | |
371 | ||
372 | ~~~~~~~~~~cpp | |
373 | Document document; | |
374 | Value author; | |
375 | char buffer[10]; | |
376 | int len = sprintf(buffer, "%s %s", "Milo", "Yip"); // 动态创建的字符串。 | |
377 | author.SetString(buffer, len, document.GetAllocator()); | |
378 | memset(buffer, 0, sizeof(buffer)); | |
379 | // 清空 buffer 后 author.GetString() 仍然包含 "Milo Yip" | |
380 | ~~~~~~~~~~ | |
381 | ||
382 | 在此例子中,我们使用 `Document` 实例的 allocator。这是使用 RapidJSON 时常用的惯用法。但你也可以用其他 allocator 实例。 | |
383 | ||
384 | 另外,上面的 `SetString()` 需要长度参数。这个 API 能处理含有空字符的字符串。另一个 `SetString()` 重载函数没有长度参数,它假设输入是空字符结尾的,并会调用类似 `strlen()` 的函数去获取长度。 | |
385 | ||
386 | 最后,对于字符串字面量或有安全生命周期的字符串,可以使用 const-string 版本的 `SetString()`,它没有 allocator 参数。对于字符串家面量(或字符数组常量),只需简单地传递字面量,又安全又高效: | |
387 | ||
388 | ~~~~~~~~~~cpp | |
389 | Value s; | |
390 | s.SetString("rapidjson"); // 可包含空字符,长度在编译萁推导 | |
391 | s = "rapidjson"; // 上行的缩写 | |
392 | ~~~~~~~~~~ | |
393 | ||
394 | 对于字符指针,RapidJSON 需要作一个标记,代表它不复制也是安全的。可以使用 `StringRef` 函数: | |
395 | ||
396 | ~~~~~~~~~cpp | |
397 | const char * cstr = getenv("USER"); | |
398 | size_t cstr_len = ...; // 如果有长度 | |
399 | Value s; | |
400 | // s.SetString(cstr); // 这不能通过编译 | |
401 | s.SetString(StringRef(cstr)); // 可以,假设它的生命周期安全,并且是以空字符结尾的 | |
402 | s = StringRef(cstr); // 上行的缩写 | |
403 | s.SetString(StringRef(cstr, cstr_len));// 更快,可处理空字符 | |
404 | s = StringRef(cstr, cstr_len); // 上行的缩写 | |
405 | ||
406 | ~~~~~~~~~ | |
407 | ||
408 | ## 修改 Array {#ModifyArray} | |
409 | Array 类型的 Value 提供与 `std::vector` 相似的 API。 | |
410 | ||
411 | * `Clear()` | |
412 | * `Reserve(SizeType, Allocator&)` | |
413 | * `Value& PushBack(Value&, Allocator&)` | |
414 | * `template <typename T> GenericValue& PushBack(T, Allocator&)` | |
415 | * `Value& PopBack()` | |
416 | * `ValueIterator Erase(ConstValueIterator pos)` | |
417 | * `ValueIterator Erase(ConstValueIterator first, ConstValueIterator last)` | |
418 | ||
419 | 注意,`Reserve(...)` 及 `PushBack(...)` 可能会为数组元素分配内存,所以需要一个 allocator。 | |
420 | ||
421 | 以下是 `PushBack()` 的例子: | |
422 | ||
423 | ~~~~~~~~~~cpp | |
424 | Value a(kArrayType); | |
425 | Document::AllocatorType& allocator = document.GetAllocator(); | |
426 | ||
427 | for (int i = 5; i <= 10; i++) | |
428 | a.PushBack(i, allocator); // 可能需要调用 realloc() 所以需要 allocator | |
429 | ||
430 | // 流畅接口(Fluent interface) | |
431 | a.PushBack("Lua", allocator).PushBack("Mio", allocator); | |
432 | ~~~~~~~~~~ | |
433 | ||
434 | 与 STL 不一样的是,`PushBack()`/`PopBack()` 返回 Array 本身的引用。这称为流畅接口(_fluent interface_)。 | |
435 | ||
436 | 如果你想在 Array 中加入一个非常量字符串,或是一个没有足够生命周期的字符串(见 [Create String](#CreateString)),你需要使用 copy-string API 去创建一个 String。为了避免加入中间变量,可以就地使用一个 [临时值](#TemporaryValues): | |
437 | ||
438 | ~~~~~~~~~~cpp | |
439 | // 就地 Value 参数 | |
440 | contact.PushBack(Value("copy", document.GetAllocator()).Move(), // copy string | |
441 | document.GetAllocator()); | |
442 | ||
443 | // 显式 Value 参数 | |
444 | Value val("key", document.GetAllocator()); // copy string | |
445 | contact.PushBack(val, document.GetAllocator()); | |
446 | ~~~~~~~~~~ | |
447 | ||
448 | ## 修改 Object {#ModifyObject} | |
449 | Object 是键值对的集合。每个键必须为 String。要修改 Object,方法是增加或移除成员。以下的 API 用来增加城员: | |
450 | ||
451 | * `Value& AddMember(Value&, Value&, Allocator& allocator)` | |
452 | * `Value& AddMember(StringRefType, Value&, Allocator&)` | |
453 | * `template <typename T> Value& AddMember(StringRefType, T value, Allocator&)` | |
454 | ||
455 | 以下是一个例子。 | |
456 | ||
457 | ~~~~~~~~~~cpp | |
458 | Value contact(kObject); | |
459 | contact.AddMember("name", "Milo", document.GetAllocator()); | |
460 | contact.AddMember("married", true, document.GetAllocator()); | |
461 | ~~~~~~~~~~ | |
462 | ||
463 | 使用 `StringRefType` 作为 name 参数的重载版本与字符串的 `SetString` 的接口相似。 这些重载是为了避免复制 `name` 字符串,因为 JSON object 中经常会使用常数键名。 | |
464 | ||
465 | 如果你需要从非常数字符串或生命周期不足的字符串创建键名(见 [创建 String](#CreateString)),你需要使用 copy-string API。为了避免中间变量,可以就地使用 [临时值](#TemporaryValues): | |
466 | ||
467 | ~~~~~~~~~~cpp | |
468 | // 就地 Value 参数 | |
469 | contact.AddMember(Value("copy", document.GetAllocator()).Move(), // copy string | |
470 | Value().Move(), // null value | |
471 | document.GetAllocator()); | |
472 | ||
473 | // 显式参数 | |
474 | Value key("key", document.GetAllocator()); // copy string name | |
475 | Value val(42); // 某 Value | |
476 | contact.AddMember(key, val, document.GetAllocator()); | |
477 | ~~~~~~~~~~ | |
478 | ||
479 | 移除成员有几个选择: | |
480 | ||
481 | * `bool RemoveMember(const Ch* name)`:使用键名来移除成员(线性时间复杂度)。 | |
482 | * `bool RemoveMember(const Value& name)`:除了 `name` 是一个 Value,和上一行相同。 | |
483 | * `MemberIterator RemoveMember(MemberIterator)`:使用迭代器移除成员(_ 常数 _ 时间复杂度)。 | |
484 | * `MemberIterator EraseMember(MemberIterator)`:和上行相似但维持成员次序(线性时间复杂度)。 | |
485 | * `MemberIterator EraseMember(MemberIterator first, MemberIterator last)`:移除一个范围内的成员,维持次序(线性时间复杂度)。 | |
486 | ||
487 | `MemberIterator RemoveMember(MemberIterator)` 使用了“转移最后”手法来达成常数时间复杂度。基本上就是析构迭代器位置的成员,然后把最后的成员转移至迭代器位置。因此,成员的次序会被改变。 | |
488 | ||
489 | ## 深复制 Value {#DeepCopyValue} | |
490 | 若我们真的要复制一个 DOM 树,我们可使用两个 APIs 作深复制:含 allocator 的构造函数及 `CopyFrom()`。 | |
491 | ||
492 | ~~~~~~~~~~cpp | |
493 | Document d; | |
494 | Document::AllocatorType& a = d.GetAllocator(); | |
495 | Value v1("foo"); | |
496 | // Value v2(v1); // 不容许 | |
497 | ||
498 | Value v2(v1, a); // 制造一个克隆 | |
499 | assert(v1.IsString()); // v1 不变 | |
500 | d.SetArray().PushBack(v1, a).PushBack(v2, a); | |
501 | assert(v1.IsNull() && v2.IsNull()); // 两个都转移动 d | |
502 | ||
503 | v2.CopyFrom(d, a); // 把整个 document 复制至 v2 | |
504 | assert(d.IsArray() && d.Size() == 2); // d 不变 | |
505 | v1.SetObject().AddMember("array", v2, a); | |
506 | d.PushBack(v1, a); | |
507 | ~~~~~~~~~~ | |
508 | ||
509 | ## 交换 Value {#SwapValues} | |
510 | ||
511 | RapidJSON 也提供 `Swap()`。 | |
512 | ||
513 | ~~~~~~~~~~cpp | |
514 | Value a(123); | |
515 | Value b("Hello"); | |
516 | a.Swap(b); | |
517 | assert(a.IsString()); | |
518 | assert(b.IsInt()); | |
519 | ~~~~~~~~~~ | |
520 | ||
521 | 无论两棵 DOM 树有多复杂,交换是很快的(常数时间)。 | |
522 | ||
523 | # 下一部分 {#WhatsNext} | |
524 | ||
525 | 本教程展示了如何询查及修改 DOM 树。RapidJSON 还有一个重要概念: | |
526 | ||
527 | 1. [流](doc/stream.zh-cn.md) 是读写 JSON 的通道。流可以是内存字符串、文件流等。用户也可以自定义流。 | |
528 | 2. [编码](doc/encoding.zh-cn.md) 定义在流或内存中使用的字符编码。RapidJSON 也在内部提供 Unicode 转换及校验功能。 | |
529 | 3. [DOM](doc/dom.zh-cn.md) 的基本功能已在本教程里介绍。还有更高级的功能,如原位(*in situ*)解析、其他解析选项及高级用法。 | |
530 | 4. [SAX](doc/sax.zh-cn.md) 是 RapidJSON 解析/生成功能的基础。学习使用 `Reader`/`Writer` 去实现更高性能的应用程序。也可以使用 `PrettyWriter` 去格式化 JSON。 | |
531 | 5. [性能](doc/performance.zh-cn.md) 展示一些我们做的及第三方的性能测试。 | |
532 | 6. [技术内幕](doc/internals.md) 讲述一些 RapidJSON 内部的设计及技术。 | |
533 | ||
534 | 你也可以参考 [常见问题](doc/faq.zh-cn.md)、API 文档、例子及单元测试。 |