Skip to content

Commit

Permalink
Merge pull request miloyip#130 from imba-tjd/patch-1
Browse files Browse the repository at this point in the history
Make some improvements
  • Loading branch information
miloyip committed Nov 28, 2018
2 parents 7b30f8b + 5dbd572 commit 79d6f6a
Show file tree
Hide file tree
Showing 21 changed files with 104 additions and 86 deletions.
4 changes: 2 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

为什么选择 JSON?因为它足够简单,除基本编程外不需大量技术背景知识。JSON 有标准,可按照标准逐步实现。JSON 也是实际在许多应用上会使用的格式,所以才会有大量的开源库。

这是一个免费、开源的教程,如果你喜欢,也可以打赏鼓励。因为工作及家庭因素,不能保证每篇文章的首发时间,请各为见谅
这是一个免费、开源的教程,如果你喜欢,也可以打赏鼓励。因为工作及家庭因素,不能保证每篇文章的首发时间,请各位见谅

## 对象与目标

Expand Down Expand Up @@ -44,7 +44,7 @@
4. [Unicode](tutorial04/tutorial04.md)(2016/10/2 完成):Unicode 和 UTF-8 的基本知识、JSON string 的 unicode 处理。练习完成 JSON string 类型的解析。[Unicode 解答篇](tutorial04_answer/tutorial04_answer.md)(2016/10/6 完成)。
5. [解析数组](tutorial05/tutorial05.md)(2016/10/7 完成):JSON array 的语法。练习完成 JSON array 类型的解析、相关内存释放。[解析数组解答篇](tutorial05_answer/tutorial05_answer.md)(2016/10/13 完成)。
6. [解析对象](tutorial06/tutorial06.md)(2016/10/29 完成):JSON object 的语法、重构 string 解析函数。练习完成 JSON object 的解析、相关内存释放。[解析对象解答篇](tutorial06_answer/tutorial06_answer.md)(2016/11/15 完成)。
7. [生成器](tutorial07/tutorial07.md)(2016/12/20 完成):JSON 生成过程、注意事项。练习完成 JSON 生成器。[生成器解答篇](tutorial07_answer/tutorial07_answer.md)(2017/1/5 完成)
7. [生成器](tutorial07/tutorial07.md)(2016/12/20 完成):JSON 生成过程、注意事项。练习完成 JSON 生成器。[生成器解答篇](tutorial07_answer/tutorial07_answer.md)(2017/1/5 完成)
8. [访问与其他功能](tutorial08/tutorial08.md)(2018/6/2 完成):JSON array/object 的访问及修改。练习完成相关功能。
9. 终点及新开始:加入 nativejson-benchmark 测试,与 RapidJSON 对比及展望。

Expand Down
54 changes: 27 additions & 27 deletions tutorial01/tutorial01.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@

本单元内容:

1. [JSON 是什么](#json-是什么)
2. [搭建编译环境](#搭建编译环境)
3. [头文件与 API 设计](#头文件与-api-设计)
4. [JSON 语法子集](#json-语法子集)
5. [单元测试](#单元测试)
6. [宏的编写技巧](#宏的编写技巧)
7. [实现解析器](#实现解析器)
8. [关于断言](#关于断言)
9. [总结与练习](#总结与练习)
10. [常见问答](#常见问答)
1. [JSON 是什么](#1-json-是什么)
2. [搭建编译环境](#2-搭建编译环境)
3. [头文件与 API 设计](#3-头文件与-api-设计)
4. [JSON 语法子集](#4-json-语法子集)
5. [单元测试](#5-单元测试)
6. [宏的编写技巧](#6-宏的编写技巧)
7. [实现解析器](#7-实现解析器)
8. [关于断言](#8-关于断言)
9. [总结与练习](#9-总结与练习)
10. [常见问答](#10-常见问答)

## JSON 是什么
## 1. JSON 是什么

JSON(JavaScript Object Notation)是一个用于数据交换的文本格式,现时的标准为[ECMA-404](http:https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf)
JSON(JavaScript Object Notation)是一个用于数据交换的文本格式,现时的标准为[ECMA-404](https:https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf)

虽然 JSON 源至于 JavaScript 语言,但它只是一种数据格式,可用于任何编程语言。现时具类似功能的格式有 XML、YAML,当中以 JSON 的语法最为简单。

Expand Down Expand Up @@ -68,7 +68,7 @@ JSON(JavaScript Object Notation)是一个用于数据交换的文本格式

我们会逐步实现这些需求。在本单元中,我们只实现最简单的 null 和 boolean 解析。

## 搭建编译环境
## 2. 搭建编译环境

我们要做的库是跨平台、跨编译器的,同学可使用任意平台进行练习。

Expand All @@ -90,7 +90,7 @@ JSON(JavaScript Object Notation)是一个用于数据交换的文本格式

按 Configure,选择编译器,然后按 Generate 便会生成 Visual Studio 的 .sln 和 .vcproj 等文件。注意这个 build 目录都是生成的文件,可以随时删除,也不用上传至仓库。

在 OS X 下,建议安装 [Homebrew](http:https://brew.sh/),然后在命令行键入:
在 OS X 下,建议安装 [Homebrew](https:https://brew.sh/),然后在命令行键入:

~~~
$ brew install cmake
Expand Down Expand Up @@ -126,7 +126,7 @@ $ ./leptjson_test

若看到类似以上的结果,说明已成功搭建编译环境,我们可以去看看那几个代码文件的内容了。

## 头文件与 API 设计
## 3. 头文件与 API 设计

C 语言有头文件的概念,需要使用 `#include`去引入头文件中的类型声明和函数声明。但由于头文件也可以 `#include` 其他头文件,为避免重复声明,通常会利用宏加入 include 防范(include guard):

Expand Down Expand Up @@ -193,9 +193,9 @@ enum {
lept_type lept_get_type(const lept_value* v);
~~~
## JSON 语法子集
## 4. JSON 语法子集
下面是此单元的 JSON 语法子集,使用 [RFC7159](http:https://rfc7159.net/rfc7159) 中的 [ABNF](https://tools.ietf.org/html/rfc5234) 表示:
下面是此单元的 JSON 语法子集,使用 [RFC7159](https:https://tools.ietf.org/html/rfc7159) 中的 [ABNF](https://tools.ietf.org/html/rfc5234) 表示:
~~~
JSON-text = ws value ws
Expand All @@ -222,11 +222,11 @@ true = "true"
* 若一个值之后,在空白之后还有其他字符,传回 `LEPT_PARSE_ROOT_NOT_SINGULAR`。
* 若值不是那三种字面值,传回 `LEPT_PARSE_INVALID_VALUE`。
## 单元测试
## 5. 单元测试
许多同学在做练习题时,都是以 `printf`/`cout` 打印结果,再用肉眼对比结果是否乎合预期。但当软件项目越来越复杂,这个做法会越来越低效。一般我们会采用自动的测试方式,例如单元测试(unit testing)。单元测试也能确保其他人修改代码后,原来的功能维持正确(这称为回归测试/regression testing)。
常用的单元测试框架有 xUnit 系列,如 C++ 的 [Google Test](https://github.com/google/googletest)、C# 的 [NUnit](http:https://www.nunit.org/)。我们为了简单起见,会编写一个极简单的单元测试方式。
常用的单元测试框架有 xUnit 系列,如 C++ 的 [Google Test](https://github.com/google/googletest)、C# 的 [NUnit](https:https://www.nunit.org/)。我们为了简单起见,会编写一个极简单的单元测试方式。
一般来说,软件开发是以周期进行的。例如,加入一个功能,再写关于该功能的单元测试。但也有另一种软件开发方法论,称为测试驱动开发(test-driven development, TDD),它的主要循环步骤是:
Expand All @@ -243,7 +243,7 @@ TDD 是先写测试,再实现功能。好处是实现只会刚好满足测试
回到 leptjson 项目,`test.c` 包含了一个极简的单元测试框架:
~~~
~~~c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Expand Down Expand Up @@ -299,7 +299,7 @@ int main() {

然而,完全按照 TDD 的步骤来开发,是会减慢开发进程。所以我个人会在这两种极端的工作方式取平衡。通常会在设计 API 后,先写部分测试代码,再写满足那些测试的实现。

## 宏的编写技巧
## 6. 宏的编写技巧

有些同学可能不了解 `EXPECT_EQ_BASE` 宏的编写技巧,简单说明一下。反斜线代表该行未结束,会串接下一行。而如果宏里有多过一个语句(statement),就需要用 `do { /*...*/ } while(0)` 包裹成单个语句,否则会有如下的问题:

Expand Down Expand Up @@ -344,7 +344,7 @@ else
c();
~~~

## 实现解析器
## 7. 实现解析器

有了 API 的设计、单元测试,终于要实现解析器了。

Expand Down Expand Up @@ -419,15 +419,15 @@ static int lept_parse_value(lept_context* c, lept_value* v) {

由于 `lept_parse_whitespace()` 是不会出现错误的,返回类型为 `void`。其它的解析函数会返回错误码,传递至顶层。

## 关于断言
## 8. 关于断言

断言(assertion)是 C 语言中常用的防御式编程方式,减少编程错误。最常用的是在函数开始的地方,检测所有参数。有时候也可以在调用函数后,检查上下文是否正确。

C 语言的标准库含有 [`assert()`](http:https://en.cppreference.com/w/c/error/assert) 这个宏(需 `#include <assert.h>`),提供断言功能。当程序以 release 配置编译时(定义了 `NDEBUG` 宏),`assert()` 不会做检测;而当在 debug 配置时(没定义 `NDEBUG` 宏),则会在运行时检测 `assert(cond)` 中的条件是否为真(非 0),断言失败会直接令程序崩溃。
C 语言的标准库含有 [`assert()`](https:https://en.cppreference.com/w/c/error/assert) 这个宏(需 `#include <assert.h>`),提供断言功能。当程序以 release 配置编译时(定义了 `NDEBUG` 宏),`assert()` 不会做检测;而当在 debug 配置时(没定义 `NDEBUG` 宏),则会在运行时检测 `assert(cond)` 中的条件是否为真(非 0),断言失败会直接令程序崩溃。

例如上面的 `lept_parse_null()` 开始时,当前字符应该是 `'n'`,所以我们使用一个宏 `EXPECT(c, ch)` 进行断言,并跳到下一字符。

初使用断言的同学,可能会错误地把含副作用的代码放在 `assert()` 中:
初使用断言的同学,可能会错误地把含[副作用](https://en.wikipedia.org/wiki/Side_effect_(computer_science))的代码放在 `assert()` 中:

~~~c
assert(x++ == 0); /* 这是错误的! */
Expand All @@ -437,15 +437,15 @@ assert(x++ == 0); /* 这是错误的! */
另一个问题是,初学者可能会难于分辨何时使用断言,何时处理运行时错误(如返回错误值或在 C++ 中抛出异常)。简单的答案是,如果那个错误是由于程序员错误编码所造成的(例如传入不合法的参数),那么应用断言;如果那个错误是程序员无法避免,而是由运行时的环境所造成的,就要处理运行时错误(例如开启文件失败)。
## 总结与练习
## 9. 总结与练习
本文介绍了如何配置一个编程环境,单元测试的重要性,以至于一个 JSON 解析器的子集实现。如果你读到这里,还未动手,建议你快点试一下。以下是本单元的练习,很容易的,但我也会在稍后发出解答篇。
1. 修正关于 `LEPT_PARSE_ROOT_NOT_SINGULAR` 的单元测试,若 json 在一个值之后,空白之后还有其它字符,则要返回 `LEPT_PARSE_ROOT_NOT_SINGULAR`。
2. 参考 `test_parse_null()`,加入 `test_parse_true()`、`test_parse_false()` 单元测试。
3. 参考 `lept_parse_null()` 的实现和调用方,解析 true 和 false 值。
## 常见问答
## 10. 常见问答
1. 为什么把例子命名为 leptjson?
Expand Down
2 changes: 1 addition & 1 deletion tutorial01_answer/tutorial01_answer.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ static void test_parse() {
}
~~~

但要记得在上一级的测试函数 `test_parse()` 调用这函数,否则会不起作用。还好如果我们记得用 `static` 修饰这两个函数,编译器会发出告警
但要记得在上一级的测试函数 `test_parse()` 调用这函数,否则会不起作用。还好如果我们记得用 `static` 修饰这两个函数,编译器会发出警告

~~~
test.c:30:13: warning: unused function 'test_parse_true' [-Wunused-function]
Expand Down
2 changes: 1 addition & 1 deletion tutorial02/leptjson.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
typedef enum { LEPT_NULL, LEPT_FALSE, LEPT_TRUE, LEPT_NUMBER, LEPT_STRING, LEPT_ARRAY, LEPT_OBJECT } lept_type;

typedef struct {
double n;
double n;
lept_type type;
}lept_value;

Expand Down
Loading

0 comments on commit 79d6f6a

Please sign in to comment.