本系列文章介紹如何用C#實(shí)現(xiàn)一個(gè)類似于查詢分析器的計(jì)算器。該計(jì)算器接受表達(dá)式輸入,支持多行表達(dá)式,可選擇部分表達(dá)式進(jìn)行計(jì)算,能定位語法錯(cuò)誤的位置,并且支持?jǐn)?shù)值、字符串和邏輯值的計(jì)算,內(nèi)置多種運(yùn)算符和函數(shù),并且可以根據(jù)需要擴(kuò)展出更多的運(yùn)算符和函數(shù)。程序中包含一些細(xì)節(jié)上的bug,有興趣的朋友可以完善一下。
本篇介紹如何調(diào)用之前實(shí)現(xiàn)的詞法分析和語法分析類以實(shí)現(xiàn)計(jì)算,以及如何在界面上實(shí)現(xiàn)多行表達(dá)式計(jì)算、選中部分表達(dá)式計(jì)算和錯(cuò)誤定位。
代碼下載:https://files.cnblogs.com/conexpress/ConExpress_MyCalculator.rar
前面幾篇文章介紹了各種分析過程,本篇作為完結(jié)篇,介紹如何調(diào)用之前實(shí)現(xiàn)的代碼,如何實(shí)現(xiàn)多行表達(dá)式或者選擇部分表達(dá)式進(jìn)行運(yùn)算,以及如何定位錯(cuò)誤。
本程序可以不需要UI界面,獨(dú)立成一個(gè)模塊。如果表達(dá)式分析與計(jì)算功能打包成一個(gè)dll,那入口只有一個(gè),SyntaxAnalyse類。new一個(gè)SyntaxAnalyse類之后,調(diào)用其中的Analyse方法,將要計(jì)算的運(yùn)算表達(dá)式作為參數(shù)傳遞進(jìn)去,返回一個(gè)頂級(jí)TokenRecord對(duì)象,再根據(jù)返回的TokenRecord對(duì)象的值類型取得結(jié)果,整個(gè)計(jì)算過程就完成了,使用起來非常方便。

Code
/// <summary>
/// 表達(dá)式分析計(jì)算類,功能入口
/// </summary>
/// <remarks>Author:Alex Leo</remarks>
public class SyntaxAnalyse
{
/// <summary>
/// 構(gòu)造函數(shù)
/// </summary>
/// <remarks>Author:Alex Leo; Date:2007-8-2</remarks>
public SyntaxAnalyse()
{ }
/// <summary>
/// 分析語句并返回記號(hào)記錄對(duì)象
/// </summary>
/// <param name="Code">運(yùn)算表達(dá)式</param>
/// <returns>頂級(jí)TokenRecord對(duì)象</returns>
public TokenRecord Analyse(string Code)
{
if (Code.Trim().Equals(string.Empty))
{
return new TokenValue(0,1);
}
List<TokenRecord> ListToken = new List<TokenRecord>();//TokenRecord列表
int intIndex = 0;
TokenFactory.LexicalAnalysis(ListToken, Code, ref intIndex);//詞法分析,將代碼轉(zhuǎn)換為TokenRecord列表
//語法樹分析,將Token列表按優(yōu)先級(jí)轉(zhuǎn)換為樹
TokenRecord TokenTop = SyntaxTreeAnalyse.SyntaxTreeGetTopTokenAnalyse(ListToken, 0, ListToken.Count - 1);
TokenTop.Execute();
return TokenTop;
}
}
從代碼中可以看出,首先是詞法分析,得到一個(gè)記號(hào)對(duì)象列表List<TokenRecord>,然后進(jìn)行語法分析,調(diào)用SyntaxTreeAnalyse的SnytaxTreeGetTopTokenAnalyse方法,分析出頂級(jí)記號(hào)對(duì)象,這樣一棵樹就出來了。接下來執(zhí)行頂級(jí)節(jié)點(diǎn)的Execute方法,該方法中首先會(huì)執(zhí)行下級(jí)節(jié)點(diǎn)的Execute方法,然后再針對(duì)下級(jí)節(jié)點(diǎn)的值執(zhí)行自身的運(yùn)算。所有的TokenRecord都是這樣的模式,逐級(jí)遞歸調(diào)用,最后得到計(jì)算結(jié)果。TokenRecord基類中包含一個(gè)object類型的Value屬性和一個(gè)Type類型的TokenValueType屬性,通過這兩個(gè)屬性可以得到具體的值及其類型,然后做下一步處理。因?yàn)檫@里不只能執(zhí)行數(shù)學(xué)運(yùn)算,還能做字符串和邏輯值運(yùn)算,所以必須通過TokenValueType來確定值的類型。如果只需要實(shí)現(xiàn)數(shù)學(xué)運(yùn)算,程序會(huì)簡(jiǎn)單一些。
窗體的調(diào)用也很簡(jiǎn)單,并沒有設(shè)計(jì)漂亮的外觀和高級(jí)設(shè)置等。主要的代碼是“計(jì)算”按鈕的Click事件處理方法,代碼如下:

