我的计算器——2 记号对象

来源:互联网 发布:中国为何封锁外国网络 编辑:程序博客网 时间:2024/05/22 05:23
2.记号对象
 
关键词:C# .NET 计算器 词法分析 语法分析 表达式计算 ConExpress Calculator
 
上一篇中提到了用树形结构来分析表达式并计算求值的思路。但对程序来说,输入的表达式只是一个字符串而已。要将表达式分析成树型结构,首先必须可以将表达式分解成一个个节点,然后才可以由节点组成树。这里将树上的每一个节点称之为记号对象TokenRecord。
 
根据上文分析得出,记号对象要求有一个存储自身值的变量,有自己特定的计算方法,还要能知道其下级的值。由此可以得出TokenRecord的基本信息(略去非关键信息):
属性
Index:在表达式中的列号,int类型,出错时用于指示错误所在,从1开始。
Priority:优先级,int类型,在分析表达式的时候需要。
TokenValueType:值类型,枚举类型TokenValueTypeEnum,可取String和Number两个值。(本来打算用泛型做,但除了数值和字符串可以在构造时指定类型,很多操作元素的值类型都无法确定,比如+可以是字符串,也可以是数值,只好用一个属性来标识了。不知有没有更好的办法。)
TokenString:字符值,string类型,如果该元素的值是字符串类型,则将值存储在该属性。
TokenNumber:数字值,double类型,如果该元素的值是数值类型,则将值存储在该属性。(另外,表达式中也存在逻辑运算,这里0代表false,否则代表true)
ChildList:下级列表,List对象,用来存储下级元素。
方法
Execute:执行该元素的操作,虚方法。
SetChildCount:设置下级数量,虚方法,用于检查下级数量合法性,在构造函数中调用。对应有一个m_ChildCount字段,用于存储下级数量。因为检查下级属于元素内部的任务,所以将m_ChildCount设置为protected,也没有对应的ChildCount属性。
SetPriority:设置优先级,虚方法,在构造函数中调用。
CheckChildCount:检查下级数量,在Execute中调用,保证表达式合法。
 
