原址
本文是《从零开始的 JSON 库教程》的第八个单元。代码位于 json-tutorial/tutorial08
(题图Photo by Rob Lambert


1. 对象键值查询

我们在第六个单元实现了 JSON 对象的数据结构,它仅为一个 lept_value 的数组:

  1. struct lept_value {
  2. union {
  3. struct { lept_member* m; size_t size; }o;
  4. /* ... */
  5. }u;
  6. lept_type type;
  7. };
  8. struct lept_member {
  9. char* k; size_t klen; /* member key string, key string length */
  10. lept_value v; /* member value */
  11. };

为了做相应的解析测试,我们实现了最基本的查询功能:

  1. size_t lept_get_object_size(const lept_value* v);
  2. const char* lept_get_object_key(const lept_value* v, size_t index);
  3. size_t lept_get_object_key_length(const lept_value* v, size_t index);
  4. lept_value* lept_get_object_value(lept_value* v, size_t index);

在实际使用时,我们许多时候需要查询一个键值是否存在,如存在,要获得其相应的值。我们可以提供一个函数,简单地用线性搜寻实现这个查询功能(时间复杂度 从零开始的 JSON 库教程(八):访问与其他功能 - 图1 ):

  1. #define LEPT_KEY_NOT_EXIST ((size_t)-1)
  2. size_t lept_find_object_index(const lept_value* v, const char* key, size_t klen) {
  3. size_t i;
  4. assert(v != NULL && v->type == LEPT_OBJECT && key != NULL);
  5. for (i = 0; i < v->u.o.size; i++)
  6. if (v->u.o.m[i].klen == klen && memcmp(v->u.o.m[i].k, key, klen) == 0)
  7. return i;
  8. return LEPT_KEY_NOT_EXIST;
  9. }}

若对象内没有所需的键,此函数返回 LEPT_KEY_NOT_EXIST。使用时:

  1. lept_value o;
  2. size_t index;
  3. lept_init(&o);
  4. lept_parse(&o, "{\"name\":\"Milo\", \"gender\":\"M\"}");
  5. index = lept_find_object_index(&o, "name", 4);
  6. if (index != LEPT_KEY_NOT_EXIST) {
  7. lept_value* v = lept_get_object_value(&o, index);
  8. printf("%s\n", lept_get_string(v));
  9. }
  10. lept_free(&o);

由于一般也是希望获取键对应的值,而不需要索引,我们再加入一个辅助函数,返回类型改为 lept_value*:

  1. lept_value* lept_find_object_value(lept_value* v, const char* key, size_t klen) {
  2. size_t index = lept_find_object_index(v, key, klen);
  3. return index != LEPT_KEY_NOT_EXIST ? &v->u.o.m[index].v : NULL;
  4. }

上述例子便可简化为:

  1. lept_value o, *v;
  2. /* ... */
  3. if ((v = lept_find_object_value(&o, "name", 4)) != NULL)
  4. printf("%s\n", lept_get_string(v));

2. 相等比较

在实现数组和对象的修改之前,为了测试结果的正确性,我们先实现 lept_value 的相等比较(equality comparison)。首先,两个值的类型必须相同,对于 true、false、null 这三种类型,比较类型后便完成比较。而对于数字和字符串,需进一步检查是否相等:

  1. int lept_is_equal(const lept_value* lhs, const lept_value* rhs) {
  2. assert(lhs != NULL && rhs != NULL);
  3. if (lhs->type != rhs->type)
  4. return 0;
  5. switch (lhs->type) {
  6. case LEPT_STRING:
  7. return lhs->u.s.len == rhs->u.s.len &&
  8. memcmp(lhs->u.s.s, rhs->u.s.s, lhs->u.s.len) == 0;
  9. case LEPT_NUMBER:
  10. return lhs->u.n == rhs->u.n;
  11. /* ... */
  12. default:
  13. return 1;
  14. }
  15. }

由于值可能复合类型(数组和对象),也就是一个树形结构。当我们要比较两个树是否相等,可通过递归实现。例如,对于数组,我们先比较元素数目是否相等,然后递归检查对应的元素是否相等:

  1. int lept_is_equal(const lept_value* lhs, const lept_value* rhs) {
  2. size_t i;
  3. /* ... */
  4. switch (lhs->type) {
  5. /* ... */
  6. case LEPT_ARRAY:
  7. if (lhs->u.a.size != rhs->u.a.size)
  8. return 0;
  9. for (i = 0; i < lhs->u.a.size; i++)
  10. if (!lept_is_equal(&lhs->u.a.e[i], &rhs->u.a.e[i]))
  11. return 0;
  12. return 1;
  13. /* ... */
  14. }
  15. }

