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

前言

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

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

语法分析器接口

因为我们要通过多态来实现两种语法分析器的分离,所以提取一个语法分析器接口IParser:

interface IParser
{
    T ToObject<T>(string json);
    object ToObject(System.Type type, string json);
    dynamic ToObject(string json);
}

基本语法分析器类

创建基本语法分析器类PrimaryParser,实现IParser接口,现在的代码是这种情况:

internal class PrimaryParser : IParser
{
    public T ToObject<T>(string json)
    {
        throw new NotImplementedException();
    }

    public object ToObject(Type type, string json)
    {
        throw new NotImplementedException();
    }

    public dynamic ToObject(string json)
    {
        throw new NotImplementedException();
    }
}

现在我们就来逐个实现这三个方法。

泛型方法

首先是泛型方法,这个方法最简单,就一行代码:

return (T) ToObject(typeof(T), json);

我靠!那你为毛不干脆给其中一个就得了?很简单,泛型方法的类型是编译期确定的,如果用户在编译期就明确知道要转换的具体类是谁,泛型方法确实很强。那要是这样呢?
那要是这样呢?
用户要通过外部输入来确定实例化Parent还是A还是B,这时候用户在编译期只有一个Type对象,没有具体的类型,泛型方法就废了。

带Type参数的方法

这个方法是我们的重头戏,核心逻辑全在这里(dynamic方法也只是在它的基础上稍作修改)。

那么就让我们来看一下这个神奇的方法:

public object ToObject(Type type, string json)
{
    return SwitchParse(Lexer.Analyze(json), type);
}

怎么还是一句话!?你丫的糊弄老子是不是?不不不,你想想Json对应的是哪种数据结构,没错就是树。实际上,词法分析器输出的单词流正是Json的前序遍历序列,我们只需要把这个序列还原成树就行了。SwitchParse则是在递归解析每个Json结点,于是干货来了:

private object SwitchParse(Queue<Token> ctx, Type ttype)
{
    var token = ctx.Dequeue();
    return token.type switch
    {
        TokenType.ObjectStart => ParseObject(ctx, ttype),
        TokenType.ArrayStart => ParseArray(ctx, ttype),
        TokenType.Number => ParseBaseType(ref token, JsonBaseType.Number, ttype),
        TokenType.String => ParseBaseType(ref token, JsonBaseType.String, ttype),
        var t when t == TokenType.True || t == TokenType.False
        => ParseBaseType(ref token, JsonBaseType.Boolean, ttype),
        TokenType.Null => ParseBaseType(ref token, JsonBaseType.Null, ttype),
        _ => throw new JsonException(ref token)
    };
}

这个方法从上下文读取一个单词,根据单词的类型将上下文转发给相应的解析方法:如果读到对象起始符,则说明下面一段单词序列表示的是一个对象结点,所以调用解析对象方法;如果读到数组起始符,则说明下面一段单词序列表示的是一个数组结点,所以调用解析数组方法…

所以,这个方法仍然只是一个“指挥中心”,真正的解析逻辑隐藏在ParseXXX方法中。

ParseObject

这是解析对象的方法,代码如下:

private object ParseObject(Queue<Token> ctx, Type ttype)
{
    var instance = Activator.CreateInstance(ttype);
    var token = ctx.Peek();
    if (token.type == TokenType.ObjectEnd)
    {
        return instance;
    }
    string key;
    PropertyInfo prop;
    object value;
    while (true)
    {
        token = ctx.Dequeue();
        CheckSyntax(ref token, TokenType.String);
        key = token.value;
        prop = ttype.GetProperty(key);
        if (prop == null || !prop.CanWrite) continue;
        token = ctx.Dequeue();
        CheckSyntax(ref token, TokenType.Colon);
        value = SwitchParse(ctx, prop.PropertyType);
        prop.SetValue(instance, value);
        token = ctx.Dequeue();
        CheckSyntax(ref token, TokenType.Comma | TokenType.ObjectEnd);
        if (token.type == TokenType.ObjectEnd)
        {
            return instance;
        }
    }
}

