本次我们实现解析器的词法分析功能。
注意:示例代码使用了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字符串中的一个字符,根据读到的内容进行单词转换、状态转换等逻辑。词法分析器的状态转换图如下:
下面分析每个状态中执行的逻辑:
我们将就绪和终止的逻辑放在Analyze方法中,将其余每个状态的逻辑放入不同的私有方法中,增强代码的可读性。
public static Queue<Token> Analyze(string json)
{
var tokens = new Queue<Token>();
// 清除空白字符
json = json.Replace(" ", "")
.Replace("\t", "")
.Replace("\n", "")
.Replace("\f", "")
.Replace("\r", "");
var reader = new StringReader(json);
int chr;
// 查看下一个字符确定下一个状态
while ((chr = reader.Peek()) != -1)
{
switch (chr)
{
case '{':
case '}':
case '[':
case ']':
case ':':
case ',':
tokens.Enqueue(ReadConstructor(reader)); // 进入构造符状态
break;
case '+':
case '-':
case '.':
case int c when c >= '0' && c <= '9':
tokens.Enqueue(ReadNumber(reader)); // 进入数字状态
break;
case '"':
tokens.Enqueue(ReadString(reader)); // 进入字符串状态
break;
case 'n':
case 't':
case 'f':
tokens.Enqueue(ReadConstant(reader)); // 进入字面量状态
break;
default:
throw new JsonException($"无效的Json字符: {chr}");
}
}
// 进入终止状态
tokens.Enqueue(new Token(TokenType.End));
return tokens;
}
private static Token ReadConstructor(StringReader reader)
{
int chr;
return (chr = reader.Read()) switch
{
'{' => new Token(TokenType.ObjectStart),
'}' => new Token(TokenType.ObjectEnd),
'[' => new Token(TokenType.ArrayStart),
']' => new Token(TokenType.ArrayEnd),
':' => new Token(TokenType.Colon),
',' => new Token(TokenType.Comma),
_ => throw new JsonException($"无效的构造符: {chr}")
};
}
// 数字的模式
private const string NUMBER_REGEX = "^(\\+|\\-)?(\\d+(\\.\\d+)?|(\\.\\d+))((e|E)(\\+|\\-)?\\d+)?$";
private static Token ReadNumber(StringReader reader)
{
int chr;
var sb = new StringBuilder();
// 循环读取
while (true)
{
chr = reader.Peek();
// 遇到这些字符就停止
if (chr == '}' || chr == ']' || chr == ',' || chr == -1)
{
break;
}
if (chr >= '0' && chr <= '9' ||
chr == '+' || chr == '-' ||
chr == '.' ||
chr == 'e' || chr == 'E')
{
sb.Append((char) chr);
reader.Read();
} else
{
throw new JsonException($"无效的数字字符: {chr}");
}
}
var result = sb.ToString();
// 匹配模式
if (Regex.IsMatch(result, NUMBER_REGEX))
{
return new Token(TokenType.Number, result);
} else
{
throw new JsonException($"数字格式非法: {result}");
}
}
private static Token ReadString(StringReader reader)
{
int chr;
var sb = new StringBuilder();
// 跳过第一个双引号
reader.Read();
// 循环读取
while (true)
{
switch (chr = reader.Read())
{
// 处理转义序列
case '\\':
sb.Append((char) chr);
switch (chr = reader.Read())
{
case '"':
case '\\':
case 'r':
case 'n':
case 'b':
case 't':
case 'f':
sb.Append((char) chr);
break;
// 处理Unicode
case 'u':
sb.Append((char) chr);
for (int i = 0; i < 4; i++)
{
chr = reader.Read();
if (chr >= '0' && chr <= '9' ||
chr >= 'a' && chr <= 'f' ||
chr >= 'A' && chr <= 'F')
{
sb.Append((char) chr);
} else
{
throw new JsonException($"无效的Unicode: {chr}");
}
}
break;
default:
throw new JsonException($"无效的转义字符: {chr}");
}
break;
case '"':
return new Token(TokenType.String, sb.ToString());
case '\n':
case '\r':
throw new JsonException("字符串中不允许换行");
default:
sb.Append((char) chr);
break;
}
}
}
private static Token ReadConstant(StringReader reader)
{
int chr = reader.Read();
switch (chr)
{
case 't':
if (reader.Read() == 'r' && reader.Read() == 'u' && reader.Read() == 'e')
{
return new Token(TokenType.True);
} else
{
throw new JsonException($"无效的字面量字符: {chr}");
}
case 'f':
if (reader.Read() == 'a' && reader.Read() == 'l' && reader.Read() == 's' && reader.Read() == 'e')
{
return new Token(TokenType.False);
} else
{
throw new JsonException($"无效的字面量字符: {chr}");
}
case 'n':
if (reader.Read() == 'u' && reader.Read() == 'l' && reader.Read() == 'l')
{
return new Token(TokenType.Null);
} else
{
throw new JsonException($"无效的字面量字符: {chr}");
}
default:
throw new JsonException($"无效的字面量字符: {chr}");
}
}
以上就是词法分析器的实现。下回将利用词法分析器的输出来实现基本语法分析器,基本语法分析器只包含Json字符串转换C#对象的基本功能,不支持自定义特性,不支持自定义转换函数。而高级语法分析器的实现将在基本语法分析器全部实现之后,以区分主要功能和附加功能。