而对象与数组的不同之处,在于概念上对象的键值对是无序的。例如,{“a”:1,”b”:2} 和 {“b”:2,”a”:1} 虽然键值的次序不同,但这两个 JSON 对象是相等的。我们可以简单地利用 lept_find_object_index() 去找出对应的值,然后递归作比较。这部分留给读者作为练习。

3. 复制、移动与交换

本单元的重点,在于修改数组和对象的内容。我们将会实现一些接口做修改的操作,例如,为对象设置一个键值,我们可能会这么设计:

  1. void lept_set_object_value(lept_value* v, const char* key, size_t klen, const lept_value* value);
  2. void f() {
  3. lept_value v, s;
  4. lept_init(&v);
  5. lept_parse(&v, "{}");
  6. lept_init(&s);
  7. lept_set_string(&s, "Hello", 5);
  8. lept_set_object_keyvalue(&v, "s", &s); /* {"s":"Hello"} */
  9. lept_free(&v)
  10. lept_free(&s); /* 第二次释放!*/
  11. }

凡涉及赋值,都可能会引起资源拥有权(resource ownership)的问题。值 s 并不能以指针方式简单地写入对象 v,因为这样便会有两个地方都拥有 s,会做成重复释放的 bug。我们有两个选择:

  1. 在 lept_set_object_value() 中,把参数 value深度复制(deep copy)一个值,即把整个树复制一份,写入其新增的键值对中。
  2. 在 lept_set_object_value() 中,把参数 value 拥有权转移至新增的键值对,再把 value 设置成 null 值。这就是所谓的移动语意(move semantics)。

深度复制是一个常用功能,使用者也可能会用到,例如把一个 JSON 复制一个版本出来修改,保持原来的不变。所以,我们实现一个公开的深度复制函数:

  1. void lept_copy(lept_value* dst, const lept_value* src) {
  2. size_t i;
  3. assert(src != NULL && dst != NULL && src != dst);
  4. switch (src->type) {
  5. case LEPT_STRING:
  6. lept_set_string(dst, src->u.s.s, src->u.s.len);
  7. break;
  8. case LEPT_ARRAY:
  9. /* \todo */
  10. break;
  11. case LEPT_OBJECT:
  12. /* \todo */
  13. break;
  14. default:
  15. lept_free(dst);
  16. memcpy(dst, src, sizeof(lept_value));
  17. break;
  18. }
  19. }

C++11 加入了右值引用的功能,可以从语言层面区分复制和移动语意。而在 C 语言中,我们也可以通过实现不同版本的接口(不同名字的函数),实现这两种语意。但为了令接口更简单和正交(orthgonal),我们修改了 lept_set_object_value() 的设计,让它返回新增键值对的值指针,所以我们可以用 lept_copy() 去复制赋值,也可以简单地改变新增的键值:

  1. /* 返回新增键值对的指针 */
  2. lept_value* lept_set_object_value(lept_value* v, const char* key, size_t klen);
  3. void f() {
  4. lept_value v;
  5. lept_init(&v);
  6. lept_parse(&v, "{}");
  7. lept_set_string(lept_set_object_value(&v, "s"), "Hello", 5);
  8. /* {"s":"Hello"} */
  9. lept_copy(
  10. lept_add_object_keyvalue(&v, "t"),
  11. lept_get_object_keyvalue(&v, "s", 1));
  12. /* {"s":"Hello","t":"Hello"} */
  13. lept_free(&v);
  14. }

我们还提供了 lept_move(),它的实现也非常简单:

  1. void lept_move(lept_value* dst, lept_value* src) {
  2. assert(dst != NULL && src != NULL && src != dst);
  3. lept_free(dst);
  4. memcpy(dst, src, sizeof(lept_value));
  5. lept_init(src);
  6. }

