YAML I/O¶
YAML 簡介¶
YAML 是一種人類可讀的資料序列化語言。完整的 YAML 語言規範可以在 yaml.org 上找到。YAML 最簡單的形式是「純量」、「映射」和「序列」。純量可以是任何數字或字串。井字號 (#) 表示註解行的開始。映射是一組鍵值對,其中鍵以冒號結尾。例如
# a mapping
name: Tom
hat-size: 7
序列是項目列表,其中每個項目都以破折號 ('-') 開頭。例如
# a sequence
- x86
- x86_64
- PowerPC
您可以透過縮排來組合映射和序列。例如,一個映射序列,其中一個映射值本身就是一個序列
# a sequence of mappings with one key's value being a sequence
- name: Tom
cpus:
- x86
- x86_64
- name: Bob
cpus:
- x86
- name: Dan
cpus:
- PowerPC
- x86
有時序列很短,一行一個條目太冗長,因此 YAML 為序列提供了另一種稱為「流程序列」的語法,您可以在其中將以逗號分隔的序列元素放入方括號中。上面的例子可以簡化為
# a sequence of mappings with one key's value being a flow sequence
- name: Tom
cpus: [ x86, x86_64 ]
- name: Bob
cpus: [ x86 ]
- name: Dan
cpus: [ PowerPC, x86 ]
YAML I/O 簡介¶
使用縮排使 YAML 易於人類閱讀和理解,但讓程式讀取和寫入 YAML 涉及許多繁瑣的細節。YAML I/O 函式庫構建並簡化了 YAML 文件的讀取和寫入。
YAML I/O 假設您有一些「原生」資料結構,您希望能夠將其轉儲為 YAML 並從 YAML 重新建立。第一步是嘗試為您的資料結構編寫 YAML 範例。在查看可能的 YAML 表示形式後,您可能會發現將資料結構直接映射到 YAML 並不容易閱讀。通常,欄位的順序與人類認為可讀的順序不同。或者,相同的信息會在多個位置重複,這使得人類難以正確編寫此類 YAML。
在關係資料庫理論中,有一個稱為正規化的設計步驟,您可以在其中重新組織欄位和表格。設計 YAML 編碼時需要考慮相同的因素。但是,您可能不想更改現有的原生資料結構。因此,在寫入 YAML 時可能會有一個正規化步驟,而在讀取 YAML 時會有一個相應的反正規化步驟。
YAML I/O 採用非侵入式、基於 traits 的設計。YAML I/O 定義了一些抽象的基礎模板。您可以針對您的資料類型專門化這些模板。例如,如果您有一個名為 FooBar 的列舉類型,則可以在該類型上專門化 ScalarEnumerationTraits 並定義 enumeration() 方法
using llvm::yaml::ScalarEnumerationTraits;
using llvm::yaml::IO;
template <>
struct ScalarEnumerationTraits<FooBar> {
static void enumeration(IO &io, FooBar &value) {
...
}
};
與所有 YAML I/O 模板特化一樣,ScalarEnumerationTraits 用於讀取和寫入 YAML。也就是說,記憶體中列舉值和 YAML 字串表示形式之間的映射只在一個地方。這確保用於寫入和解析 YAML 的代碼保持同步。
要指定 YAML 映射,您可以在 llvm::yaml::MappingTraits 上定義一個特化。如果您的原生資料結構恰好是一個已經標準化的結構,則特化很簡單。例如
using llvm::yaml::MappingTraits;
using llvm::yaml::IO;
template <>
struct MappingTraits<Person> {
static void mapping(IO &io, Person &info) {
io.mapRequired("name", info.name);
io.mapOptional("hat-size", info.hatSize);
}
};
如果您的資料類型具有 begin()/end() 迭代器和 push_back() 方法,則會自動推斷出 YAML 序列。因此,任何 STL 容器(例如 std::vector<>)都將自動轉換為 YAML 序列。
為資料類型定義特化後,您就可以以程式設計方式使用 YAML I/O 來寫入 YAML 文件
using llvm::yaml::Output;
Person tom;
tom.name = "Tom";
tom.hatSize = 8;
Person dan;
dan.name = "Dan";
dan.hatSize = 7;
std::vector<Person> persons;
persons.push_back(tom);
persons.push_back(dan);
Output yout(llvm::outs());
yout << persons;
這會寫入以下內容
- name: Tom
hat-size: 8
- name: Dan
hat-size: 7
您也可以使用以下代碼讀取此類 YAML 文件
using llvm::yaml::Input;
typedef std::vector<Person> PersonList;
std::vector<PersonList> docs;
Input yin(document.getBuffer());
yin >> docs;
if ( yin.error() )
return;
// Process read document
for ( PersonList &pl : docs ) {
for ( Person &person : pl ) {
cout << "name=" << person.name;
}
}
YAML 的另一個特性是能夠在單個文件中定義多個文件。這就是讀取 YAML 會產生文件類型向量的原因。
錯誤處理¶
解析 YAML 文件時,如果輸入與您的結構定義(如您的 XxxTraits<> 特化中所表達)不匹配,則 YAML I/O 將列印一條錯誤消息,並且您的 Input 物件的 error() 方法將返回 true。例如,以下文件
- name: Tom
shoe-size: 12
- name: Dan
hat-size: 7
具有一個未在結構定義中定義的鍵(shoe-size)。YAML I/O 將自動產生此錯誤
YAML:2:2: error: unknown key 'shoe-size'
shoe-size: 12
^~~~~~~~~
其他不符合結構定義的輸入也會產生類似的錯誤。
純量¶
YAML 純量只是字串(即不是序列或映射)。YAML I/O 庫提供在 YAML 純量和特定 C++ 類型之間轉換的支援。
內建類型¶
以下類型在 YAML I/O 中具有內建支援
bool
float
double
StringRef
std::string
int64_t
int32_t
int16_t
int8_t
uint64_t
uint32_t
uint16_t
uint8_t
也就是說,您可以在 MappingTraits 的欄位中使用這些類型,或者將其用作序列中的元素類型。讀取時,YAML I/O 將驗證找到的字串是否可轉換為該類型,如果不可轉換,則會出錯。
唯一類型¶
假設 YAML I/O 是基於 trait 的,則選擇如何將資料轉換為 YAML 的依據是資料的類型。但在 C++ 類型匹配中,typedef 不會產生唯一的類型名稱。這意味著如果您有兩個 unsigned int 的 typedef,則對於 YAML I/O 來說,這兩種類型看起來都與 unsigned int 完全一樣。為了便於建立唯一的類型名稱,YAML I/O 提供了一個宏,它像內建類型上的 typedef 一樣使用,但會擴展為建立一個具有與基類型之間的轉換運算符的類別。例如
LLVM_YAML_STRONG_TYPEDEF(uint32_t, MyFooFlags)
LLVM_YAML_STRONG_TYPEDEF(uint32_t, MyBarFlags)
這會產生兩個類別 MyFooFlags 和 MyBarFlags,您可以在原生資料結構中使用它們來代替 uint32_t。它們會隱式轉換為 uint32_t 和從 uint32_t 轉換。建立這些唯一類型的意義在於,您現在可以在它們上指定 trait 以獲得不同的 YAML 轉換。
十六進制類型¶
YAML I/O 提供了固定大小的無符號整數,它們以十六進制而不是內建整數類型使用的十進制格式寫入 YAML I/O,這是一個獨特類型使用的例子。
Hex64
Hex32
Hex16
Hex8
您可以使用 llvm::yaml::Hex32 代替 uint32_t,唯一的區別是當 YAML I/O 寫入該類型時,它將以十六進制格式化。
ScalarEnumerationTraits¶
YAML I/O 支援在記憶體列舉和 YAML 文件中的一組字串值之間進行轉換。這是通過專門化您的列舉類型上的 ScalarEnumerationTraits<> 並定義一個 enumeration() 方法來完成的。例如,假設您有一個 CPU 的列舉和一個包含它的結構體作為欄位
enum CPUs {
cpu_x86_64 = 5,
cpu_x86 = 7,
cpu_PowerPC = 8
};
struct Info {
CPUs cpu;
uint32_t flags;
};
為了支援讀取和寫入此列舉,您可以在 CPU 上定義一個 ScalarEnumerationTraits 專門化,然後它可以用作欄位類型
using llvm::yaml::ScalarEnumerationTraits;
using llvm::yaml::MappingTraits;
using llvm::yaml::IO;
template <>
struct ScalarEnumerationTraits<CPUs> {
static void enumeration(IO &io, CPUs &value) {
io.enumCase(value, "x86_64", cpu_x86_64);
io.enumCase(value, "x86", cpu_x86);
io.enumCase(value, "PowerPC", cpu_PowerPC);
}
};
template <>
struct MappingTraits<Info> {
static void mapping(IO &io, Info &info) {
io.mapRequired("cpu", info.cpu);
io.mapOptional("flags", info.flags, 0);
}
};
當讀取 YAML 時,如果找到的字串與 enumCase() 方法指定的任何字串不匹配,則會自動產生錯誤。當寫入 YAML 時,如果要寫入的值與 enumCase() 方法指定的任何值不匹配,則會觸發執行時斷言。
BitValue¶
C++ 中另一個常見的資料結構是一個欄位,其中每個位元都有一個獨特的含義。這通常用於「標誌」欄位。YAML I/O 支援將此類欄位轉換為流程序列。例如,假設您定義了以下位元標誌
enum {
flagsPointy = 1
flagsHollow = 2
flagsFlat = 4
flagsRound = 8
};
LLVM_YAML_STRONG_TYPEDEF(uint32_t, MyFlags)
為了支援 MyFlags 的讀取和寫入,您可以在 MyFlags 上專門化 ScalarBitSetTraits<> 並提供位元值及其名稱。
using llvm::yaml::ScalarBitSetTraits;
using llvm::yaml::MappingTraits;
using llvm::yaml::IO;
template <>
struct ScalarBitSetTraits<MyFlags> {
static void bitset(IO &io, MyFlags &value) {
io.bitSetCase(value, "hollow", flagHollow);
io.bitSetCase(value, "flat", flagFlat);
io.bitSetCase(value, "round", flagRound);
io.bitSetCase(value, "pointy", flagPointy);
}
};
struct Info {
StringRef name;
MyFlags flags;
};
template <>
struct MappingTraits<Info> {
static void mapping(IO &io, Info& info) {
io.mapRequired("name", info.name);
io.mapRequired("flags", info.flags);
}
};
透過上述操作,YAML I/O(在寫入時)將針對 flags 欄位測試掩碼位元集特徵中的每個值,並且每個匹配的值將導致相應的字串被添加到流程序列中。讀取時會執行相反的操作,任何未知的字串值都會導致錯誤。透過上述模式,一個相同的有效 YAML 文件是
name: Tom
flags: [ pointy, flat ]
有時,「標誌」欄位可能包含由位元遮罩定義的列舉部分。
enum {
flagsFeatureA = 1,
flagsFeatureB = 2,
flagsFeatureC = 4,
flagsCPUMask = 24,
flagsCPU1 = 8,
flagsCPU2 = 16
};
為了支援讀取和寫入此類欄位,您需要使用 maskedBitSet() 方法並提供位元值、它們的名稱和列舉遮罩。
template <>
struct ScalarBitSetTraits<MyFlags> {
static void bitset(IO &io, MyFlags &value) {
io.bitSetCase(value, "featureA", flagsFeatureA);
io.bitSetCase(value, "featureB", flagsFeatureB);
io.bitSetCase(value, "featureC", flagsFeatureC);
io.maskedBitSetCase(value, "CPU1", flagsCPU1, flagsCPUMask);
io.maskedBitSetCase(value, "CPU2", flagsCPU2, flagsCPUMask);
}
};
YAML I/O(在寫入時)將對 flags 欄位應用列舉遮罩,並比較結果和位元集中的值。與常規位元集的情況一樣,每個匹配的值都將導致將相應的字串添加到流程序列中。
自定義標量¶
有時為了便於閱讀,需要以自定義方式格式化標量。例如,您的內部資料結構可能使用整數表示時間(自某個紀元以來的秒數),但在 YAML 中,以某種時間格式(例如 2012 年 5 月 4 日晚上 10:30)表示該整數會好得多。YAML I/O 可以透過專門化您資料類型上的 ScalarTraits<> 來支援自定義格式化和解析標量類型。寫入時,YAML I/O 將提供原生類型,並且您的專門化必須創建一個臨時的 llvm::StringRef。讀取時,YAML I/O 將提供一個標量的 llvm::StringRef,並且您的專門化必須將其轉換為您的原生資料類型。自定義標量類型的概要如下所示
using llvm::yaml::ScalarTraits;
using llvm::yaml::IO;
template <>
struct ScalarTraits<MyCustomType> {
static void output(const MyCustomType &value, void*,
llvm::raw_ostream &out) {
out << value; // do custom formatting here
}
static StringRef input(StringRef scalar, void*, MyCustomType &value) {
// do custom parsing here. Return the empty string on success,
// or an error message on failure.
return StringRef();
}
// Determine if this scalar needs quotes.
static QuotingType mustQuote(StringRef) { return QuotingType::Single; }
};
區塊標量¶
YAML 區塊標量是使用文字區塊符號在 YAML 中表示的字串文字,就像下面顯示的示例一樣
text: |
First line
Second line
YAML I/O 函式庫允許您將 BlockScalarTraits<> 特化為您的資料型別,藉此提供在 YAML 區塊純量與特定 C++ 型別之間轉換的支援。該函式庫不為 std::string 和 llvm::StringRef 等型別提供任何內建的區塊純量 I/O 支援,因為 YAML I/O 已經支援這些型別,並且預設使用一般的純量標記法。
BlockScalarTraits 特化的方式與 ScalarTraits 特化非常相似 - YAML I/O 將提供原生型別,而您的特化在寫入時必須建立一個暫時的 llvm::StringRef,並且它也會提供一個具有該區塊純量值的 llvm::StringRef,而您的特化在讀取時必須將其轉換為您的原生資料型別。自訂型別與 BlockScalarTraits 的適當特化的範例如下所示
using llvm::yaml::BlockScalarTraits;
using llvm::yaml::IO;
struct MyStringType {
std::string Str;
};
template <>
struct BlockScalarTraits<MyStringType> {
static void output(const MyStringType &Value, void *Ctxt,
llvm::raw_ostream &OS) {
OS << Value.Str;
}
static StringRef input(StringRef Scalar, void *Ctxt,
MyStringType &Value) {
Value.Str = Scalar.str();
return StringRef();
}
};
映射¶
要將您的型別 T 轉換為 YAML 映射或從 YAML 映射轉換,您必須在 T 上特化 llvm::yaml::MappingTraits 並實作「void mapping(IO &io, T&)」方法。如果您的原生資料結構在任何地方都使用指向類別的指標,則您可以特化為類別指標。範例
using llvm::yaml::MappingTraits;
using llvm::yaml::IO;
// Example of struct Foo which is used by value
template <>
struct MappingTraits<Foo> {
static void mapping(IO &io, Foo &foo) {
io.mapOptional("size", foo.size);
...
}
};
// Example of struct Bar which is natively always a pointer
template <>
struct MappingTraits<Bar*> {
static void mapping(IO &io, Bar *&bar) {
io.mapOptional("size", bar->size);
...
}
};
在某些情況下,我們希望允許將整個映射作為列舉讀取。例如,假設某些配置選項最初是一個列舉。然後它變得更加複雜,所以現在是一個映射。但是有必要支援舊的配置文件。在這種情況下,請添加一個函數 enumInput
,例如 ScalarEnumerationTraits::enumeration
。範例
struct FooBarEnum {
int Foo;
int Bar;
bool operator==(const FooBarEnum &R) const {
return Foo == R.Foo && Bar == R.Bar;
}
};
template <> struct MappingTraits<FooBarEnum> {
static void enumInput(IO &io, FooBarEnum &Val) {
io.enumCase(Val, "OnlyFoo", FooBarEnum({1, 0}));
io.enumCase(Val, "OnlyBar", FooBarEnum({0, 1}));
}
static void mapping(IO &io, FooBarEnum &Val) {
io.mapOptional("Foo", Val.Foo);
io.mapOptional("Bar", Val.Bar);
}
};
無正規化¶
mapping()
方法負責在需要時進行正規化和反正規化。在原生資料結構不需要正規化的簡單情況下,mapping 方法只使用 mapOptional() 或 mapRequired() 將結構的欄位綁定到 YAML 鍵名。例如
using llvm::yaml::MappingTraits;
using llvm::yaml::IO;
template <>
struct MappingTraits<Person> {
static void mapping(IO &io, Person &info) {
io.mapRequired("name", info.name);
io.mapOptional("hat-size", info.hatSize);
}
};
正規化¶
當需要 [反]正規化時,mapping() 方法需要一種方法來存取正規化值作為欄位。為了幫助實現這一點,我們提供了一個範本 MappingNormalization<>,您可以使用它自動執行正規化和反正規化。該範本用於在您的 mapping() 方法中建立一個局部變數,其中包含正規化的鍵。
假設您有一個原生資料型別 Polar,它以極座標 (距離、角度) 指定位置
struct Polar {
float distance;
float angle;
};
但您決定正規化的 YAML 應該使用 x,y 座標。也就是說,您希望 yaml 看起來像這樣
x: 10.3
y: -4.7
您可以透過定義一個 MappingTraits 來支援這一點,該 MappingTraits 在寫入 YAML 時將極座標正規化為 x,y 座標,並在讀取 YAML 時將 x,y 座標反正規化為極座標。
using llvm::yaml::MappingTraits;
using llvm::yaml::IO;
template <>
struct MappingTraits<Polar> {
class NormalizedPolar {
public:
NormalizedPolar(IO &io)
: x(0.0), y(0.0) {
}
NormalizedPolar(IO &, Polar &polar)
: x(polar.distance * cos(polar.angle)),
y(polar.distance * sin(polar.angle)) {
}
Polar denormalize(IO &) {
return Polar(sqrt(x*x+y*y), arctan(x,y));
}
float x;
float y;
};
static void mapping(IO &io, Polar &polar) {
MappingNormalization<NormalizedPolar, Polar> keys(io, polar);
io.mapRequired("x", keys->x);
io.mapRequired("y", keys->y);
}
};
在寫入 YAML 時,局部變數「keys」將是一個堆疊分配的 NormalizedPolar 實例,它由提供的 polar 物件建構,該物件初始化其 x 和 y 欄位。然後,mapRequired() 方法將 x 和 y 值作為鍵/值對寫出。
在讀取 YAML 時,局部變數「keys」將是一個堆疊分配的 NormalizedPolar 實例,由空建構函數建構。mapRequired 方法將在 YAML 文檔中找到匹配的鍵,並填寫 NormalizedPolar 物件 keys 的 x 和 y 欄位。在 mapping() 方法結束時,當局部 keys 變數超出範圍時,將自動調用 denormalize() 方法將讀取的值轉換回極座標,然後再分配給 mapping() 的第二個參數。
在某些情況下,標準化類別可能是原生類別的子類別,並且可以通過 denormalize() 方法返回,但臨時標準化實例是在堆棧上分配的。在這些情況下,可以使用工具範本 MappingNormalizationHeap<> 來代替。它就像 MappingNormalization<> 一樣,只是它在讀取 YAML 時會在堆積上分配標準化物件。它永遠不會銷毀標準化物件。denormalize() 方法可以返回「this」。
預設值¶
在 mapping() 方法中,呼叫 io.mapRequired() 表示解析 YAML 文件時該鍵必須存在,否則 YAML I/O 將發出錯誤。
另一方面,使用 io.mapOptional() 註冊的鍵允許在讀取的 YAML 文件中不存在。那麼,對於那些可選鍵,欄位中放入什麼值呢?這些可選欄位如何填寫分為兩個步驟。首先,mapping() 方法的第二個參數是對原生類別的引用。該原生類別必須具有預設建構函式。預設建構函式最初為可選欄位設置的任何值都將成為該欄位的值。其次,mapOptional() 方法有一個可選的第三個參數。如果提供,則它是 mapOptional() 應該設置該欄位的值(如果 YAML 文件沒有該鍵)。
這兩種方式(預設建構函式和 mapOptional 的第三個參數)之間有一個重要的區別。當 YAML I/O 生成 YAML 文件時,如果使用了 mapOptional() 的第三個參數,並且要寫入的實際值與(使用 ==)預設值相同,則不會寫入該鍵/值對。
鍵的順序¶
寫入 YAML 文件時,鍵的寫入順序與在 mapping() 方法中呼叫 mapRequired()/mapOptional() 的順序相同。這使您有機會以人類讀者認為自然的順序寫入欄位。這可能與原生類別中欄位的順序不同。
讀取 YAML 文件時,文件中的鍵可以按任意順序排列,但它們的處理順序與在 mapping() 方法中呼叫 mapRequired()/mapOptional() 的順序相同。這啟用了一些有趣的功能。例如,如果綁定的第一個欄位是 cpu,第二個欄位是 flags,並且 flags 是特定於 cpu 的,則您可以根據 cpu 以程式設計方式切換 flags 如何轉換為 YAML 和從 YAML 轉換。這適用於讀取和寫入。例如
using llvm::yaml::MappingTraits;
using llvm::yaml::IO;
struct Info {
CPUs cpu;
uint32_t flags;
};
template <>
struct MappingTraits<Info> {
static void mapping(IO &io, Info &info) {
io.mapRequired("cpu", info.cpu);
// flags must come after cpu for this to work when reading yaml
if ( info.cpu == cpu_x86_64 )
io.mapRequired("flags", *(My86_64Flags*)info.flags);
else
io.mapRequired("flags", *(My86Flags*)info.flags);
}
};
驗證¶
有時在 YAML 映射中,每個鍵/值對都是有效的,但組合起來無效。這類似於沒有語法錯誤,但仍然有語義錯誤。為了支援語義級別的檢查,YAML I/O 允許在 MappingTraits 範本特化中使用可選的 validate()
方法。
解析 YAML 時,會在處理完映射中的所有鍵/值對「之後」才呼叫 validate()
方法。 validate()
方法在輸入過程中返回的任何錯誤訊息,都會像列印語法錯誤一樣被列印出來。寫入 YAML 時,會在寫入 YAML 鍵/值對「之前」呼叫 validate()
方法。輸出過程中的任何錯誤都會觸發 assert()
,因為結構值無效是程式設計錯誤。
using llvm::yaml::MappingTraits;
using llvm::yaml::IO;
struct Stuff {
...
};
template <>
struct MappingTraits<Stuff> {
static void mapping(IO &io, Stuff &stuff) {
...
}
static std::string validate(IO &io, Stuff &stuff) {
// Look at all fields in 'stuff' and if there
// are any bad values return a string describing
// the error. Otherwise return an empty string.
return std::string{};
}
};
流程映射¶
YAML「流程映射」是一種映射,在寫入 YAML 時使用內嵌表示法(例如 { x: 1, y: 0 })。若要指定在 YAML 中使用流程映射寫入類型,您的 MappingTraits 特製化應該加入「static const bool flow = true;」。例如:
using llvm::yaml::MappingTraits;
using llvm::yaml::IO;
struct Stuff {
...
};
template <>
struct MappingTraits<Stuff> {
static void mapping(IO &io, Stuff &stuff) {
...
}
static const bool flow = true;
}
流程映射會根據 Output 物件的設定進行斷行。
序列¶
若要將您的類型 T 轉換為 YAML 序列或從 YAML 序列轉換,您必須在 T 上特製化 llvm::yaml::SequenceTraits 並實作兩個方法:size_t size(IO &io, T&)
和 T::value_type& element(IO &io, T&, size_t indx)
。例如:
template <>
struct SequenceTraits<MySeq> {
static size_t size(IO &io, MySeq &list) { ... }
static MySeqEl &element(IO &io, MySeq &list, size_t index) { ... }
};
size() 方法會傳回序列中目前有多少個元素。 element() 方法會傳回序列中第 i 個元素的參考。解析 YAML 時,可能會使用比目前大小大一的索引來呼叫 element() 方法。您的 element() 方法應該為另一個元素配置空間(如果元素是 C++ 物件,則使用預設建構函式),並傳回該新配置空間的參考。
流程序列¶
YAML「流程序列」是一種序列,在寫入 YAML 時使用內嵌表示法(例如 [ foo, bar ])。若要指定序列類型應該在 YAML 中寫成流程序列,您的 SequenceTraits 特製化應該加入「static const bool flow = true;」。例如:
template <>
struct SequenceTraits<MyList> {
static size_t size(IO &io, MyList &list) { ... }
static MyListEl &element(IO &io, MyList &list, size_t index) { ... }
// The existence of this member causes YAML I/O to use a flow sequence
static const bool flow = true;
};
在上述情況下,如果您在原生資料結構中使用 MyList 作為資料類型,則在轉換為 YAML 時,將使用整數的流程序列(例如 [ 10, -3, 4 ])。
流程序列會根據 Output 物件的設定進行斷行。
工具巨集¶
由於序列的常見來源是 std::vector<>,因此 YAML I/O 提供了巨集:LLVM_YAML_IS_SEQUENCE_VECTOR() 和 LLVM_YAML_IS_FLOW_SEQUENCE_VECTOR(),可用於輕鬆地在 std::vector 類型上指定 SequenceTraits<>。YAML I/O 不會在 std::vector<> 上部分特製化 SequenceTraits,因為這會強制所有向量都成為序列。巨集的使用範例:
std::vector<MyType1>;
std::vector<MyType2>;
LLVM_YAML_IS_SEQUENCE_VECTOR(MyType1)
LLVM_YAML_IS_FLOW_SEQUENCE_VECTOR(MyType2)
文件清單¶
YAML 允許您在單一 YAML 檔案中定義多個「文件」。每個新文件都以靠左對齊的「—」權杖開始。所有文件的結尾都以靠左對齊的「…」權杖表示。許多 YAML 使用者永遠不需要多個文件。其 YAML 綱要中的頂層節點將是映射或序列。對於這些情況,不需要以下內容。但如果您確實需要多個文件,則可以為您的文件清單類型指定特性。該特性具有與 SequenceTraits 相同的方法,但名稱為 DocumentListTraits。例如:
template <>
struct DocumentListTraits<MyDocList> {
static size_t size(IO &io, MyDocList &list) { ... }
static MyDocType element(IO &io, MyDocList &list, size_t index) { ... }
};
使用者情境資料¶
當建立 llvm::yaml::Input 或 llvm::yaml::Output 物件時,它們的建構函式會接受一個可選的「情境」參數。這是一個指向您可能需要的任何狀態資訊的指標。
舉例來說,在先前的範例中,我們展示了如何根據映射中另一個欄位的值在執行時期決定旗標欄位的轉換類型。但如果內部映射需要知道外部映射的某些欄位值呢?這就是「情境」參數的用武之地。您可以在外部映射的 mapping() 方法中設定情境中的值,並在內部映射的 mapping() 方法中取得這些值。
情境值只是一個 void*。所有使用情境並對您的原生資料類型進行操作的特徵,都需要協調情境值的實際意義。它可以是指向物件或結構的指標,您的各種特徵可以使用它來共用情境相關資訊。
輸出¶
llvm::yaml::Output 類別用於根據您的記憶體資料結構產生 YAML 文件,使用在您的資料類型上定義的特徵。若要實例化 Output 物件,您需要一個 llvm::raw_ostream、一個可選的情境指標和一個可選的換行欄位
class Output : public IO {
public:
Output(llvm::raw_ostream &, void *context = NULL, int WrapColumn = 70);
擁有 Output 物件後,您可以對其使用 C++ 串流運算子,將您的原生資料寫入 YAML。需要注意的一件事是,一個 YAML 檔案可以包含多個「文件」。如果您要串流為 YAML 的頂層資料結構是映射、純量或序列,則 Output 會假設您正在產生一個文件,並將映射輸出以「---
」和尾隨的「...
」包覆。
WrapColumn 參數會在流程映射和序列超過提供的欄位時,使它們換行。傳遞 0 以完全抑制換行。
using llvm::yaml::Output;
void dumpMyMapDoc(const MyMapType &info) {
Output yout(llvm::outs());
yout << info;
}
以上內容可能會產生如下輸出
---
name: Tom
hat-size: 7
...
另一方面,如果您要串流為 YAML 的頂層資料結構具有 DocumentListTraits 特製化,則 Output 會走訪您的 DocumentList 中的每個元素,並在每個元素開始之前產生「—」,並以「…」結尾。
using llvm::yaml::Output;
void dumpMyMapDoc(const MyDocListType &docList) {
Output yout(llvm::outs());
yout << docList;
}
以上內容可能會產生如下輸出
---
name: Tom
hat-size: 7
---
name: Tom
shoe-size: 11
...
輸入¶
llvm::yaml::Input 類別用於將 YAML 文件解析為您的原生資料結構。若要實例化 Input 物件,您需要一個指向整個 YAML 檔案的 StringRef,以及一個可選的情境指標
class Input : public IO {
public:
Input(StringRef inputContent, void *context=NULL);
擁有 Input 物件後,您可以使用 C++ 串流運算子來讀取文件。如果您預期一個檔案中可能有多個 YAML 文件,您需要在文件類型列表上特製化 DocumentListTraits 並串流該文件列表類型。否則,您可以直接串流文件類型。此外,您可以透過呼叫 Input 物件上的 error() 方法來檢查 YAML 中是否有任何語法錯誤。例如
// Reading a single document
using llvm::yaml::Input;
Input yin(mb.getBuffer());
// Parse the YAML file
MyDocType theDoc;
yin >> theDoc;
// Check for error
if ( yin.error() )
return;
// Reading multiple documents in one file
using llvm::yaml::Input;
LLVM_YAML_IS_DOCUMENT_LIST_VECTOR(MyDocType)
Input yin(mb.getBuffer());
// Parse the YAML file
std::vector<MyDocType> theDocList;
yin >> theDocList;
// Check for error
if ( yin.error() )
return;