仿查询分析器的C#计算器——3.词法分析

承接上一篇,这一篇讲如何把表达式转换成记号对象,这里就涉及到了编译原理中的词法分析。关于编译原理我不想多讲,毕竟我自己也不怎么熟悉,现在只知道其中有个有限自动机的概念。不管什么概念,用代码实现才是最终目标。
因为不清楚字符串中到底包含什么字符,只能一个个字符进行处理,采用循环一次次向后取一个字符进行判断。这里建立一个TokenFactory记号“工厂”类,由这个类负责对表达式进行分析并“生产”出TokenRecord对象。其中包括两个方法,LexicalAnalysis和ProduceToken。LexicalAnalysis用于词法分析,分析到符合规则的记号对象后调用ProduceToken方法,“生产”出对应的TokenRecord对象。这里偷了一点懒,把所有方法全部写成了static,这样就不用实例化多个子类了。
从这个类衍生出多个子类:
TokenKeywordFactory:用于处理关键字
TokenSymbolFactory:用于处理运算符
TokenStringFactory:用于处理字符串
TokenNumberFactory:用于处理数字
类图如下:

分析表达式的入口只有一个,就是TokenFactory中的LexicalAnalysis。TokenFactory类的代码如下:
Code
在LexicalAnalysis方法中,只需要判断当前字符是否符合一定的起始规则,如果符合起始规则,就交给对应的工厂类去处理。
下面用例子来讲吧,比如123.3*2-(24+34),分析成记号对象如下:

记号对象

对应表达式

TokenValue

123.3

TokenMultiply

*

TokenValue

2

TokenMinus

-

TokenLeftBracket

(

TokenValue

24

TokenPlus

+

TokenValue

34

TokenRightBracket

)

这里的处理过程是
1.取字符“1”,转到TokenNumberFactory,把分析取到的字符串“123.3”转换为TokenNumber并存到TokenList中
2.取字符“*”,转到TokenSymbolFactory,把“*”转换成TokenMultiply并存到TokenList中
3.取字符“2”,转到TokenNumberFactory,把分析取到的字符串“2”转换为TokenNumber并存到TokenList中
4.取字符“- ”,转到TokenSymbolFactory,把“-”转换成TokenMinus并存到TokenList中
5.取字符“( ”,转到TokenSymbolFactory,把“(”转换成TokenLeftBracket并存到TokenList中
6.取字符“2”,转到TokenNumberFactory,把分析取到的字符串“24”转换为TokenNumber并存到TokenList中
7.取字符“+”,转到TokenSymbolFactory,把“+”转换成TokenPlus并存到TokenList中
8.取字符“3”,转到TokenNumberFactory,把分析取到的字符串“34”转换为TokenNumber并存到TokenList中
9.取字符“) ”,转到TokenSymbolFactory,把“)”转换成TokenRightBracket并存到TokenList中
至于各个工厂类中怎么分析提取出对应的字符串,则有各自不同的规则。如果符合规则就继续向后分析,否则代表分析结束,然后从源字符串中截取开始分析的序号到结束分析的序号之间的字符串即可。这里的Index参数相当于C++中的指针,指示当前分析到哪一个字符。因为各个“工厂”类需要在分析完一个记号后将指针后移,这里就将Index设置为ref类型。

另一个方法GetOperateTokenDictionary是用来获取记号字典的,字典的Key是运算符和关键字,Value是对应的类名称。在分析中遇到运算符和关键字的时候,通过查询字典就可以获取对应的类名称,然后通过反射生成类的实例,这样就可以灵活将操作符和类对应起来。字典的来源是本地的一个XML文件,当新增一个操作符的时候,到XML文件里注册一下,程序就可以识别出新操作符了,“工厂”类不需要做任何修改。如果需要修改操作符,可以直接在XML文件里面修改,程序也能识别,比如把mid改成substring,程序照样可以运行。这就是“依赖注入”的实际应用。
这里需要使用的XML如下:
Code

接下来介绍各个工厂类。首先是关键字工厂TokenKeywordFactory。当TokenFactory分析到英文字母的时候,把任务转交给TokenKeywordFactory。该类从分析得到的第一个英文字母开始向后分析,如果后面还是英文字母或者数字,则继续向后分析,否则结束分析。在这里关键字允许包含数字,但第一个字符必须是英文字母。分析结束后,截取分析得到的字符串,然后到交给ProduceToken方法产生一个TokenRecord类的实例。
在ProduceToken方法中,首先判断传入的字符串是否存在于关键字字典中,如果不存在则报错,如果存在则用反射产生一个对应类的实例。其中有些关键字是常量,所以进行了特殊处理,需要设置记号对象的值和类型。
以上工作完成后,调整分析指针的位置,回到TokenFactory类,执行后续分析。TokenKeywordFactory的代码如下:
Code
TokenSymbolFactory运算符工厂类的分析过程和TokenKeywordFactory基本相似。但是运算符一般只包含一个字符或者两个字符,就不需要一直向后分析了,只需要判断运算符字典中是否存在对应的项即可。另外有些操作符的第一个字符是重复的,比如“>”和“>=”,这时候就需要判断“>”之后是否还存在“=”。如果向后再截取一个字符,在字典中也存在对应项,则按双字符运算符处理,否则就是单字符运算符。TokenSymbolFactory的代码如下:
Code
TokenStringFactory字符串工厂类的分析是按照VB语法,只有引号需要转义,用两个引号即可,其他特殊字符不需要转义。而且引号和JavaScript一样,可以用大写或者小写,只要两端匹配就可以了。如果在单引号标记的字符串中包含双引号,则不需要转义,双引号标记的字符串中出现单引号也不需要转义。字符串的分析是以引号作为界定,向后分析的过程中,如果遇到与起始引号相同的字符,判断后面是否存在重复引号,存在则转义,不存在则结束分析。将两个引号之间的字符串截取,创建一个TokenValue对象,结束分析。TokenStringFactory代码如下:
Code
TokenNumberFactory数值工厂的分析最简单,主要是判断下一个字符是否是数字或者小数点。将分析得到的字符串转换为double类型,然后赋值给创建的TokenValue对象即可。TokenNumberFactory代码如下:
Code
通过这些“工厂”类就可以完成把表达式分析成记号对象列表,为程序理解表达式做好了基础工作。在下一篇中会介绍如何将记号对象分析成树视图,进而通过树视图逐级求解。
代码下载:https://files.cnblogs.com/conexpress/ConExpress_MyCalculator.rar
原文地址:https://www.cnblogs.com/conexpress/p/MyCalculator_03.html