类似地,我们也实现了一个交换值的接口:

  1. void lept_swap(lept_value* lhs, lept_value* rhs) {
  2. assert(lhs != NULL && rhs != NULL);
  3. if (lhs != rhs) {
  4. lept_value temp;
  5. memcpy(&temp, lhs, sizeof(lept_value));
  6. memcpy(lhs, rhs, sizeof(lept_value));
  7. memcpy(rhs, &temp, sizeof(lept_value));
  8. }
  9. }

当我们要修改对象或数组里的值时,我们可以利用这 3 个函数。例如:

  1. const char* json = "{\"a\":[1,2],\"b\":3}";
  2. char *out;
  3. lept_value v;
  4. lept_init(&v);
  5. lept_parse(&v, json);
  6. lept_copy(
  7. lept_find_object_value(&v, "b", 1),
  8. lept_find_object_value(&v, "a", 1));
  9. printf("%s\n", out = lept_stringify(&v, NULL)); /* {"a":[1,2],"b":[1,2]} */
  10. free(out);
  11. lept_parse(&v, json);
  12. lept_move(
  13. lept_find_object_value(&v, "b", 1),
  14. lept_find_object_value(&v, "a", 1));
  15. printf("%s\n", out = lept_stringify(&v, NULL)); /* {"a":null,"b":[1,2]} */
  16. free(out);
  17. lept_parse(&v, json);
  18. lept_swap(
  19. lept_find_object_value(&v, "b", 1),
  20. lept_find_object_value(&v, "a", 1));
  21. printf("%s\n", out = lept_stringify(&v, NULL)); /* {"a":3,"b":[1,2]} */
  22. free(out);
  23. lept_free(&v);

在使用时,可尽量避免 lept_copy(),而改用 lept_move() 或 lept_swap(),因为后者不需要分配内存。当中 lept_swap() 更是无须做释放的工作,令它达到 从零开始的 JSON 库教程(八):访问与其他功能 - 图2 时间复杂度,其性能与值的内容无关。


4. 动态数组

在此单元之前的实现里,每个数组的元素数目在解析后是固定不变的,其数据结构是:

  1. struct lept_value {
  2. union {
  3. /* ... */
  4. struct { lept_value* e; size_t size; }a; /* array: elements, element count*/
  5. /* ... */
  6. }u;
  7. lept_type type;
  8. };

用这种数据结构增删元素时,我们需要重新分配一个数组,把适当的旧数据拷贝过去。但这种做法是非常低效的。例如我们想要从一个空的数组加入 从零开始的 JSON 库教程(八):访问与其他功能 - 图3 个元素,便要做 从零开始的 JSON 库教程(八):访问与其他功能 - 图4 次元素复制,即 从零开始的 JSON 库教程(八):访问与其他功能 - 图5 的时间复杂度。
其中一个改进方法,是使用动态数组(dynamic array,或称可增长数组/growable array)的数据结构。C++ STL 标准库中最常用的 std::vector 也是使用这种数据结构的容器。
改动也很简单,只需要在数组中加入容量 capacity 字段,表示当前已分配的元素数目,而 size 则表示现时的有效元素数目:

  1. /* ... */
  2. struct { lept_value* e; size_t size, capacity; }a; /* array: elements, element count, capacity */
  3. /* ... */

我们终于提供设置数组的函数,而且它可提供初始的容量:

  1. void lept_set_array(lept_value* v, size_t capacity) {
  2. assert(v != NULL);
  3. lept_free(v);
  4. v->type = LEPT_ARRAY;
  5. v->u.a.size = 0;
  6. v->u.a.capacity = capacity;
  7. v->u.a.e = capacity > 0 ? (lept_value*)malloc(capacity * sizeof(lept_value)) : NULL;
  8. }

我们需要稍修改 lept_parse_array(),调用 lept_set_array() 去设置类型和分配空间。
另外,类似于 lept_get_array_size(),也加入获取当前容量的函数:

  1. size_t lept_get_array_capacity(const lept_value* v) {
  2. assert(v != NULL && v->type == LEPT_ARRAY);
  3. return v->u.a.capacity;
  4. }

如果当前的容量不足,我们需要扩大容量,标准库的 realloc() 可以分配新的内存并把旧的数据拷背过去:

  1. void lept_reserve_array(lept_value* v, size_t capacity) {
  2. assert(v != NULL && v->type == LEPT_ARRAY);
  3. if (v->u.a.capacity < capacity) {
  4. v->u.a.capacity = capacity;
  5. v->u.a.e = (lept_value*)realloc(v->u.a.e, capacity * sizeof(lept_value));
  6. }
  7. }

