用C#实现一个Json解析器(4)——词法分析器

前言

本次我们实现解析器的词法分析功能。

注意:示例代码使用了C#8.0的语法特性,如果要在你的机器上运行,请确保安装了.Net Core 3.x开发环境。

单词类和词性类

词法分析器输出的是单词流,所以先要有单词类。这里有三点需要声明:

  1. 单词这种轻量级对象,直接将其声明成结构体能让它们在内存中连续分布,并且不用消耗额外空间生成引用变量。
  2. 我们不需要修改单词变量,因此直接声明为只读结构。
  3. 属性的本质是方法,需要切换上下文。为了提高性能,我们直接使用公有字段。
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字符串中的一个字符,根据读到的内容进行单词转换、状态转换等逻辑。词法分析器的状态转换图如下:
状态转换图
下面分析每个状态中执行的逻辑:

  1. 就绪:读取下一字符,根据读取到的内容转换状态。
  2. 构造符:将对应的构造符单词加到队尾,回到就绪状态。
  3. 字面量:如果读到的是t,就向下读三个字符,若读完为true则将true单词加到队尾,回到就绪状态,否则抛出异常。false和null同理。
  4. 数字:一直读直到读到逗号或-1,并将每次读到的结果存入一个字符串中,将结果与一个表示数字的模式匹配,若匹配则将对应的数字单词加入队尾,回到就绪状态,否则抛出异常。
  5. 字符串:一直读直到读到双引号("不算),并将每次读到的结果存入一个字符串中,将对应的字符串单词加入队尾,回到就绪状态。
  6. 终止:向队尾加入终止单词,结束流程。

对应方法

我们将就绪和终止的逻辑放在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#对象的基本功能,不支持自定义特性,不支持自定义转换函数。而高级语法分析器的实现将在基本语法分析器全部实现之后,以区分主要功能和附加功能。

发布了27 篇原创文章 · 获赞 41 · 访问量 2066

猜你喜欢

转载自blog.csdn.net/DIAX_/article/details/104374728