Folly dynamic 完全指南:在 C++ 中驾驭运行时动态类型与 JSON 处理
【免费下载链接】follyAn open-source C++ library developed and used at Facebook.项目地址: https://gitcode.com/GitHub_Trending/fol/folly
folly::dynamic是 Meta 开源的 C++ 库 Folly 提供的一种运行时动态类型值,它以接近原生类型的语法在 C++ 中承载 int、double、bool、null、字符串、数组与对象(键值映射)七种类型,行为类似于带运行时类型系统的语言(如 Python)。本文以 folly/docs/Dynamic.md 为骨架,结合 folly/json/dynamic.h 的源码实现与官方示例,系统讲解 dynamic 的构造、运行时类型检查、比较与哈希、迭代与查找、删除、JSON 序列化/反序列化、性能特性与设计权衡,帮助你掌握在 C++ 项目中把 JSON 文档处理做到近乎"脚本语言级"流畅的完整方案。
概述:什么是 folly::dynamic
folly/dynamic.h(实际实现位于 folly/json/dynamic.h,顶层头文件仅为转发 shim)提供了一个运行时动态类型的值,类似于 Python 等运行时类型系统语言的工作方式。它可以保存一个预定类型集合中的任意类型(int、bool、其他 dynamic 组成的数组等),与std::variant类似,但语法上更接近直接使用原生类型。
在源码中,dynamic 的类型集合由 folly/json/dynamic.h 的枚举明确定义,共七种:
enum Type { NULLT, ARRAY, BOOL, DOUBLE, INT64, OBJECT, STRING, };其内部存储采用"类型标记 + 联合体"的设计(见 folly/json/dynamic.h):一个Type type_记录当前类型,一个union Data同时容纳nullptr_t、std::vector<dynamic>数组、bool、double、int64_t、std::string以及为对象预留的F14NodeMap对齐缓冲区。对象之所以用 char 缓冲区 placement new 存放,是因为无法在dynamic尚不完整时直接参数化std::模板。
快速上手示例
以下代码假设已使用了using folly::dynamic;(原文示例,略作注释说明):
dynamic twelve = 12; // 创建持有整数的 dynamic dynamic str = "string"; // 字符串类型,内部是 fbstring // 其他几种类型 dynamic nul = nullptr; dynamic boolean = false; // 数组可以用 dynamic::array 初始化 dynamic array = dynamic::array("array ", "of ", 4, " elements"); assert(array.size() == 4); dynamic emptyArray = dynamic::array; assert(emptyArray.empty()); // dynamic 到 dynamic 的映射称为对象(object)。 // dynamic::object 常量用来创建空的"dynamic 到 dynamic"映射。 dynamic map = dynamic::object; map["something"] = 12; map["another_something"] = map["something"] * 2; // 动态对象也可以这样一次性初始化 dynamic map2 = dynamic::object("something", 12)("another_something", 24);官方示例 folly/docs/examples/folly/dynamic/array.cpp 展示了数组构造的可测试形态:
TEST(dynamic, arrayCtor) { auto a = folly::dynamic::array(123, "hello", nullptr); ASSERT_TRUE(a.isArray()); ASSERT_EQ(a.size(), 3); EXPECT_EQ(a[0], 123); EXPECT_EQ(a[1], "hello"); EXPECT_TRUE(a[2].isNull()); }folly/docs/examples/folly/dynamic/object.cpp 则演示了对象构造,注意对象的键也必须是 dynamic(因此可以出现整数键、null 键):
TEST(dynamic, objectCtor) { folly::dynamic o1 = folly::dynamic::object; folly::dynamic o2 = folly::dynamic::object("key", "value")(1, 2)(nullptr, nullptr); ASSERT_TRUE(o1.isObject()); ASSERT_TRUE(o2.isObject()); ASSERT_EQ(o1.size(), 0); ASSERT_EQ(o2.size(), 3); EXPECT_EQ(o2["key"], "value"); EXPECT_EQ(o2[1], 2); EXPECT_EQ(o2[nullptr], nullptr); }在 folly/json/dynamic.h 的头文件注释中还有一段更紧凑的"动态用法"演示,展示了dynamic接近原生类型的操作体验(++map[str]、map[str + "another_str"]、insert等):
dynamic twelve = 12; dynamic str = "string"; dynamic map = dynamic::object; map[str] = twelve; map[str + "another_str"] = dynamic::array("array", "of", 4, "elements"); map.insert("null_element", nullptr); ++map[str]; assert(map[str] == 13);运行时类型检查与转换
对 dynamic 的任何操作都要求在运行时检查类型与该操作是否兼容。如果不兼容,会抛出folly::TypeError。其他异常也可能被抛出,例如当你把一个很大的 64 位整数塞进 dynamic 后试图按 double 读出来时(见下文 asDouble 的精度说明)。
示例(沿用原文):
dynamic dint = 42; dynamic str = "foo"; dynamic anotherStr = str + "something"; // 没问题 dynamic thisThrows = str + dint; // 抛出 TypeError字符串与字符串相加是合法的(拼接),而字符串与整数相加在运行时类型不兼容,直接抛folly::TypeError——这就是"运行时类型检查"的直观体现。源码中的算术运算符(如operator+=等,见 folly/json/dynamic.h)都会在使用错误类型或类型组合时抛出 TypeError;同时文档明确提示:如果你把无法精确表示的大 64 位整数与 double 混用,这些运算符也可能抛出异常。
显式类型转换
可以对部分基础类型请求显式类型转换(asXxx系列):
dynamic dint = 12345678; dynamic doub = dint.asDouble(); // doub 将持有 12345678.0 dynamic str = dint.asString(); // str == "12345678" dynamic hugeInt = std::numeric_limits<int64_t>::max(); dynamic hugeDoub = hugeInt.asDouble(); // 抛出 folly/Conv.h 相关错误, // 因为它无法装进一个 doubleasString()、asDouble()、asInt()、asBool()这四个转换接口在 folly/json/dynamic.h 中有完整声明。从源码注释(folly/json/dynamic.h)可以确认:C++ 会在 bool、int、double 之间隐式转换,而这些转换函数还尝试在算术类型与字符串之间转换,例如dynamic d = "12"; d.asDouble()会得到12.0。同时也应区分"转换"与"提取"两组 API:
asXxx():带类型转换,尝试把当前值转成目标类型(如字符串 "12" 可转成 12.0),失败时抛异常;getXxx():不带类型转换的严格提取,类型不匹配时抛 TypeError(见 folly/json/dynamic.h)。
对于更复杂的转换需求,参见 folly/docs/DynamicConverter.md,其完整实现位于 folly/json/DynamicConverter.h。
比较运算符与哈希
相等运算符
==与!=对所有类型都支持。同类型的 dynamic 之间使用底层类型的相等运算符;不同数值类型(double 与 int64)之间按数值相等比较,因此2.0 == 2成立;其他不同类型之间的值一律判定为不相等。
源码在 folly/json/dynamic.h 的实现注释中进一步说明:相等比较是深比较(deep equality),会一路比较到底层对象或数组,因此可能较昂贵;int 与 double 之间存在隐式转换,其余不同类型比较恒为 false。operator!=直接由==取反实现。
排序运算符
<、<=、>、>=对所有类型都支持,唯独dynamic::object例外——它参与排序运算会抛出异常(TypeError)。
- 同类型 dynamic 之间使用底层类型的排序运算符;
- 不同数值类型(double 与 int64)之间按数值排序,因此
1.5 < 2成立; - 其他不同类型值之间的排序保持全序(total ordering)性质,且在同一二进制运行内一致,因此可以安全用于
std::set等场景。但实际顺序是未定义的,可能随版本变化,因此除了"同一二进制运行内的全序"这一性质外,不应依赖具体顺序。
从源码(folly/json/dynamic.h)可以看出:>、<=、>=均由operator<派生而来(b < a、!(b < a)、!(a < b)),且注释明确说明对 object 抛 TypeError。
哈希
所有类型都支持哈希,且两个值在dynamic::operator==下相等时,其哈希值必然一致。由此推论:数值类型无论以 int64 还是 double 存储,只要数值相等,哈希就相同——例如std::hash<dynamic>()(2)与std::hash<dynamic>()(2.0)结果一致。
源码中 folly/json/dynamic.h 的hash()注释详细说明了这一保证:int64_t 与 double 在"舍入前数值相等"时产生相同哈希,如整数 2 与浮点 2.0;但不会有 double 故意哈希成只有舍入后才与它相等的值的哈希(例如不会有 double 故意哈希成 INT64_MAX 的哈希,因为 double 无法表示 2^63 - 1 这个值)。std::hash<folly::dynamic>的特化位于 folly/json/dynamic.h,标记为folly_is_avalanching = std::true_type(雪崩式哈希),直接调用d.hash()。
迭代与查找
遍历数组
可以像遍历任何 C++ 序列容器一样遍历 dynamic 数组:
dynamic array = dynamic::array(2, 3, "foo"); for (auto& val : array) { doSomethingWith(val); }数组的迭代器直接解引用为数组元素(见 folly/json/dynamic.h 注释),其底层就是std::vector<dynamic>的迭代器(using iterator = Array::iterator;,见 folly/json/dynamic.h)。
遍历对象:items() / keys() / values()
可以通过items()、keys()、values()遍历对象,其行为与 Python 字典的同名方法相似:
dynamic obj = dynamic::object(2, 3)("hello", "world")("x", 4); for (auto& pair : obj.items()) { // Key 是 pair.first,Value 是 pair.second processKey(pair.first); processValue(pair.second); } for (auto& key : obj.keys()) { processKey(key); } for (auto& value : obj.values()) { processValue(value); }从源码注释(folly/json/dynamic.h)可以确认:这三个方法返回的是IterableProxy迭代器代理,其中items()的元素类型是(const dynamic&, dynamic&)键值对,且对非对象调用会抛 TypeError;keys()、values()、items()均有 const 与非 const 重载。
find() 按键查找
可以使用find()方法在对象中按键查找元素,它返回与items()兼容的迭代器:
dynamic obj = dynamic::object(2, 3)("hello", "world")("x", 4); auto pos = obj.find("hello"); // pos->first 是 "hello" // pos->second 是 "world" auto pos = obj.find("no_such_key"); // pos == obj.items().end()find()的声明见 folly/json/dynamic.h:对非对象调用会抛异常,未找到时返回items().end();同时提供接受StringPiece与接受可转换为 dynamic 的键的模板重载,支持异构查找。与之配套的还有count()(对象中统计键、数组中统计匹配值的个数)与contains()(判断键或值是否存在),见 folly/json/dynamic.h。
其他元素访问方式
在operator[]之外,源码还提供了几种值得了解的访问手段:
at():带边界/存在性检查的访问。对数组越界、对象键不存在会抛std::out_of_range,对非数组/对象抛 TypeError(folly/json/dynamic.h);get_ptr():不抛异常的可空访问,返回指向元素的指针或 nullptr,适合"判断存在 + 取值"一步完成(folly/json/dynamic.h);getDefault():带默认值的查找,只对对象定义,键不存在时返回传入的默认值(folly/json/dynamic.h);setDefault():键不存在时设置默认值并返回其引用,已存在则直接返回现有值的引用(folly/json/dynamic.h);try_get_ptr(json_pointer)/get_ptr(json_pointer):按 JSON Pointer(RFC 6901)路径定位元素(folly/json/dynamic.h)。
删除(Erasure)
可以从 dynamic 数组中删除元素——调用成员函数dynamic::erase,其行为与重载形式类似std::vector::erase;也可以从 dynamic 对象中删除元素——同样调用成员dynamic::erase,行为与重载形式类似std::unordered_map::erase。此外:
- 从 dynamic 数组中"查找并删除"可使用自由函数
erase; - 从 dynamic 数组或对象中"按谓词条件删除"可使用自由函数
erase_if;
它们的行为与重载形式类似 C++20 的erase(std::vector)、erase_if(std::vector)以及erase_if(std::unordered_map)(对应 folly/json/dynamic.h 中的模板声明,原文档参考了 vector/erase2 与 unordered_map/erase_if 的语义)。
从 folly/json/dynamic.h 的源码可见,成员erase有多组重载:
- 按键删除(
erase(StringPiece)/ 模板erase(K&&)),返回删除个数(1 或 0); - 按迭代器或迭代器区间删除(数组的
iterator、对象的const_key_iterator/const_value_iterator/const_item_iterator),返回被删元素之后的首个迭代器; - 数组删除会使被删元素之后的迭代器失效,对象删除会使被删元素的迭代器失效;
- 另有
eraseInto()(folly/json/dynamic.h)可以在删除对象条目的同时把键值移交给回调,适合"边遍历边重组对象"的场景。
用于 JSON:解析、构建与序列化
实现该类型的初衷,就是让 C++ 处理 JSON 文档的难度接近 PHP 或 JavaScript 等动态类型语言。下面是原文给出的完整流程:
// 解析 JSON 字符串并使用它。 std::string jsonDocument = R"({"key":12,"key2":[false, null, true, "yay"]})"; dynamic parsed = folly::parseJson(jsonDocument); assert(parsed["key"] == 12); assert(parsed["key2"][0] == false); assert(parsed["key2"][1] == nullptr); // 用编程方式构建同一份文档。 dynamic sonOfAJ = dynamic::object ("key", 12) ("key2", dynamic::array(false, nullptr, true, "yay")); // 打印输出。(另见 folly::toPrettyJson) auto str = folly::toJson(sonOfAJ); assert(jsonDocument.compare(str) == 0);JSON 相关 API 的声明集中在 folly/json/json.h:
parseJson(StringPiece):把 JSON 文本解析为 dynamic,另有接受json::serialization_opts的版本用于定制解析行为(folly/json/json.h);parseJsonWithMetadata():解析的同时记录每个 dynamic 节点对应的源文本位置等元数据(folly/json/json.h);toJson(dynamic const&):序列化为紧凑 JSON 字符串(folly/json/json.h);toPrettyJson(dynamic const&):带缩进的格式化输出,注意此时所有对象的键会被排序(folly/json/json.h);parseJson5():实验性特性,头文件中已被标记 deprecated,不建议生产环境使用(folly/json/json.h)。
值得注意的两个实现细节:
- JSON 文档中的对象天然映射为 dynamic::object,因为 JSON 对象本身就是"字符串键到值的映射";
- dynamic 对象不仅限于字符串键——构造时键可以是任意 dynamic(参考上文 object 示例中
(1, 2)(nullptr, nullptr)的用法)。不过序列化为 JSON 时,非字符串键会带来限制:dynamic.h 中operator<<的注释(folly/json/dynamic.h)明确指出,只有 dynamic 能"合法地表示一个 JSON 对象"(即键都是字符串)时,对象/数组的打印输出才严格等于 JSON。若想构建 JSON Schema 校验、JSON Pointer 解析等周边能力,可以继续阅读仓库中的 folly/json/JSONSchema.h、folly/json/json_pointer.h 与 folly/json/json_patch.h。
性能考量
动态类型即使发生在 C++ 中,也比静态类型昂贵。不过 Folly 在常见场景下对folly::dynamic及 JSON 的(反)序列化性能做了合理优化:
- 只有数组和对象使用堆:联合体里的标量(null、bool、double、int64)直接内联存储,字符串用
std::string(其本身即小字符串优化),只有std::vector<dynamic>与对象映射(placement new 在缓冲区的 F14 系列节点表)才涉及堆分配; - 移动构造完全支持:
dynamic(dynamic&&) noexcept与dynamic& operator=(dynamic&&) noexcept在头文件中声明(folly/json/dynamic.h),加上赋值运算符对std::vector<bool>::reference等代理类型的专门重载(folly/json/dynamic.h),避免了不少不必要的拷贝; - 字符串格式化内部使用高性能的
folly::to<>(见 folly/Conv.h),而不是走流式 IO。
需要牢记的代价是:sizeof(folly::dynamic)为64 字节。如果你需要分配大量 dynamic(例如作为数组元素密集存放),应优先考虑静态类型,而不是用 dynamic 硬扛。
设计思路答疑(Design Rationale)
Q. 为什么 dynamic 字符串不支持 begin()、end() 和 operator[]?
dynamic 迭代器的 value_type 是dynamic本身,而operator[](或at())必须返回 dynamic 的引用。如果想让字符串支持这些操作,就意味着要支持"字符类型的 dynamic",并且字符串的内部表示要允许把单个字符作为 dynamic 引用暴露给调用方。这在效率上有大量潜在损失,而实践中这种需求并不常见。
Q. 这不就是对 C# 语言特性的拙劣模仿吗?
差不多。(原文以自嘲的口吻承认这一点。)
小结
folly::dynamic为 C++ 提供了一套完整的运行时动态类型与 JSON 处理方案:七种类型通过统一语法承载,算术、比较、哈希按运行时类型分派并保证数值类型间的等价语义;dynamic::array/dynamic::object工厂与链式初始化让文档构建接近字面量;items()/keys()/values()/find()提供脚本风格的遍历查找;配合folly::parseJson/toJson/toPrettyJson完成 JSON 的读写闭环。使用时要留意其运行时类型检查(错误抛folly::TypeError)、64 字节的体积成本,以及对象排序运算会抛异常等边界约束。对于需要灵活处理半结构化数据、JSON 配置或协议报文的 C++ 工程,dynamic 是一个值得纳入工具箱的高性价比选择。
【免费下载链接】follyAn open-source C++ library developed and used at Facebook.项目地址: https://gitcode.com/GitHub_Trending/fol/folly
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考