TokenRecord类的代码如下:
    /// <summary>    /// 记号值类型    /// </summary>    public enum TokenValueTypeEnum    {        String,        Number,    }    /// <summary>    /// 记号记录    /// </summary>    public abstract class TokenRecord    {        #region 属性和字段        //下级个数        protected int m_ChildCount;        private int m_Index;        /// <summary>        /// 列序号        /// </summary>        public int Index        {            get            {                return m_Index;            }        }        /// <summary>        /// 优先级,必须赋值        /// </summary>        protected int m_Priority;        /// <summary>        /// 优先级        /// </summary>        /// <returns></returns>        public int Priority        {            get            {                return m_Priority;            }        }                private TokenValueTypeEnum m_TokenValueType;        /// <summary>        /// 记号值类型,字符串/数值        /// </summary>        public TokenValueTypeEnum TokenValueType        {            get            {                return m_TokenValueType;            }            set            {                m_TokenValueType = value;            }        }        private string m_TokenString;        /// <summary>        /// 记号字符串        /// </summary>        public string TokenString        {            get            {                return m_TokenString;            }            set            {                m_TokenString = value;            }        }        private double m_TokenNumber = 0;        /// <summary>        /// 记号数值        /// </summary>        public double TokenNumber        {            get            {                return m_TokenNumber;            }            set            {                m_TokenNumber = value;            }        }        private List<TokenRecord> m_ChildList = new List<TokenRecord>();        /// <summary>        /// 下级列表        /// </summary>        public List<TokenRecord> ChildList        {            get            {                return m_ChildList;            }            set            {                m_ChildList = value;            }        }        #endregion        /// <summary>        /// 构造函数        /// </summary>        /// <param name="Index">序号</param>        public TokenRecord(int Index)        {            this.m_Index = Index;            this.SetPriority();            this.SetChildCount();        }        /// <summary>        /// 重写ToString方法        /// </summary>        /// <returns></returns>        public override string ToString()        {            return this.GetType().Name + "_" + GetValueString() + "_" + TokenValueType.ToString();        }        /// <summary>        /// 获取值的字符串表示        /// </summary>        /// <returns></returns>        public string GetValueString()        {            return this.TokenValueType == TokenValueTypeEnum.String ? this.TokenString : this.TokenNumber.ToString();        }        /// <summary>        /// 执行代码,必须重写        /// </summary>        public abstract void Execute();        /// <summary>        /// 设置下级数量        /// </summary>        protected abstract void SetChildCount();        /// <summary>        /// 设置优先级        /// </summary>        protected abstract void SetPriority();        /// <summary>        /// 检查下级数量,必要时可以重写,因为有些Token的下级数量可以是一个区间        /// </summary>        /// <param name="ErrorInformation">下级数量不符时显示的错误信息</param>        protected void CheckChildCount(string ErrorInformation)        {            if (this.m_ChildList.Count != this.m_ChildCount)            {                throw new ArgumentException(string.Format("语法错误,列{0},{1}。", this.m_Index.ToString(), ErrorInformation));            }        }        #region 转换记号值类型        /// <summary>        /// 将记号值转换为字符串类型        /// </summary>        internal void ChangeTokenToString()        {            if (this.TokenValueType == TokenValueTypeEnum.Number)            {                this.TokenValueType = TokenValueTypeEnum.String;                this.TokenString = this.TokenNumber.ToString();            }        }        /// <summary>        /// 将记号值转换为数字类型        /// </summary>        /// <param name="ErrorInformation">无法转换成数字时显示的错误信息</param>        internal void ChangeTokenToDouble(string ErrorInformation)        {            if (this.TokenValueType == TokenValueTypeEnum.String)            {                this.TokenValueType = TokenValueTypeEnum.Number;                double dblValue;                if (double.TryParse(this.TokenString, out dblValue))                {                    this.TokenNumber = dblValue;                }                else                {                    throw new ArithmeticException(string.Format("语法错误,列{0},{1}。" ,this.m_Index, ErrorInformation));//throw exception                }            }        }        /// <summary>        /// 将记号值转换为逻辑值        /// </summary>        internal void ChangeTokenToBoolean()        {            if (this.TokenValueType == TokenValueTypeEnum.String)            {                //Error or if string is not empty then value is true,else value is false                this.TokenValueType = TokenValueTypeEnum.Number;                switch (this.TokenString.Trim().ToLower())                {                    case "true":                        this.TokenNumber = 1;                        break;                    case "false":                    case "":                        this.TokenNumber = 0;                        break;                    default:                        this.TokenNumber = 1;                        break;                }                this.TokenNumber = this.TokenString.Trim().Equals(string.Empty) ? 0 : 1;            }            else            {                this.TokenNumber = (this.TokenNumber == 0 ? 0 : 1);            }        }        #endregion    }

设计出基类TokenRecord后,其他记号类都可以从它继承,然后实现自己的特定计算方法即可。那么实际的表达式中有哪些类型的记号呢?经过分析得出,表达式中所包含的的操作元素无非这几种:关键字,运算符,字符串,数字。

关键字:ifsincostruefalse等以英文字母开头的词,其中可以包含数字或者其他允许的符号

运算符:+-*//><>=<=&&||,!等,纯符号操作元素,可以由好几个字符组成

字符串:"hello"'good'"where's my book"等,由字符串标识符单引号或者双引号标识的字符串。字符串中允许包含当前字符串标识符之外另一种字符串标识符,即双引号字符串中可以包含单引号,单引号字符串中可以包含双引号,这一点和JavaScript类似。如果在字符串中包含当前标识符,则必须用连续两个标识符进行转义。

数值:1285642.123-62.45,允许包含小数点和负数。

进一步分析发现,字符串和数值有一定的相似性,就是它们并不需要计算,只需要作为一个存储单元即可。那么就可以将它们合并,称为值记号对象TokenValue。其他记号对象在Execute中实现自己的特定算法,而值记号对象的Execute方法中不做任何操作。

 

