本次我们实现解析器的语法分析功能。
注意:示例代码使用了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对象,没有具体的类型,泛型方法就废了。
这个方法是我们的重头戏,核心逻辑全在这里(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,将在以后修正),下回将实现高级语法分析器中的部分功能。