内网站做映射,吉林长春seo网站建设网站优化,模具设计培训,蒙文网站建设的意义1. JSON 是什么
JSON#xff08;JavaScript Object Notation#xff09;是一个用于数据交换的文本格式#xff0c;现时的标准为ECMA-404 。
虽然 JSON 源自于 JavaScript 语言#xff0c;但它只是一种数据格式#xff0c;可用于任何编程语言。现时具有类似功能的格式有X…1. JSON 是什么
JSONJavaScript Object Notation是一个用于数据交换的文本格式现时的标准为ECMA-404 。
虽然 JSON 源自于 JavaScript 语言但它只是一种数据格式可用于任何编程语言。现时具有类似功能的格式有XML、YAML当中以 JSON 的语法最为简单。
例如一个动态网页想从服务器获得数据时服务器从数据库查找数据然后把数据转换成 JSON 文本格式
{title: Design Patterns,subtitle: Elements of Reusable Object-Oriented Software,author: [Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides],year: 2009,weight: 1.8,hardcover:: true,publisher: {Company: Person Education,Country: India},website: null
}网页的脚本代码就可以把此 JSON 文本解析为内部的数据结构去使用。
从此例子可看出JSON 是树状结构而 JSON 只包含 6 种数据类型
null表示为 nullboolean表示为 true 或 falsenumber一般的浮点数表示方式在下一单元详细说明string表示为 “…”array表示为 […]object表示为 {…}
我们要实现的 JSON 库主要是完成 3 个需求
把 JSON 文本解析为一个树状数据结构parse。提供接口访问该数据结构access。把数据结构转换成 JSON 文本stringify。
我们会逐步实现这些需求。在本单元中我们只实现最简单的 null 和 boolean 解析。
2. 搭建编译环境
源代码位于 kikajson当中 01 为本单元的代码。
代码文件只有 3 个
kikajson.hleptjson 的头文件header file含有对外的类型和 API 函数声明。kikajson.cleptjson 的实现文件implementation file含有内部的类型声明和函数实现。此文件会编译成库。test.c我们使用测试驱动开发test driven development, TDD。此文件包含测试程序需要链接 leptjson 库。
为了方便跨平台开发使用一个软件配置工具 CMake。
使用 Visual Studio 打开本单元项目VS会自动进行CMake的配置前提是已经VS已经安装好相应的插件然后点开 CMakeLists.txt运行当前文档就可以编译运行了
编译运行后会出现
D:\rep\kikajson\test.c:56: expect: 3 actual: 0
11/12 (91.67%) passed若看到类似以上的结果说明已成功搭建编译环境可以去看看那几个代码文件的内容了。
3. 头文件与 API 设计
C 语言有头文件的概念需要使用 #include去引入头文件中的类型声明和函数声明。但由于头文件也可以 #include 其他头文件为避免重复声明通常会利用宏加入 include 防范include guard
#ifndef KIKAJSON_H__
#define KIKAJSON_H__/* ... */#endif /* KIKAJSON_H__ */宏的名字必须是唯一的通常习惯以 _H__ 作为后缀。由于 leptjson 只有一个头文件可以简单命名为 KIKAJSON_H__。如果项目有多个文件或目录结构可以用 项目名称_目录_文件名称_H__ 这种命名方式。
如前所述JSON 中有 6 种数据类型如果把 true 和 false 当作两个类型就是 7 种我们为此声明一个枚举类型enumeration type
typedef enum { KIKA_NULL, KIKA_FALSE, KIKA_TRUE, KIKA_NUMBER, KIKA_STRING, KIKA_ARRAY, KIKA_OBJECT } kika_type;因为 C 语言没有 C 的命名空间namespace功能一般会使用项目的简写作为标识符的前缀。通常枚举值用全大写如 KIKA_NULL而类型及函数则用小写如 kika_type。
接下来我们声明 JSON 的数据结构。JSON 是一个树形结构我们最终需要实现一个树的数据结构每个节点使用 kika_value 结构体表示我们会称它为一个 JSON 值JSON value。 在此单元中我们只需要实现 null, true 和 false 的解析因此该结构体只需要存储一个 kika_type。之后的单元会逐步加入其他数据。
typedef struct {kika_type type;
}kika_value;C 语言的结构体是以 struct X {} 形式声明的定义变量时也要写成 struct X x;。为方便使用上面的代码使用了 typedef。
然后我们现在只需要两个 API 函数
1一个是解析 JSON
int kika_parse(kika_value* v, const char* json);传入的 JSON 文本是一个 C 字符串空字符结尾字符串null-terminated string由于我们不应该改动这个输入字符串所以使用 const char* 类型。
另一注意点是传入的根节点指针 v 是由使用方负责分配的所以一般用法是
kika_value v;
const char json[] ...;
int ret kika_parse(v, json);返回值是以下这些枚举值无错误会返回 KIKA_PARSE_OK其他值在下节解释。
enum {KIKA_PARSE_OK 0,KIKA_PARSE_EXPECT_VALUE,KIKA_PARSE_INVALID_VALUE,KIKA_PARSE_ROOT_NOT_SINGULAR
};2另一个是一个访问结果的函数就是获取其类型
kika_type kika_get_type(const kika_value* v);4. JSON 语法子集
下面是此单元的 JSON 语法子集使用 RFC7159 中的 ABNF 表示
JSON-text ws value ws
ws *(%x20 / %x09 / %x0A / %x0D)
value null / false / true
null null
false false
true true当中 %xhh 表示以 16 进制表示的字符/ 是多选一* 是零或多个() 用于分组。
第一行的意思是JSON 文本由 3 部分组成首先是空白whitespace接着是一个值最后是空白。
第二行的意思是所谓空白是由零或多个空格符space U0020、制表符tab U0009、换行符LF U000A、回车符CR U000D所组成。
第三行的意思是我们现时的值只可以是 null、false 或 true。
第四到六行指出了 null、false、 true 对应的字面值literal。
我们的解析器应能判断输入是否一个合法的 JSON。如果输入的 JSON 不合符这个语法我们要产生对应的错误码方便使用者追查问题。
在这个 JSON 语法子集下我们定义 3 种错误码
若一个 JSON 只含有空白传回 KIKA_PARSE_EXPECT_VALUE。若一个值之后在空白之后还有其他字符传回 KIKA_PARSE_ROOT_NOT_SINGULAR。若值不是那三种字面值传回 KIKA_PARSE_INVALID_VALUE。
5. 单元测试
许多初学者在做编程练习时都是以 printfcout 打印结果再用肉眼对比结果是否乎合预期。但当软件项目越来越复杂这个做法会越来越低效。一般我们会采用自动的测试方式例如单元测试unit testing。单元测试也能确保其他人修改代码后原来的功能维持正确这称为回归测试regression testing。
常用的单元测试框架有 xUnit 系列如 C 的 Google Test、C# 的 NUnit。我们为了简单起见会编写一个极简单的单元测试方式。
一般来说软件开发是以周期进行的。例如加入一个功能再写关于该功能的单元测试。但也有另一种软件开发方法论称为测试驱动开发test-driven development, TDD它的主要循环步骤是
加入一个测试。运行所有测试新的测试应该会失败。编写实现代码。运行所有测试若有测试失败回到3。重构代码。回到 1
TDD 是先写测试再实现功能。这样的好处是实现只会刚好满足测试而不会写了一些不需要的代码或是没有被测试的代码。
但无论我们是采用 TDD或是先实现后测试都应尽量加入足够覆盖率的单元测试。
回到 kikajson 项目test.c 包含了一个极简的单元测试框架
#include stdio.h
#include stdlib.h
#include string.h
#include kikajson.hstatic int main_ret 0;
static int test_count 0;
static int test_pass 0;#define EXPECT_EQ_BASE(equality, expect, actual, format) \do {\test_count;\if (equality)\test_pass;\else {\fprintf(stderr, %s:%d: expect: format actual: format \n, __FILE__, __LINE__, expect, actual);\main_ret 1;\}\} while(0)#define EXPECT_EQ_INT(expect, actual) EXPECT_EQ_BASE((expect) (actual), expect, actual, %d)static void test_parse_null() {kika_value v;v.type KIKA_TRUE;EXPECT_EQ_INT(KIKA_PARSE_OK, kika_parse(v, null));EXPECT_EQ_INT(KIKA_NULL, kika_get_type(v));
}/* ... */static void test_parse() {test_parse_null();/* ... */
}int main() {test_parse();printf(%d/%d (%3.2f%%) passed\n, test_pass, test_count, test_pass * 100.0 / test_count);return main_ret;
}现时只提供了一个 EXPECT_EQ_INT(expect, actual) 的宏每次使用这个宏时如果 expect ! actual预期值不等于实际值便会输出错误信息。
若按照 TDD 的步骤我们先写一个测试如上面的 test_parse_null()而 kika_parse() 只返回 KIKA_PARSE_OK
D:\rep\kikajson\test.c:27: expect: 0 actual: 1
1/2 (50.00%) passed第一个测试因为 kika_parse() 返回 KIKA_PARSE_OK所以是通过的。
第二个测试因为 kika_parse() 没有把 v.type 改成 KIKA_NULL造成失败。我们再实现 kika_parse() 令到它能通过测试。
然而完全按照 TDD 的步骤来开发是会减慢开发进程。所以有时需要在这两种极端的工作方式取平衡。一种做法是在设计 API 后先写部分测试代码再写满足那些测试的实现。
6. 宏的编写技巧
关于 EXPECT_EQ_BASE 宏的编写技巧简单说明一下。反斜线代表该行未结束会串接下一行。而如果宏里有多过一个语句statement就需要用 do { /*...*/ } while(0) 包裹成单个语句否则会有如下的问题
#define M() a(); b()
if (cond)M();
elsec();/* 预处理后 */if (cond)a(); b(); /* b(); 在 if 之外 */
else /* - else 缺乏对应 if */c();只用 { } 也不行
#define M() { a(); b(); }/* 预处理后 */if (cond){ a(); b(); }; /* 最后的分号代表 if 语句结束 */
else /* else 缺乏对应 if */c();用 do while 就行了
#define M() do { a(); b(); } while(0)/* 预处理后 */if (cond)do { a(); b(); } while(0);
elsec();7. 实现解析器
有了 API 的设计、单元测试终于要实现解析器了。
首先为了减少解析函数之间传递多个参数我们把这些数据都放进一个 kika_context 结构体
typedef struct {const char* json;
}kika_context;/* ... *//* 提示这里应该是 JSON-text ws value ws */
/* 以下实现没处理最后的 ws 和 KIKA_PARSE_ROOT_NOT_SINGULAR */
int kika_parse(kika_value* v, const char* json) {kika_context c;assert(v ! NULL);c.json json;v-type KIKA_NULL;kika_parse_whitespace(c);return kika_parse_value(c, v);
}暂时我们只储存 json 字符串当前位置之后的单元我们需要加入更多内容。
若 kika_parse() 失败会把 v 设为 null 类型所以这里先把它设为 null让 kika_parse_value() 写入解析出来的根值。
kikajson 是一个手写的递归下降解析器recursive descent parser。由于 JSON 语法特别简单我们不需要写分词器tokenizer只需检测下一个字符便可以知道它是哪种类型的值然后调用相关的分析函数。对于完整的 JSON 语法跳过空白后只需检测当前字符
n ➔ nullt ➔ truef ➔ false ➔ string0-9/- ➔ number[ ➔ array{ ➔ object
所以我们可以按照 JSON 语法一节的 EBNF 简单翻译成解析函数
#define EXPECT(c, ch) do { assert(*c-json (ch)); c-json; } while(0)/* ws *(%x20 / %x09 / %x0A / %x0D) */
static void kika_parse_whitespace(kika_context* c) {const char *p c-json;while (*p || *p \t || *p \n || *p \r)p;c-json p;
}/* null null */
static int kika_parse_null(kika_context* c, kika_value* v) {EXPECT(c, n);if (c-json[0] ! u || c-json[1] ! l || c-json[2] ! l)return KIKA_PARSE_INVALID_VALUE;c-json 3;v-type KIKA_NULL;return KIKA_PARSE_OK;
}/* value null / false / true */
/* 提示下面代码没处理 false / true将会是练习之一 */
static int kika_parse_value(kika_context* c, kika_value* v) {switch (*c-json) {case n: return kika_parse_null(c, v);case \0: return KIKA_PARSE_EXPECT_VALUE;default: return KIKA_PARSE_INVALID_VALUE;}
}由于 kika_parse_whitespace() 是不会出现错误的返回类型为 void。其它的解析函数会返回错误码传递至顶层。
8. 关于断言
断言assertion是 C 语言中常用的防御式编程方式减少编程错误。最常用的是在函数开始的地方检测所有参数。有时候也可以在调用函数后检查上下文是否正确。
C 语言的标准库含有 assert() 这个宏需 #include assert.h提供断言功能。当程序以 release 配置编译时定义了 NDEBUG 宏assert() 不会做检测而当在 debug 配置时没定义 NDEBUG 宏则会在运行时检测 assert(cond) 中的条件是否为真非 0断言失败会直接令程序崩溃。
例如上面的 kika_parse_null() 开始时当前字符应该是 n所以我们使用一个宏 EXPECT(c, ch) 进行断言并跳到下一字符。
初使用断言的同学可能会错误地把含副作用的代码放在 assert() 中
assert(x 0); /* 这是错误的! */这样会导致 debug 和 release 版的行为不一样。
另一个问题是初学者可能会难于分辨何时使用断言何时使用运行时错误如返回错误值或在 C 中抛出异常。简单的答案是如果那个错误是由于程序员错误编码所造成的例如传入不合法的参数那么应用断言如果那个错误是程序员无法避免而是由运行时的环境所造成的就要处理运行时错误例如开启文件失败。
9. 总结与练习
本文介绍了如何配置一个编程环境单元测试的重要性以至于一个 JSON 解析器的子集实现。以下是本单元的练习解答在 01-exercise 中。
修正关于 KIKA_PARSE_ROOT_NOT_SINGULAR 的单元测试若 json 在一个值之后空白之后还有其它字符则要返回 KIKA_PARSE_ROOT_NOT_SINGULAR。参考 test_parse_null()加入 test_parse_true()、test_parse_false() 单元测试。参考 kika_parse_null() 的实现和调用方法解析 true 和 false 值。
10. 常见问答
1… 为什么使用宏而不用函数或内联函数
因为这个测试框架使用了 __LINE__ 这个编译器提供的宏代表编译时该行的行号。如果用函数或内联函数每次的行号便都会相同。另外内联函数是 C99 的新增功能本框架使用 C89。
其他常见问答将会从评论中整理。