按照上面对操作元素的分类,从TokenRecord类衍生出一下几个类:TokenKeyword(对应关键字,抽象类),  TokenSymbol(对应运算符,抽象类), TokenValue(对应字符串和数值)。类图如下:

TokenRecord

 

 

然后再从这些类中衍生出更多的具体的类,比如TokenIf TokenSin TokenCos TokenPlus TokenMinus

以这个表达式为例,23.5+(54/3-9)*2,分析出来的TokenList记号对象列表就是

记号对象

对应表达式

TokenValue

23.5

TokenPlus

+

TokenLeftBracket

(

TokenValue

54

TokenDivide

/

TokenValue

3

TokenMinus

-

TokenValue

9

TokenRightBracket

)

TokenMultiply

*

TokenValue

2

 

这样就把传入的表达式字符串表示成程序可以识别的对象了,然后再对这些记号对象进行处理,最终得到计算结果。这一节只是介绍记号对象,至于怎么将表达式转换成记号对象,下一篇再详细介绍。

 

接下来是如何实现具体的TokenRecord类。以乘法记号类TokenMultiply为例,TokenMultiply继承自TokenArithmeticTokenArithmetic继承自TokenSymbolTokenSymbol继承自TokenRecord。类图如下:

 TokenSymbol

 

TokenSymbol类也是一个抽象类,只是将部分重复的设置实现了,减少其子类中重复的代码,TokenSymbol类的代码如下:

    public abstract class TokenSymbol : TokenRecord    {        public TokenSymbol(int Index)            : base(Index)        {            this.TokenValueType = TokenValueTypeEnum.Number;//部分操作符必须根据实际修改        }        protected override void SetChildCount()        {            this.m_ChildCount = 2;//Not必须根据实际修改        }        public abstract override void Execute();    }

TokenArithmetic类则是个完全的抽象类,只是为了进行分类而已,任何功能都没实现,其代码如下:

    public abstract class TokenArithmetic : TokenSymbol    {        public TokenArithmetic(int Index)            : base(Index)        {        }        public abstract override void Execute();    }

 

最后是TokenMultiply类,它实现具体的乘法操作,代码如下:

    public class TokenMultiply : TokenArithmetic    {        public TokenMultiply(int Index)            : base(Index)        {        }        protected override void SetPriority()        {            this.m_Priority = 3;//设置优先级,这里必须根据实际情况设置,保证优先级的正确性        }        public override void Execute()        {            this.CheckChildCount("乘法的运算元素数量不合法");//检查下级数量,不合法则抛出错误                          //经过上面的检查后,可以保证其包含两个下级            TokenRecord TokenFirst = this.ChildList[0];            TokenRecord TokenSecond = this.ChildList[1];                          //对下级求值,比如5*sin(45),就必须先对sin求值            TokenFirst.Execute();            TokenSecond.Execute();                          //保证乘法下级是数值,否则无法进行计算,如果转换失败则抛出错误            TokenFirst.ChangeTokenToDouble("乘法的运算元素不是数值");            TokenSecond.ChangeTokenToDouble("乘法的运算元素不是数值");                          //执行实际的运算并保存计算得到的值            this.TokenNumber = TokenFirst.TokenNumber * TokenSecond.TokenNumber;        }    }

  TokenRecord中的三个虚方法SetChildCount SetPriority Execute都得到了实现。这里每一个具体的记号都只负责自己内部的计算,降低了类和外部的联系,即高内聚低耦合。如果需要添加新的计算方法,只需要从TokenRecord继承,然后实现这三个方法即可,可以很容易的进行扩展。例如本程序中实现了三角函数sincos,但未实现tan。如果需要实现tan,只需要根据sin或者cos的实现方法去做就可以了。甚至可以再提炼出一个抽象类,让所有的三角函数计算都从它继承。 

今天就到此,下一篇将介绍如何将表达式字符串转换成程序可以处理的TokenRecord对象列表。