7
7
8
8
本单元内容:
9
9
10
- 1 . [ JSON 是什么] ( #json-是什么 )
11
- 2 . [ 搭建编译环境] ( #搭建编译环境 )
12
- 3 . [ 头文件与 API 设计] ( #头文件与-api-设计 )
13
- 4 . [ JSON 语法子集] ( #json-语法子集 )
14
- 5 . [ 单元测试] ( #单元测试 )
15
- 6 . [ 宏的编写技巧] ( #宏的编写技巧 )
16
- 7 . [ 实现解析器] ( #实现解析器 )
17
- 8 . [ 关于断言] ( #关于断言 )
18
- 9 . [ 总结与练习] ( #总结与练习 )
19
- 10 . [ 常见问答] ( #常见问答 )
10
+ 1 . [ JSON 是什么] ( #1- json-是什么 )
11
+ 2 . [ 搭建编译环境] ( #2- 搭建编译环境 )
12
+ 3 . [ 头文件与 API 设计] ( #3- 头文件与-api-设计 )
13
+ 4 . [ JSON 语法子集] ( #4- json-语法子集 )
14
+ 5 . [ 单元测试] ( #5- 单元测试 )
15
+ 6 . [ 宏的编写技巧] ( #6- 宏的编写技巧 )
16
+ 7 . [ 实现解析器] ( #7- 实现解析器 )
17
+ 8 . [ 关于断言] ( #8- 关于断言 )
18
+ 9 . [ 总结与练习] ( #9- 总结与练习 )
19
+ 10 . [ 常见问答] ( #10- 常见问答 )
20
20
21
- ## JSON 是什么
21
+ ## 1. JSON 是什么
22
22
23
- JSON(JavaScript Object Notation)是一个用于数据交换的文本格式,现时的标准为[ ECMA-404] ( http ://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf) 。
23
+ JSON(JavaScript Object Notation)是一个用于数据交换的文本格式,现时的标准为[ ECMA-404] ( https ://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf) 。
24
24
25
25
虽然 JSON 源至于 JavaScript 语言,但它只是一种数据格式,可用于任何编程语言。现时具类似功能的格式有 XML、YAML,当中以 JSON 的语法最为简单。
26
26
@@ -68,7 +68,7 @@ JSON(JavaScript Object Notation)是一个用于数据交换的文本格式
68
68
69
69
我们会逐步实现这些需求。在本单元中,我们只实现最简单的 null 和 boolean 解析。
70
70
71
- ## 搭建编译环境
71
+ ## 2. 搭建编译环境
72
72
73
73
我们要做的库是跨平台、跨编译器的,同学可使用任意平台进行练习。
74
74
@@ -90,7 +90,7 @@ JSON(JavaScript Object Notation)是一个用于数据交换的文本格式
90
90
91
91
按 Configure,选择编译器,然后按 Generate 便会生成 Visual Studio 的 .sln 和 .vcproj 等文件。注意这个 build 目录都是生成的文件,可以随时删除,也不用上传至仓库。
92
92
93
- 在 OS X 下,建议安装 [ Homebrew] ( http ://brew.sh/) ,然后在命令行键入:
93
+ 在 OS X 下,建议安装 [ Homebrew] ( https ://brew.sh/) ,然后在命令行键入:
94
94
95
95
~~~
96
96
$ brew install cmake
@@ -126,7 +126,7 @@ $ ./leptjson_test
126
126
127
127
若看到类似以上的结果,说明已成功搭建编译环境,我们可以去看看那几个代码文件的内容了。
128
128
129
- ## 头文件与 API 设计
129
+ ## 3. 头文件与 API 设计
130
130
131
131
C 语言有头文件的概念,需要使用 ` #include ` 去引入头文件中的类型声明和函数声明。但由于头文件也可以 ` #include ` 其他头文件,为避免重复声明,通常会利用宏加入 include 防范(include guard):
132
132
@@ -193,9 +193,9 @@ enum {
193
193
lept_type lept_get_type (const lept_value* v);
194
194
~~~
195
195
196
- ## JSON 语法子集
196
+ ## 4. JSON 语法子集
197
197
198
- 下面是此单元的 JSON 语法子集,使用 [RFC7159](http ://rfc7159.net /rfc7159) 中的 [ABNF](https://tools.ietf.org/html/rfc5234) 表示:
198
+ 下面是此单元的 JSON 语法子集,使用 [RFC7159](https ://tools.ietf.org/html /rfc7159) 中的 [ABNF](https://tools.ietf.org/html/rfc5234) 表示:
199
199
200
200
~~~
201
201
JSON-text = ws value ws
@@ -222,11 +222,11 @@ true = "true"
222
222
* 若一个值之后,在空白之后还有其他字符,传回 `LEPT_PARSE_ROOT_NOT_SINGULAR`。
223
223
* 若值不是那三种字面值,传回 `LEPT_PARSE_INVALID_VALUE`。
224
224
225
- ## 单元测试
225
+ ## 5. 单元测试
226
226
227
227
许多同学在做练习题时,都是以 `printf`/`cout` 打印结果,再用肉眼对比结果是否乎合预期。但当软件项目越来越复杂,这个做法会越来越低效。一般我们会采用自动的测试方式,例如单元测试(unit testing)。单元测试也能确保其他人修改代码后,原来的功能维持正确(这称为回归测试/regression testing)。
228
228
229
- 常用的单元测试框架有 xUnit 系列,如 C++ 的 [Google Test](https://github.com/google/googletest)、C# 的 [NUnit](http ://www.nunit.org/)。我们为了简单起见,会编写一个极简单的单元测试方式。
229
+ 常用的单元测试框架有 xUnit 系列,如 C++ 的 [Google Test](https://github.com/google/googletest)、C# 的 [NUnit](https ://www.nunit.org/)。我们为了简单起见,会编写一个极简单的单元测试方式。
230
230
231
231
一般来说,软件开发是以周期进行的。例如,加入一个功能,再写关于该功能的单元测试。但也有另一种软件开发方法论,称为测试驱动开发(test-driven development, TDD),它的主要循环步骤是:
232
232
@@ -243,7 +243,7 @@ TDD 是先写测试,再实现功能。好处是实现只会刚好满足测试
243
243
244
244
回到 leptjson 项目,`test.c` 包含了一个极简的单元测试框架:
245
245
246
- ~~~
246
+ ~~~c
247
247
#include <stdio.h>
248
248
#include <stdlib.h>
249
249
#include <string.h>
@@ -299,7 +299,7 @@ int main() {
299
299
300
300
然而,完全按照 TDD 的步骤来开发,是会减慢开发进程。所以我个人会在这两种极端的工作方式取平衡。通常会在设计 API 后,先写部分测试代码,再写满足那些测试的实现。
301
301
302
- ## 宏的编写技巧
302
+ ## 6. 宏的编写技巧
303
303
304
304
有些同学可能不了解 ` EXPECT_EQ_BASE ` 宏的编写技巧,简单说明一下。反斜线代表该行未结束,会串接下一行。而如果宏里有多过一个语句(statement),就需要用 ` do { /*...*/ } while(0) ` 包裹成单个语句,否则会有如下的问题:
305
305
344
344
c();
345
345
~~~
346
346
347
- ## 实现解析器
347
+ ## 7. 实现解析器
348
348
349
349
有了 API 的设计、单元测试,终于要实现解析器了。
350
350
@@ -419,15 +419,15 @@ static int lept_parse_value(lept_context* c, lept_value* v) {
419
419
420
420
由于 ` lept_parse_whitespace() ` 是不会出现错误的,返回类型为 ` void ` 。其它的解析函数会返回错误码,传递至顶层。
421
421
422
- ## 关于断言
422
+ ## 8. 关于断言
423
423
424
424
断言(assertion)是 C 语言中常用的防御式编程方式,减少编程错误。最常用的是在函数开始的地方,检测所有参数。有时候也可以在调用函数后,检查上下文是否正确。
425
425
426
- C 语言的标准库含有 [ ` assert() ` ] ( http ://en.cppreference.com/w/c/error/assert) 这个宏(需 ` #include <assert.h> ` ),提供断言功能。当程序以 release 配置编译时(定义了 ` NDEBUG ` 宏),` assert() ` 不会做检测;而当在 debug 配置时(没定义 ` NDEBUG ` 宏),则会在运行时检测 ` assert(cond) ` 中的条件是否为真(非 0),断言失败会直接令程序崩溃。
426
+ C 语言的标准库含有 [ ` assert() ` ] ( https ://en.cppreference.com/w/c/error/assert) 这个宏(需 ` #include <assert.h> ` ),提供断言功能。当程序以 release 配置编译时(定义了 ` NDEBUG ` 宏),` assert() ` 不会做检测;而当在 debug 配置时(没定义 ` NDEBUG ` 宏),则会在运行时检测 ` assert(cond) ` 中的条件是否为真(非 0),断言失败会直接令程序崩溃。
427
427
428
428
例如上面的 ` lept_parse_null() ` 开始时,当前字符应该是 ` 'n' ` ,所以我们使用一个宏 ` EXPECT(c, ch) ` 进行断言,并跳到下一字符。
429
429
430
- 初使用断言的同学,可能会错误地把含副作用的代码放在 ` assert() ` 中:
430
+ 初使用断言的同学,可能会错误地把含 [ 副作用 ] ( https://en.wikipedia.org/wiki/Side_effect_(computer_science) ) 的代码放在 ` assert() ` 中:
431
431
432
432
~~~ c
433
433
assert (x++ == 0 ); /* 这是错误的! */
@@ -437,15 +437,15 @@ assert(x++ == 0); /* 这是错误的! */
437
437
438
438
另一个问题是,初学者可能会难于分辨何时使用断言,何时处理运行时错误(如返回错误值或在 C++ 中抛出异常)。简单的答案是,如果那个错误是由于程序员错误编码所造成的(例如传入不合法的参数),那么应用断言;如果那个错误是程序员无法避免,而是由运行时的环境所造成的,就要处理运行时错误(例如开启文件失败)。
439
439
440
- ## 总结与练习
440
+ ## 9. 总结与练习
441
441
442
442
本文介绍了如何配置一个编程环境,单元测试的重要性,以至于一个 JSON 解析器的子集实现。如果你读到这里,还未动手,建议你快点试一下。以下是本单元的练习,很容易的,但我也会在稍后发出解答篇。
443
443
444
444
1 . 修正关于 ` LEPT_PARSE_ROOT_NOT_SINGULAR ` 的单元测试,若 json 在一个值之后,空白之后还有其它字符,则要返回 ` LEPT_PARSE_ROOT_NOT_SINGULAR ` 。
445
445
2 . 参考 ` test_parse_null() ` ,加入 ` test_parse_true() ` 、` test_parse_false() ` 单元测试。
446
446
3 . 参考 ` lept_parse_null() ` 的实现和调用方,解析 true 和 false 值。
447
447
448
- ## 常见问答
448
+ ## 10. 常见问答
449
449
450
450
1 . 为什么把例子命名为 leptjson?
451
451
0 commit comments