rapidjson官方教程
目錄
- Value 及 Document
- 查詢Value
- 查詢Array
- 查詢Object
- 查詢Number
- 查詢String
- 創建/修改值
- 改變Value類型
- 轉移語意(Move Semantics)
- 轉移語意及臨時值
- 創建String
- 修改Array
- 修改Object
- 深復制Value
- 交換Value
- 下一部分
本教程簡介文件對象模型(Document Object Model, DOM)API。
如用法一覽中所示,可以解析一個JSON至DOM,然后就可以輕松查詢及修改DOM,并最終轉換回JSON。
Value 及 Document
每個JSON值都儲存為Value類,而Document類則表示整個DOM,它存儲了一個DOM樹的根Value。RapidJSON的所有公開類型及函數都在rapidjson命名空間中。
查詢Value
在本節中,我們會使用到example/tutorial/tutorial.cpp中的代碼片段。
假設我們用C語言的字符串儲存一個JSON(const char* 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樹*:
教程中的DOM自從RFC 7159作出更新,合法JSON文件的根可以是任何類型的JSON值。而在較早的RFC 4627中,根值只允許是Object或Array。而在上述例子中,根是一個Object。
assert(document.IsObject());讓我們查詢一下根Object中有沒有"hello"成員。由于一個Value可包含不同類型的值,我們可能需要驗證它的類型,并使用合適的API去獲取其值。在此例中,"hello"成員關聯到一個JSON String。
assert(document.HasMember("hello")); assert(document["hello"].IsString()); printf("hello = %s\n", document["hello"].GetString()); worldJSON True/False值是以bool表示的。
assert(document["t"].IsBool()); printf("t = %s\n", document["t"].GetBool() ?"true" : "false"); trueJSON Null值可用IsNull()查詢。
printf("n = %s\n", document["n"].IsNull() ?"null" : "?"); nullJSON Number類型表示所有數值。然而,C++需要使用更專門的類型。
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()); i = 123 pi = 3.1416JSON 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注意,RapidJSON并不自動轉換各種JSON類型。例如,對一個String的Value調用GetInt()是非法的。在調試模式下,它會被斷言失敗。在發布模式下,其行為是未定義的。
以下將會討論有關查詢各類型的細節。
查詢Array
缺省情況下,SizeType是unsigned的typedef。在多數系統中,Array最多能存儲2^32-1個元素。
你可以用整數字面量訪問元素,如a[0]、a[1]、a[2]。
Array與std::vector相似,除了使用索引,也可使用迭代器來訪問所有元素。
for (Value::ConstValueIterator itr = a.Begin(); itr != a.End(); ++itr) printf("%d ", itr->GetInt());還有一些熟悉的查詢函數:
- SizeType Capacity() const
- bool Empty() const
查詢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 Array注意,當operator[](const char*)找不到成員,它會斷言失敗。
若我們不確定一個成員是否存在,便需要在調用operator[](const char*)前先調用HasMember()。然而,這會導致兩次查找。更好的做法是調用FindMember(),它能同時檢查成員是否存在并返回它的Value:
Value::ConstMemberIterator itr = document.FindMember("hello"); if (itr != document.MemberEnd()) printf("%s %s\n", itr->value.GetString());查詢Number
JSON只提供一種數值類型──Number。數字可以是整數或實數。RFC 4627規定數字的范圍由解析器指定。
由于C++提供多種整數及浮點數類型,DOM嘗試盡量提供最廣的范圍及良好性能。
當解析一個Number時, 它會被存儲在DOM之中,成為下列其中一個類型:
| unsigned | 32位無號整數 |
| int | 32位有號整數 |
| uint64_t | 64位無號整數 |
| int64_t | 64位有號整數 |
| double | 64位雙精度浮點數 |
當查詢一個Number時, 你可以檢查該數字是否能以目標類型來提取:
| bool IsNumber() | 不適用 |
| bool IsUint() | unsigned GetUint() |
| bool IsInt() | int GetInt() |
| bool IsUint64() | uint64_t GetUint64() |
| bool IsInt64() | int64_t GetInt64() |
| bool IsDouble() | double GetDouble() |
注意,一個整數可能用幾種類型來提取,而無需轉換。例如,一個名為x的Value包含123,那么x.IsInt() == x.IsUint() == x.IsInt64() == x.IsUint64() == true。但如果一個名為y的Value包含-3000000000,那么僅會令x.IsInt64() == true。
當要提取Number類型,GetDouble()是會把內部整數的表示轉換成double。注意int?和unsigned可以安全地轉換至double,但int64_t及uint64_t可能會喪失精度(因為double的尾數只有52位)。
查詢String
除了GetString(),Value類也有一個GetStringLength()。這里會解釋個中原因。
根據RFC 4627,JSON String可包含Unicode字符U+0000,在JSON中會表示為"\\u0000"。問題是,C/C++通常使用空字符結尾字符串(null-terminated string),這種字符串把`\0'作為結束符號。
為了符合RFC 4627,RapidJSON支持包含U+0000的String。若你需要處理這些String,便可使用GetStringLength()去獲得正確的字符串長度。
例如,當解析以下的JSON至Document d之后:
{ "s" : "a\u0000b" }"a\\u0000b"值的正確長度應該是3。但strlen()會返回1。
GetStringLength()也可以提高性能,因為用戶可能需要調用strlen()去分配緩沖。
此外,std::string也支持這個構造函數:
string(const char* s, size_t count);此構造函數接受字符串長度作為參數。它支持在字符串中存儲空字符,也應該會有更好的性能。
比較兩個Value
你可使用==及!=去比較兩個Value。當且僅當兩個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。
改變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(Type):
Value o(kObjectType); Value a(kArrayType);轉移語意(Move Semantics)
在設計RapidJSON時有一個非常特別的決定,就是Value賦值并不是把來源Value復制至目的Value,而是把把來源Value轉移(move)至目的Value。例如:
Value a(123); Value b(456); b = a; // a變成Null,b變成數字123。 使用移動語意賦值。為什么?此語意有何優點?
最簡單的答案就是性能。對于固定大小的JSON類型(Number、True、False、Null),復制它們是簡單快捷。然而,對于可變大小的JSON類型(String、Array、Object),復制它們會產生大量開銷,而且這些開銷常常不被察覺。尤其是當我們需要創建臨時Object,把它復制至另一變量,然后再析構它。
例如,若使用正常*復制*語意:
Value o(kObjectType); { Value contacts(kArrayType); // 把元素加進contacts數組。 // ... o.AddMember("contacts", contacts, d.GetAllocator());// 深度復制contacts (可能有大量內存分配) // 析構contacts。 } 復制語意產生大量的復制操作。那個o?Object需要分配一個和contacts相同大小的緩沖區,對conacts做深度復制,并最終要析構contacts。這樣會產生大量無必要的內存分配/釋放,以及內存復制。
有一些方案可避免實質地復制這些數據,例如引用計數(reference counting)、垃圾回收(garbage collection, GC)。
為了使RapidJSON簡單及快速,我們選擇了對賦值采用*轉移*語意。這方法與std::auto_ptr相似,都是在賦值時轉移擁有權。轉移快得多簡單得多,只需要析構原來的Value,把來源memcpy()至目標,最后把來源設置為Null類型。
因此,使用轉移語意后,上面的例子變成:
Value o(kObjectType); { Value contacts(kArrayType); // adding elements to contacts array. o.AddMember("contacts", contacts, d.GetAllocator());// 只需 memcpy() contacts本身至新成員的Value(16字節) // contacts在這里變成Null。它的析構是平凡的。 } 轉移語意不需復制。在C++11中這稱為轉移賦值操作(move assignment operator)。由于RapidJSON 支持C++03,它在賦值操作采用轉移語意,其它修改形函數如AddMember(),?PushBack()也采用轉移語意。
轉移語意及臨時值
有時候,我們想直接構造一個Value并傳遞給一個“轉移”函數(如PushBack()、AddMember())。由于臨時對象是不能轉換為正常的Value引用,我們加入了一個方便的Move()函數:
Value 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);// 和上一行相同創建String
RapidJSON提供兩個String的存儲策略。
Copy-string總是安全的,因為它擁有數據的克隆。Const-string可用于存儲字符串字面量,以及用于在DOM一節中將會提到的in-situ解析中。
為了讓用戶自定義內存分配方式,當一個操作可能需要內存分配時,RapidJSON要求用戶傳遞一個allocator實例作為API參數。此設計避免了在每個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"在此例子中,我們使用Document實例的allocator。這是使用RapidJSON時常用的慣用法。但你也可以用其他allocator實例。
另外,上面的SetString()需要長度參數。這個API能處理含有空字符的字符串。另一個SetString()重載函數沒有長度參數,它假設輸入是空字符結尾的,并會調用類似strlen()的函數去獲取長度。
最后,對于字符串字面量或有安全生命周期的字符串,可以使用const-string版本的SetString(),它沒有allocator參數。對于字符串家面量(或字符數組常量),只需簡單地傳遞字面量,又安全又高效:
Value s; s.SetString("rapidjson");// 可包含空字符,長度在編譯萁推導 s = "rapidjson"; // 上行的縮寫對于字符指針,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); // 上行的縮寫修改Array
Array類型的Value提供與std::vector相似的API。
- Clear()
- 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。
以下是PushBack()的例子:
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中加入一個非常量字符串,或是一個沒有足夠生命周期的字符串(見Create 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());修改Object
Object是鍵值對的集合。每個鍵必須為String。要修改Object,方法是增加或移除成員。以下的API用來增加城員:
- Value& 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中經常會使用常數鍵名。
如果你需要從非常數字符串或生命周期不足的字符串創建鍵名(見創建String),你需要使用copy-string API。為了避免中間變量,可以就地使用臨時值:
// 就地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是一個Value,和上一行相同。
- MemberIterator RemoveMember(MemberIterator):使用迭代器移除成員(_常數_時間復雜度)。
- MemberIterator EraseMember(MemberIterator):和上行相似但維持成員次序(線性時間復雜度)。
- MemberIterator EraseMember(MemberIterator first, MemberIterator last):移除一個范圍內的成員,維持次序(線性時間復雜度)。
MemberIterator RemoveMember(MemberIterator)使用了“轉移最后”手法來達成常數時間復雜度。基本上就是析構迭代器位置的成員,然后把最后的成員轉移至迭代器位置。因此,成員的次序會被改變。
深復制Value
若我們真的要復制一個DOM樹,我們可使用兩個APIs作深復制:含allocator的構造函數及CopyFrom()。
Document 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);交換Value
RapidJSON也提供Swap()。
Value a(123); Value b("Hello"); a.Swap(b); assert(a.IsString()); assert(b.IsInt());無論兩棵DOM樹有多復雜,交換是很快的(常數時間)。
下一部分
本教程展示了如何詢查及修改DOM樹。RapidJSON還有一個重要概念:
你也可以參考常見問題、API文檔、例子及單元測試。
總結
以上是生活随笔為你收集整理的rapidjson官方教程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jsoncpp和rapidjson哪个好
- 下一篇: rapidjson的read和write