当数组不需要再修改,可以使用以下的函数,把容量缩小至刚好能放置现有元素:

  1. void lept_shrink_array(lept_value* v) {
  2. assert(v != NULL && v->type == LEPT_ARRAY);
  3. if (v->u.a.capacity > v->u.a.size) {
  4. v->u.a.capacity = v->u.a.size;
  5. v->u.a.e = (lept_value*)realloc(v->u.a.e, v->u.a.capacity * sizeof(lept_value));
  6. }
  7. }

我们不逐一检视每个数组修改函数,仅介绍一下两个例子:

  1. lept_value* lept_pushback_array_element(lept_value* v) {
  2. assert(v != NULL && v->type == LEPT_ARRAY);
  3. if (v->u.a.size == v->u.a.capacity)
  4. lept_reserve_array(v, v->u.a.capacity == 0 ? 1 : v->u.a.capacity * 2);
  5. lept_init(&v->u.a.e[v->u.a.size]);
  6. return &v->u.a.e[v->u.a.size++];
  7. }
  8. void lept_popback_array_element(lept_value* v) {
  9. assert(v != NULL && v->type == LEPT_ARRAY && v->u.a.size > 0);
  10. lept_free(&v->u.a.e[--v->u.a.size]);
  11. }

lept_pushback_array_element() 在数组末端压入一个元素,返回新的元素指针。如果现有的容量不足,就需要调用 lept_reserve_array() 扩容。我们现在用了一个最简单的扩容公式:若容量为 0,则分配 1 个元素;其他情况倍增容量。
lept_popback_array_element() 则做相反的工作,记得删去的元素需要调用 lept_free()。
下面这 3 个函数留给读者练习:

  1. lept_insert_array_element() 在 index 位置插入一个元素;
  2. lept_erase_array_element() 删去在 index 位置开始共 count 个元素(不改容量);
  3. lept_clear_array() 清除所有元素(不改容量)。
    1. lept_value* lept_insert_array_element(lept_value* v, size_t index);
    2. void lept_erase_array_element(lept_value* v, size_t index, size_t count);
    3. void lept_clear_array(lept_value* v);

5. 动态对象

动态对象也是采用上述相同的结构,所以直接留给读者修改结构体,并实现以下函数:

  1. void lept_set_object(lept_value* v, size_t capacity);
  2. size_t lept_get_object_capacity(const lept_value* v);
  3. void lept_reserve_object(lept_value* v, size_t capacity);
  4. void lept_shrink_object(lept_value* v);
  5. void lept_clear_object(lept_value* v);
  6. lept_value* lept_set_object_value(lept_value* v, const char* key, size_t klen);
  7. void lept_remove_object_value(lept_value* v, size_t index);

注意 lept_set_object_value() 会先搜寻是否存在现有的键,若存在则直接返回该值的指针,不存在时才新增。

6. 总结与练习

本单元主要加入了数组和对象的访问、修改方法。当中的赋值又引申了三种赋值的方式(复制、移动、交换)。这些问题是各种编程语言中都需要考虑的事情,为了减少深度复制的成本,有些程序库或运行时还会采用写入时复制(copy-on-write, COW)。而浅复制(shallow copy)则需要 引用计数(reference count)或 垃圾回收(garbage collection, GC)等技术。
另外,我们实现了以动态数组的数据结构,能较高效地对数组和对象进行增删操作。至此,我们已经完成本教程的所有核心功能。做完下面的练习后,我们还会作简单讲解,然后将迎来本教程的最后一个单元。
本单元练习内容:

  1. 完成 lept_is_equal() 里的对象比较部分。不需要考虑对象内有重复键的情况。
  2. 打开 test_array_access() 里的 #if 0,实现 lept_insert_array_element()、lept_erase_array_element() 和 lept_clear_array()。
  3. 打开 test_object_access() 里的 #if 0,参考动态数组,实现第 5 部分列出的所有函数。
  4. 完成 lept_copy() 里的数组和对象的复制部分。

如果你遇到问题,有不理解的地方,或是有建议,都欢迎在评论或 issue 中提出,让所有人一起讨论。