简单讲解一下这段代码:首先用Activator创建一个所需类型的对象,紧接着读取下一个单词看看是不是对象终止符。如果是,则直接返回默认对象。这里有一个假设:目标类有无参构造器,如果没有,Activator就无法构造默认对象。接下来循环以下步骤:

  1. 解析键并反射查找对应的属性,若未找到或属性不可写,则跳过本轮循环。
  2. 读取并跳过冒号。
  3. 递归解析值并设置到对应的属性。
  4. 读取下一个单词,如果是逗号,则继续循环;如果是对象终止符,则结束循环返回该对象。

ParseArray

这是解析数组的方法,代码如下:

private object ParseArray(Queue<Token> ctx, Type ttype)
{
    if (!ttype.IsArray)
    {
        throw new JsonException("目标属性不是数组类型");
    }
    var etype = ttype.GetElementType();
    var token = ctx.Peek();
    if (token.type == TokenType.ArrayEnd)
    {
        return Array.CreateInstance(etype, 0);
    }
    object value;
    var elements = new List<object>();
    while (true)
    {
        value = SwitchParse(ctx, etype);
        elements.Add(value);
        token = ctx.Dequeue();
        CheckSyntax(ref token, TokenType.Comma | TokenType.ArrayEnd);
        if (token.type == TokenType.ArrayEnd)
        {
            var instance = Array.CreateInstance(etype, elements.Count);
            for (int i = 0; i < elements.Count; i++)
            {
                instance.SetValue(elements[i], i);
            }
            return instance;
        }
    }
}

过程和解析对象大同小异,只是多了一个获取元素类型的步骤,少了一个解析键的步骤。

ParseBaseType

这是解析Json基本类型的方法,代码如下:

private object ParseBaseType(ref Token token, JsonBaseType jbtype, Type ttype)
{
    if (jbtype == JsonBaseType.Null)
    {
        return ttype.IsClass switch
        {
            true => null,
            false => throw new JsonException("值类型不能为null")
        };
    } else if (converters[jbtype].ContainsKey(ttype))
    {
        return converters[jbtype][ttype].Invoke(token.value);
    } else
    {
        throw new JsonException($"未找到从{Enum.GetName(typeof(JsonBaseType), jbtype)}到{ttype.FullName}的转换函数");
    }
}

这里用到了一个converters变量,这是一张记录了从Json基本类型到C#类型的转换函数的表。前面也说过,Json基本类型和C#类型之间不是一一映射,所以需要根据目标类型来动态选择转换函数。converters是PrimaryParser的一个字段,在构造时初始化:

private Dictionary<JsonBaseType, Dictionary<Type, ObjectConverter>> converters;

public PrimaryParser()
{
    InitializeConverters();
}

private void InitializeConverters()
{
    converters = new Dictionary<JsonBaseType, Dictionary<Type, ObjectConverter>>();
    converters[JsonBaseType.Number] = new Dictionary<Type, ObjectConverter>();
    converters[JsonBaseType.String] = new Dictionary<Type, ObjectConverter>();
    converters[JsonBaseType.Boolean] = new Dictionary<Type, ObjectConverter>();

    converters[JsonBaseType.Number][typeof(sbyte)] = value => Convert.ToSByte(value);
    converters[JsonBaseType.Number][typeof(short)] = value => Convert.ToInt16(value);
    converters[JsonBaseType.Number][typeof(int)] = value => Convert.ToInt32(value);
    converters[JsonBaseType.Number][typeof(long)] = value => Convert.ToInt64(value);
    converters[JsonBaseType.Number][typeof(decimal)] = value => Convert.ToDecimal(value);
    converters[JsonBaseType.Number][typeof(byte)] = value => Convert.ToByte(value);
    converters[JsonBaseType.Number][typeof(ushort)] = value => Convert.ToUInt16(value);
    converters[JsonBaseType.Number][typeof(uint)] = value => Convert.ToUInt32(value);
    converters[JsonBaseType.Number][typeof(ulong)] = value => Convert.ToUInt64(value);
    converters[JsonBaseType.Number][typeof(float)] = value => Convert.ToSingle(value);
    converters[JsonBaseType.Number][typeof(double)] = value => Convert.ToDouble(value);
    converters[JsonBaseType.Number][typeof(object)] = value => Convert.ToDouble(value);

    converters[JsonBaseType.String][typeof(string)] = value => value;
    converters[JsonBaseType.String][typeof(char[])] = value => value.ToCharArray();
    converters[JsonBaseType.String][typeof(object)] = value => value;

    converters[JsonBaseType.Boolean][typeof(bool)] = value => Convert.ToBoolean(value);
    converters[JsonBaseType.Boolean][typeof(object)] = value => Convert.ToBoolean(value);
}

