[體驗編譯原理]編寫簡易計算器


Demo: CaculationTest

 

前言

有想過自己寫一個計算器么?輸入一些數學表達式就能自己計算解析生成結果。

如果沒有,可以現在開始想想,也許你會發現自己計算要不了幾秒鍾的表達式,讓程序計算卻沒這么簡單。

本文以簡化版的計算器為例,采用了編譯原理的token解析及分析的方式,旨在讓初學者了解和感受編譯原理的基本思維。

【如果有一定的基礎,可以閱讀此文:http://www.codeproject.com/Articles/246374/A-Calculation-Engine-for-NET】

 

假定

為了便於理解,我們現在簡化需求,數據類型只有整數,運算符只有加減乘除,沒有括號。運行結果如圖所示:

 

解析過程

逐個分析表達式字符串的每一個char,將其解析為一系列的token(記號)。然后根據token代表的不同含義進行相應的操作,直到計算出最終結果。

(本例中並沒有全部解析完token,再遍歷token,而是邊解析邊進行操作。這樣做效率稍微高一點,但不能直接查看解析出來的全部token。)

這和我們閱讀也比較接近:我們從左往右依次讀取信息,讀的過程中,我們會根據上下文即前后組成,形成一定的語義,如“其實不忍”,你可能會理解為”他實在不忍心“,或者理解為”他其實是不忍心的“,這得依照你對上下文的理解去選擇了。

對照剛才的比喻,可以得出,token是語義的基本組成,或者說對字符組成的一種抽象。程序中將token抽象為以下數據結構:

 enum TokenType
   {
       Add,Sub,
       Mul,Div,
       Int,
       Start,End
   }

   class Token
   {
       public TokenType Type;
       public object Value;

       public Token(TokenType type , object value = null)
       {
           Type = type;
           Value = value;
       }
   }

 

 

Token和表達式

Token必須解析為表達式才會有意義。有了表達式,我們才能計算出最終結果。一個表達式,是能表示明確意義的一個或一組token。

在C#中,我們有一元表達式,二元表達式;表達式中有不同的運算,如加減法;在此例中,所有的表達式都可以計算出某個值,而且表達式之間可以相互計算形成新的表達式,如”表達式(1*2)+表達式(2*3)”。基於此,有Expression類,也並不復雜:

abstract class Expression
    {
        public abstract int GetValue();
    }

    class UnaryExpress : Expression
    {
        int _value;

        public UnaryExpress(int value)
        {
            _value = value;
        }


        public override int GetValue()
        {
            return _value;
        }
    }


    class BinaryExpression : Expression
    {
        TokenType _tokenType;
        int _left;
        int _right;


        public BinaryExpression(TokenType tokenType, Expression left, Expression right)
        {
            _tokenType = tokenType;
            _left = left.GetValue();
            _right = right.GetValue();
        }

        public override int GetValue()
        {
            switch (_tokenType)
            {
                case TokenType.Add:
                    return _left + _right;
                case TokenType.Sub:
                    return _left - _right;
                case TokenType.Mul:
                    return _left * _right;
                case TokenType.Div:
                    return _left / _right;
                default:
                    throw new Exception("unexceptional token!");
            }
        }
    }

 

此例中,並沒有真正意義上的“一元表達式”,僅將數字看作它而已。二元表達式的值計算相對復雜,但類別也不多。

 

算法優先級

如果不算括號,恐怕對我們來說,用自己的“原始方式”解析表達式,最大的麻煩就是解決算法優先級的問題。

為什么“1+2*3” 不解析為1+2再乘以3呢,如何才能將其正確解析為2*3再和1相加呢?

首先我們需要順序分析每個token, 表達式的解析順序決定了最后的運算順序,看下原代碼中比較重要的這3個方法:

        //解析加減
        Expression ParseAddSub()
        {
            //左操作數為優先級較高的運算符
            var l = ParseMulDiv();
            while (_token.Type == TokenType.Add || _token.Type == TokenType.Sub)
            {
                var t = _token.Type;
                ParseToken();
                var r = ParseMulDiv();//解析右操作數
                l = new BinaryExpression(t, l, r);
            }
            return l;
        }

        //解析乘除
        Expression ParseMulDiv()
        {
            var l = ParseUnary();
            while (_token.Type == TokenType.Mul || _token.Type == TokenType.Div)
            {
                var t = _token.Type;
                ParseToken();
                var r=ParseUnary();
                l = new BinaryExpression(t, l, r);
            }
            return l;
        }

        //解析一元表達式(目前只有單個整數)
        Expression ParseUnary()
        {
            Expression ret = null;
            if (_token.Type == TokenType.Int)
            {
                ret= new UnaryExpress((int)_token.Value);
            }

            //解析完int后,移到下一個token處,即+-*/
            ParseToken();
            return ret;
        }

 

1*2+2*2,我們可以看作兩個乘法表達式相加,所以,解析加法運算的左右操作符前,程序嘗試讀取其是否是乘法表達式。

如果是1+2*3呢,左操作符當乘法運算解析時,並沒有匹配上,由最高優先級的一元表達式決定其值,返回1,所以左操作數就是1了,當解析到+時,程序嘗 試解析右操作數,從優先級比加法高一級的乘除開始,往上搜索匹配。很明顯,2*3命中了乘法表達式,計算出右操作數的結果后,再和1相加,結果就正確了。

 

練習

如果研究透了demo,可以嘗試把括號,取模運算,一元表達式(正負號)。

 

擴展

在IL代碼和LinqExpression API中,“表達式”,“二元表達式”,“賦值表達式”,“成員獲取表達式(.運算)”等,都很常見,解析的表達后對應的操作也很多,新建實例,引用實 例,訪問局部變量等,賦值等。有興趣可以查看一下Dynamic Linq的原代碼(后續章節中,我們會寫一個簡單易版的Dynamic Linq動態計算 IQueryable.Where(string))。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM