ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Folly dynamic 完全指南:在 C++ 中驾驭运行时动态类型与 JSON 处理

Folly dynamic 完全指南:在 C++ 中驾驭运行时动态类型与 JSON 处理 Folly dynamic 完全指南在 C 中驾驭运行时动态类型与 JSON 处理【免费下载链接】follyAn open-source C library developed and used at Facebook.项目地址: https://gitcode.com/GitHub_Trending/fol/follyfolly::dynamic是 Meta 开源的 C 库 Folly 提供的一种运行时动态类型值它以接近原生类型的语法在 C 中承载 int、double、bool、null、字符串、数组与对象键值映射七种类型行为类似于带运行时类型系统的语言如 Python。本文以 folly/docs/Dynamic.md 为骨架结合 folly/json/dynamic.h 的源码实现与官方示例系统讲解 dynamic 的构造、运行时类型检查、比较与哈希、迭代与查找、删除、JSON 序列化/反序列化、性能特性与设计权衡帮助你掌握在 C 项目中把 JSON 文档处理做到近乎脚本语言级流畅的完整方案。概述什么是 folly::dynamicfolly/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::vectordynamic数组、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_limitsint64_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。同时也应区分转换与提取两组 APIasXxx()带类型转换尝试把当前值转成目标类型如字符串 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::hashdynamic()(2)与std::hashdynamic()(2.0)结果一致。源码中 folly/json/dynamic.h 的hash()注释详细说明了这一保证int64_t 与 double 在舍入前数值相等时产生相同哈希如整数 2 与浮点 2.0但不会有 double 故意哈希成只有舍入后才与它相等的值的哈希例如不会有 double 故意哈希成 INT64_MAX 的哈希因为 double 无法表示 2^63 - 1 这个值。std::hashfolly::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::vectordynamic的迭代器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.firstValue 是 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)键值对且对非对象调用会抛 TypeErrorkeys()、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对非数组/对象抛 TypeErrorfolly/json/dynamic.hget_ptr()不抛异常的可空访问返回指向元素的指针或 nullptr适合判断存在 取值一步完成folly/json/dynamic.hgetDefault()带默认值的查找只对对象定义键不存在时返回传入的默认值folly/json/dynamic.hsetDefault()键不存在时设置默认值并返回其引用已存在则直接返回现有值的引用folly/json/dynamic.htry_get_ptr(json_pointer)/get_ptr(json_pointer)按 JSON PointerRFC 6901路径定位元素folly/json/dynamic.h。删除Erasure可以从 dynamic 数组中删除元素——调用成员函数dynamic::erase其行为与重载形式类似std::vector::erase也可以从 dynamic 对象中删除元素——同样调用成员dynamic::erase行为与重载形式类似std::unordered_map::erase。此外从 dynamic 数组中查找并删除可使用自由函数erase从 dynamic 数组或对象中按谓词条件删除可使用自由函数erase_if它们的行为与重载形式类似 C20 的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.hparseJson(StringPiece)把 JSON 文本解析为 dynamic另有接受json::serialization_opts的版本用于定制解析行为folly/json/json.hparseJsonWithMetadata()解析的同时记录每个 dynamic 节点对应的源文本位置等元数据folly/json/json.htoJson(dynamic const)序列化为紧凑 JSON 字符串folly/json/json.htoPrettyJson(dynamic const)带缩进的格式化输出注意此时所有对象的键会被排序folly/json/json.hparseJson5()实验性特性头文件中已被标记 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::vectordynamic与对象映射placement new 在缓冲区的 F14 系列节点表才涉及堆分配移动构造完全支持dynamic(dynamic) noexcept与dynamic operator(dynamic) noexcept在头文件中声明folly/json/dynamic.h加上赋值运算符对std::vectorbool::reference等代理类型的专门重载folly/json/dynamic.h避免了不少不必要的拷贝字符串格式化内部使用高性能的folly::to见 folly/Conv.h而不是走流式 IO。需要牢记的代价是sizeof(folly::dynamic)为64 字节。如果你需要分配大量 dynamic例如作为数组元素密集存放应优先考虑静态类型而不是用 dynamic 硬扛。设计思路答疑Design RationaleQ. 为什么 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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进