2500行代碼實現高性能數值表達式引擎 git
南京都昌信息科技有限公司 袁永福 2018-9-23github
Http://www.dcwriter.cn架構
◆◆前言編輯器
在一些高自由度的軟件中,特別是報表之類的軟件。須要讓用戶自定義數值表達式,好比定義"A+B*C-D/E",而後再實際運行中把具體的A,B,C,D,E的值代入表達式運算。這能顯著增長軟件的運行時的可配置性,是一個值得普遍應用的軟件功能。本文就說明了如何使用2500行C#代碼實現一種高性能的數值運算表達式引擎。函數
完整源代碼下載頁面https://github.com/dcsoft-yyf/DCSoft.Expression。性能
◆◆ANTLR引擎spa
提到數值表達式引擎,不得不提起Antlr,一個很著名的開源軟件,能自動生成源代碼來生成語法解析引擎,而後能夠在這個語法解析引擎的基礎上來實現運算表達式。3d
筆者此前也在使用ANTLR相關的代碼來實現了運算表達式引擎,包含了10000行C#源代碼。不過代碼晦澀難懂,改進更加麻煩。近期在處理一個包含大量表達式的場景時出現了性能問題,須要改進。code
◆◆新表達式運算引擎orm
如今做者拋棄了舊的基於ANTLR的引擎,構造了全新的表達式運算引擎,核心模塊只有2500行C#代碼,生成的程序集文件只有27KB,但運行速度提高了10倍。並且程序簡單易懂,擴展性強。固然再也不具有ANTLR的普遍的通用性,但足夠應付做者遇到的應用場景了。
新源代碼中定義的主要類型有:
DCConstExpressionItem |
常量表達式元素。 |
DCExpression |
表達式對象,爲頂級API類型。 |
DCExpressionItem |
抽象的表達式元素類型。全部的表達式元素都是從這個類型派生出來的。 |
DCExpressionItemList |
表達式元素列表。 |
DCFunctionExpressionItem |
函數調用表達式元素。 |
DCGroupExpressionItem |
表達式元素組。 |
DCOperatorExpressionItem |
運算操做符表達式元素。 |
DCToken |
表達式語法中的標識符對象。 |
DCTokenList |
標識符列表 |
DCVariableExpressionItem |
變量表達式元素。 |
IDCExpressionContext |
運行表達式時的上下文對象接口。 |
新引擎工做過程以下:
◆第一步,表達式字符串符號解析
其代碼定義在DCToken.cs中。在此處,將字符分爲如下幾種:
標識符 |
包括0到9的數字字符,英文字母字符,$符號,其餘標識符類型的字符。 |
操做符 |
包括"+-*/%\"字符。 |
邏輯運算符 |
包括"&^|=><"字符。 |
分組開始字符 |
爲字符"("。 |
分組結束字符 |
爲字符")"。 |
全部相鄰的同類型的字符合並在一塊兒成爲一組符號,此外還識別單引號或雙引號做爲邊界的字符串常量。
如下是符號解析的例子:
原始文本 |
解析結果 |
A+B |
"A" , "+" , "B" |
A+SIN(B)*99 |
"A" , "+" , "SIN" , "(" , "B" , ")" , "*" ,"99" |
A+B>98 && C<10 |
"A" , "+" , "B" , ">" , "98" , "&&" , "C" , "<" , "10" |
體重/(身高*身高) |
"體重" , "/" , "(" , "身高" , "*" , "身高" , ")" |
◆第二步,解析表達式單元元素
其代碼定義在DCExpression.cs中的 Parse()/ParseItem()函數中。將一個個標識符轉換爲表達式單元元素。目前支持如下元素類型:
DCConstExpressionItem |
常量元素類型。分爲字符串/數字/布爾值三種類型。 |
DCFunctionExpressionItem |
函數元素類型。用於調用外部函數。 |
DCGroupExpressionItem |
分組元素類型。由一對圓括號定義的子單元元素組合。 |
DCOperatorExpressionItem |
操做符元素。 |
DCVariableExpressionItem |
變量元素類型。由外部傳入具體的變量值。 |
解析過程是一種遞歸操做,用於構造出一個表達式元素樹狀結構。
首先定義一個DCGroupExpressionItem做爲根元素。能夠看做把表達式的最外頭放了一組圓括號。好比把"A+B"看成"(A+B)"。
在這個過程當中,遇到標識符而且緊跟着的是"("則認爲是函數元素;遇到標識符"true"或"false"則認爲是布爾類型的常量元素;遇到能夠轉換爲數字的標識符則認爲是數字常量元素;遇到其餘標識符則認爲是變量元素;遇到操做標識符則認爲是操做符元素類型;遇到"("則爲分組元素類型並遞歸解析子元素;遇到")"則認爲結束分組元素類型,結束當前遞歸。此外還解析出字符串常量元素。
通過解析操做,構造了只有一個根節點的表達式元素樹狀結構。如下是幾個例子:
A+B |
|
A+B*SIN(C+D) |
|
體重/(身高*身高) |
|
◆第三步,優先級調整
數值運算有優先級。規則是數學運算高於邏輯運算,乘除運算高於加減運算,圓括號能夠改變運算優先級。
此時須要將一長串的表達式元素按照優先級在這次整理。主要代碼在DCExpression.cs中的CollpaseItems()中,做者稱之爲表達式元素列表的收縮。其過程以下:
首先是特別處理減號元素的處理。當減號處於當前組的第一的位置,或者其前面是邏輯運算符時,則將減號元素轉換爲取負元素。
而後對元素列表進行一個不定次數的循環。在循環體中,找到優先級最高的並且未經收縮的操做符元素,而後吞併左邊和右邊的元素。如此循環直到無法產生吞併操做爲止。
舉個例子,對於表達式"A+B*C-D",則生成的表達式元素有
在第一次循環中,*號是優先級最高的,則進行收縮處理,處理後的結構以下:
上圖中黃色元素表示該元素收縮處理過了,再也不參與處理。
在第二次循環中,"+"號優先級最高(後面的"-"和它是同等級的),則對它進行收縮,結果以下:
在第三次循環中,"-"號優先級最高,則進行收縮,結果以下:
以後根節點下再無能夠處理的運算符元素,則退出循環。
通過上述步驟,最爲關鍵的表達式元素樹狀列表產生出來了。
◆第四步,運行表達式
本功能的入口點在DCExpression.cs中的Eval()中,內部調用了根元素的Eval(),而後遞歸調用個子元素的Eval(),得到最終的運算結果。
運算過程須要一個運行環境上下文對象,上下午對象只有2個功能:執行外界函數和取變量值。
爲此定義了IDCExpressionContext接口,其代碼以下:
namespace DCSoft.Expression { /// <summary> /// 表達式執行上下文對象 /// </summary> [System.Runtime.InteropServices.ComVisible( false )] public interface IDCExpressionContext { /// <summary> /// 執行函數 /// </summary> /// <param name="name">函數名</param> /// <param name="parameters">參數列表</param> /// <returns>函數返回值</returns> object ExecuteFunction(string name, object[] parameters); /// <summary> /// 得到變量值 /// </summary> /// <param name="name">變量名</param> /// <returns>變量值</returns> object GetVariableValue(string name); } }
應用程序只需定義一個實現了該接口的類型便可傳遞給表達式執行引擎便可進行數值表達式運算了。
◆◆應用
這個表達式引擎完成後,開發者就能很方便的使用這個引擎了。首先是定義實現了IDCExpressionContext接口的上下文對象。其代碼以下:
private class MyContext : IDCExpressionContext { public object ExecuteFunction(string name, object[] parameters) { name = name.ToUpper(); try { switch (name) { case "SIN": return Math.Sin(Convert.ToDouble(parameters[0])); case "MAX": return Math.Max( Convert.ToDouble(parameters[0]), Convert.ToDouble(parameters[1])); default: MessageBox.Show("不支持的函數" + name); return 0; } } catch (System.Exception ext) { MessageBox.Show("執行函數" + name + " 錯誤:" + ext.Message); return 0; } } public object GetVariableValue(string name) { name = name.ToUpper(); switch( name ) { case "A":return 1.5; case "B": return 2.3; case "C": return -99; case "D": return 998; case "E": return 123.5; case "F": return 3.1415; default: MessageBox.Show("不支持的參數名:" + name); return 0; } } }
在這個上下文中,定義了函數SIN和MAX的執行過程,並定義了變量A,B,C,D,E,F的具體的數值
而後咱們能夠使用如下代碼來調用表達式引擎了。
private void Form1_Load(object sender, EventArgs e) { cboExpression.Items.Add("SIN(99+123)"); cboExpression.Items.Add("A*99.1+MAX(-11,-10)"); cboExpression.Items.Add("FIND('7048dcb034d94ce6bfd750c3f1672096',[zz])>=0"); cboExpression.Items.Add("SUM(A1:B3)"); cboExpression.Items.Add("[A1]-[B2]"); cboExpression.Items.Add("A+B+C+在三+是+213"); cboExpression.Items.Add("A+B*(C+D*(E-F))-999"); cboExpression.Items.Add("-A+B+C+D"); cboExpression.Items.Add("A+B*C-D"); cboExpression.Items.Add("10+8>-9"); } private void toolStripButton1_Click(object sender, EventArgs e) { DCExpression exp = new DCExpression(cboExpression.Text); MyContext c = new MyContext(); object vresult = exp.Eval(c); txtResult.Text = "運算結果:" + vresult; }
如此這樣咱們用2500行代碼實現了一個功能強大、性能卓越的數值表達式引擎。可普遍用於各種軟件開發。
在筆者主導開發的電子病歷編輯器控件中就使用了這個數值表達式引擎來實現了相似EXCEL的公式運算功能,其界面以下:
在這個文檔中,咱們設置右下角的單元格的數值表達式爲"SUM([D3:D13])",則用戶如下拉列表方式設置D3:D13區域的單元格內容時,軟件會調用DCSoft.Expression的功能,自動執行數值運算,並將運算結果顯示在右下角單元格中。
◆◆小結
在本文中使用了2500行C#代碼實現了一個結構清晰、易於理解和掌握、性能卓越的數值表達式運算引擎。支持四則數學運算、布爾邏輯運算、能調用外部函數和外部參數值。很容易實現一個相似EXCEL的公式運算引擎。
使用該引擎,能顯著提升軟件的自由度,讓軟件在運行階段便可自由改變運行模式,實現了軟件的高度的運行時可配置化。
中秋之夜編寫此文,詩性大發:玉宇無塵,朗月虛空三千里;架構有道,代碼奔騰百萬行。獻給紅塵中這百萬代碼人共勉。
◆◆關於做者
袁永福,80後,南京東南大學畢業,中國知名醫療信息技術專家,南京都昌信息科技有限公司(www.dcwriter.cn)的聯合創始人,微軟MVP,從事軟件研發近20年。長期從事電子病歷軟件底層技術的研發和推廣,對醫學病歷文檔技術有着深厚的積累。