可以看到,这张表里记录了一些基本的转换函数。如果用户不需要高级功能,这些已经足够了。

还有一个JsonBaseType类,这是一个枚举类,表示Json的基本数据类型:

internal enum JsonBaseType
{
    None,
    Number,
    String,
    Boolean,
    Null
}

回到ParseBaseType,可以发现它就是两个简单的步骤:

  1. 判断Json基本类型是不是null,如果是,则再看目标类型是不是类(引用类型),如果是,则设为null;如果不是,则抛出异常(值类型不能设为null)。增加这一步判断的原因是:所有引用类型都可以为null,而我们不可能将所有引用类型都注册到表里。
  2. 查表并调用对应的转换函数,若表中没有,则抛出异常。

这就是语法分析的全部逻辑,它实际上就是在还原一个前序遍历序列,总体来说还是比较容易理解的。把所有函数调用代换成相应的代码,可以发现,这就是一大串递归代码:如果读取到对象或数组,则递归向下解析;如果读到基本类型,则直接解析出数据,终止递归。

dynamic方法

dynamic方法就是上面的动态版本,代码如下,不再赘述:

private dynamic ParseObject(Queue<Token> ctx)
{
    dynamic instance = new ExpandoObject();
    var token = ctx.Peek();
    if (token.type == TokenType.ObjectEnd)
    {
        return instance;
    }
    string key;
    object value;
    while (true)
    {
        token = ctx.Dequeue();
        CheckSyntax(ref token, TokenType.String);
        key = token.value;
        token = ctx.Dequeue();
        CheckSyntax(ref token, TokenType.Colon);
        value = SwitchParse(ctx);
        ((IDictionary<string, object>) instance)[key] = value;
        token = ctx.Dequeue();
        CheckSyntax(ref token, TokenType.Comma | TokenType.ObjectEnd);
        if (token.type == TokenType.ObjectEnd)
        {
            return instance;
        }
    }
}

private dynamic ParseArray(Queue<Token> ctx)
{
    var token = ctx.Peek();
    if (token.type == TokenType.ArrayEnd)
    {
        return Array.CreateInstance(typeof(object), 0);
    }
    object value;
    var elements = new List<object>();
    while (true)
    {
        value = SwitchParse(ctx);
        elements.Add(value);
        token = ctx.Dequeue();
        CheckSyntax(ref token, TokenType.Comma | TokenType.ArrayEnd);
        if (token.type == TokenType.ArrayEnd)
        {
            dynamic instance = Array.CreateInstance(typeof(object), elements.Count);
            for (int i = 0; i < elements.Count; i++)
            {
                instance[i] = elements[i];
            }
            return instance;
        }
    }
}

private dynamic ParseBaseType(ref Token token, JsonBaseType jbtype)
{
    var ttype = jbtype switch
    {
        JsonBaseType.Number => typeof(double),
        JsonBaseType.String => typeof(string),
        JsonBaseType.Boolean => typeof(bool),
        JsonBaseType.Null => null,
        _ => throw new JsonException(ref token)
    };
    return converters[jbtype][ttype].Invoke(token.value);
}

private dynamic SwitchParse(Queue<Token> ctx)
{
    var token = ctx.Dequeue();
    return token.type switch
    {
        TokenType.ObjectStart => ParseObject(ctx),
        TokenType.ArrayStart => ParseArray(ctx),
        TokenType.Number => ParseBaseType(ref token, JsonBaseType.Number),
        TokenType.String => ParseBaseType(ref token, JsonBaseType.String),
        var t when t == TokenType.True || t == TokenType.False
        => ParseBaseType(ref token, JsonBaseType.Boolean),
        TokenType.Null => ParseBaseType(ref token, JsonBaseType.Null),
        _ => throw new JsonException(ref token)
    };
}

语法分析器的基本功能到这里已经全部实现了(发现了一些bug,将在以后修正),下回将实现高级语法分析器中的部分功能。

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

猜你喜欢

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