前言
本次我们实现解析器的词法分析功能。
注意:示例代码使用了C#8.0的语法特性,如果要在你的机器上运行,请确保安装了.Net Core 3.x开发环境。
单词类和词性类
词法分析器输出的是单词流,所以先要有单词类。这里有三点需要声明:
- 单词这种轻量级对象,直接将其声明成结构体能让它们在内存中连续分布,并且不用消耗额外空间生成引用变量。
- 我们不需要修改单词变量,因此直接声明为只读结构。
- 属性的本质是方法,需要切换上下文。为了提高性能,我们直接使用公有字段。
internal readonly struct Token
{
public readonly TokenType type;
public readonly string value;
public Token(TokenType type) => (this.type, value) = (type, null);
public Token(TokenType type, string value) => (this.type, this.value) = (type, value);
}
在单词类的定义中有一个TokenType字段,这是一个枚举类,表示单词的词性。
[Flags]
internal enum TokenType
{
None = 0x0,
ObjectStart = 0x1,
ObjectEnd = 0x2,
ArrayStart = 0x4,
ArrayEnd = 0x8,
Colon = 0x10,
Comma = 0x20,
Number = 0x40,
String = 0x80,
True = 0x100,
False = 0x200,
Null = 0x400,
End = 0x800
}
词法分析器类
有了单词类,我们就可以着手实现词法分析器了。词法分析器是一个静态类,其核心是Analyze方法:
public static Queue<Token> Analyze(string json)
{
...
}
返回值是一个由Token构成的队列,这种数据结构能简化语法分析器读取单词的代码。
有限状态机
词法分析器基于有限状态机实现,每次读取json字符串中的一个字符,根据读到的内容进行单词转换、状态转换等逻辑。词法分析器的状态转换图如下:
下面分析每个状态中执行的逻辑:
- 就绪:读取下一字符,根据读取到的内容转换状态。
- 构造符:将对应的构造符单词加到队尾,回到就绪状态。
- 字面量:如果读到的是t,就向下读三个字符,若读完为true则将true单词加到队尾,回到就绪状态,否则抛出异常。false和null同理。
- 数字:一直读直到读到逗号或-1,并将每次读到的结果存入一个字符串中,将结果与一个表示数字的模式匹配,若匹配则将对应的数字单词加入队尾,回到就绪状态,否则抛出异常。
- 字符串:一直读直到读到双引号("不算),并将每次读到的结果存入一个字符串中,将对应的字符串单词加入队尾,回到就绪状态。
- 终止:向队尾加入终止单词,结束流程。
对应方法
我们将就绪和终止的逻辑放在Analyze方法中,将其余每个状态的逻辑放入不同的私有方法中,增强代码的可读性。
就绪和终止状态
public static Queue<Token> Analyze(string json)
{
var tokens = new Queue<Token>();
// 清除空白字符
json = json.Replace(" ", "")
.Replace("\t", "")
.Replace("\n", "")
.Replace("\f", "")
.Replace