Code
/// <summary>
/// 點(diǎn)擊“計(jì)算”按鈕
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnExecute_Click(object sender, EventArgs e)
{
if (this.rtbInput.Text.Trim().Replace("\n", "").Length == 0)
{
this.rtbOutput.Text = "輸入的表達(dá)式不能為空,請(qǐng)重新輸入。";
}
else
{
string strSource;
int intTotalIndex = 0;
this.rtbOutput.Text = "";
string[] strLines;
this.trvSyntaxTree.Nodes.Clear();//清空語法樹
if (this.rtbInput.SelectedText.Trim().Length == 0)//獲取選中的代碼,如果未選中,則執(zhí)行全部
{
strSource = this.rtbInput.Text;
}
else
{
strSource = this.rtbInput.SelectedText;
intTotalIndex = this.rtbInput.SelectionStart;
}
if (this.chkAllowMultiLine.Checked)//判斷是按多行執(zhí)行還是單行執(zhí)行
{
strLines = strSource.Split(new char[] { '\n' });//多行則用換行符分割成多行
}
else
{
strLines = new string[] { strSource.Replace("\n", "") };//單行則移除換行符成一行
}
foreach (string Line in strLines)
{
if (Line.Trim().Length != 0)//避免中間出現(xiàn)空行
{
try
{
TokenRecord TokenTop = myAnalyse.Analyse(Line);//計(jì)算表達(dá)式
this.rtbOutput.Text += TokenTop.GetValueString() + "\n";//顯示計(jì)算結(jié)果
this.LoadSyntaxTree(TokenTop);//加載語法樹到TreeView控件
}
catch (Exception ex)
{
this.rtbOutput.Text += "發(fā)生錯(cuò)誤\n" + ex.Message + "\n";//顯示錯(cuò)誤信息
if (ex is SyntaxException)//如果是語法錯(cuò)誤,則選中錯(cuò)誤的代碼
{
SyntaxException myException = (SyntaxException)ex;
this.ActiveControl = this.rtbInput;//設(shè)置輸入框?yàn)榧せ羁丶?/span>
this.rtbInput.Select(myException.Index + intTotalIndex, myException.Length);//定位發(fā)生錯(cuò)誤的字符串
}
return;
}//try
}//if
intTotalIndex += Line.Length + 1;
}//foreach
}//else
}//btnExecute_Click
代碼中包含詳細(xì)的注釋,這里做簡(jiǎn)要說明。未選中輸入框中的文本則執(zhí)行全部代碼,否則執(zhí)行選中部分的代碼。將要執(zhí)行的代碼根據(jù)是否計(jì)算多行進(jìn)行分解,存放在一個(gè)字符串?dāng)?shù)組中。然后對(duì)表達(dá)式數(shù)組循環(huán)計(jì)算。如此實(shí)現(xiàn)了選擇部分表達(dá)式計(jì)算以及多行表達(dá)式計(jì)算。另外如何實(shí)現(xiàn)錯(cuò)誤定位,則是通過捕獲錯(cuò)誤。程序中定義了一個(gè)Exception類,但進(jìn)行詞法分析和語法分析的時(shí)候,如果發(fā)生錯(cuò)誤,則會(huì)拋出該異常。通過該異常類中的錯(cuò)誤序號(hào)以及長(zhǎng)度,就可以選中輸入框中的錯(cuò)誤部分。但是這里只能選中第一次發(fā)生的錯(cuò)誤,不能像VS.NET的IDE一樣捕獲所有錯(cuò)誤。Exception類的定義如下:

Code
/// <summary>
/// 語法錯(cuò)誤類,用于發(fā)生錯(cuò)誤時(shí)提示用戶并選中錯(cuò)誤的操作符
/// </summary>
/// <remarks>Author:Alex Leo; Date:2008-5-21;</remarks>
public class SyntaxException : Exception
{
private int m_Index;
/// <summary>
/// 錯(cuò)誤列號(hào)
/// </summary>
/// <remarks>Author:Alex Leo; Date:2008-5-21;</remarks>
public int Index
{
get { return m_Index; }
}
private int m_Length;
/// <summary>
/// 錯(cuò)誤操作符長(zhǎng)度
/// </summary>
/// <remarks>Author:Alex Leo; Date:2008-5-21;</remarks>
public int Length
{
get { return m_Length; }
}
private string m_Message;
/// <summary>
/// 錯(cuò)誤信息
/// </summary>
public override string Message
{
get { return m_Message; }
}
/// <summary>
/// 構(gòu)造函數(shù)
/// </summary>
/// <param name="Index">錯(cuò)誤處的列號(hào)(用于錯(cuò)誤時(shí)確定錯(cuò)誤操作符起始位置)</param>
/// <param name="Length">錯(cuò)誤操作符長(zhǎng)度(用于錯(cuò)誤時(shí)選擇錯(cuò)誤操作符的長(zhǎng)度)</param>
/// <param name="ErrorInformation">錯(cuò)誤信息</param>
public SyntaxException(int Index, int Length, string ErrorInformation)
{
this.m_Index = Index;
this.m_Length = Length;
this.m_Message = ErrorInformation;
}
}
單行多行切換只需要設(shè)置窗體的AcceptButton屬性為“計(jì)算按鈕”即可,這樣在單行狀態(tài)下,用戶回車就相當(dāng)于點(diǎn)擊“計(jì)算按鈕”。而按“F5”鍵執(zhí)行計(jì)算則是通過檢測(cè)輸入框的KeyUp事件,當(dāng)釋放“F5”鍵時(shí)用代碼去執(zhí)行“計(jì)算”按鈕的Click操作實(shí)現(xiàn)計(jì)算。
另外這里有一個(gè)語法樹分析,是為了顯示語法樹的結(jié)構(gòu),用更直觀的方法來驗(yàn)證分析是否正確。樹節(jié)點(diǎn)的文本是調(diào)用TokenRecord的ToString方法得到的,如果需要顯示為其他信息,也可以自行修改。
到這里本系列文章就結(jié)束了,其中包含了一些編程的技巧,希望對(duì)看了本系列文章的各位有幫助。


代碼下載:https://files.cnblogs.com/conexpress/ConExpress_MyCalculator.rar
Author:Alex Leo
Email:conexpress@qq.com
Blog:http://conexpress.cnblogs.com/