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

Aure ·
更新时间:2024-09-20
· 657 次阅读

目录前言语法分析器接口基本语法分析器类泛型方法带Type参数的方法ParseObjectParseArrayParseBaseTypedynamic方法 前言

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

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

语法分析器接口

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

interface IParser { T ToObject(string json); object ToObject(System.Type type, string json); dynamic ToObject(string json); } 基本语法分析器类

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

internal class PrimaryParser : IParser { public T ToObject(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 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 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就无法构造默认对象。接下来循环以下步骤:

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

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

private object ParseArray(Queue 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(); 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> converters; public PrimaryParser() { InitializeConverters(); } private void InitializeConverters() { converters = new Dictionary<JsonBaseType, Dictionary>(); converters[JsonBaseType.Number] = new Dictionary(); converters[JsonBaseType.String] = new Dictionary(); converters[JsonBaseType.Boolean] = new Dictionary(); 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,可以发现它就是两个简单的步骤:

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

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

dynamic方法

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

private dynamic ParseObject(Queue 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) instance)[key] = value; token = ctx.Dequeue(); CheckSyntax(ref token, TokenType.Comma | TokenType.ObjectEnd); if (token.type == TokenType.ObjectEnd) { return instance; } } } private dynamic ParseArray(Queue ctx) { var token = ctx.Peek(); if (token.type == TokenType.ArrayEnd) { return Array.CreateInstance(typeof(object), 0); } object value; var elements = new List(); 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 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 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,将在以后修正),下回将实现高级语法分析器中的部分功能。


作者:DiaX



JSON json解析 语法分析器 C# 语法分析

需要 登录 后方可回复, 如果你还没有账号请 注册新账号