概述
在應用軟件開發領域,對表達式計算的應用有非常廣泛的應用。例如,在報表開發中,經常為用戶提供公式輸入功能,從而實現更靈活的報表匯總;工作流應用軟件中,經常利用邏輯條件進行動態配置,從而提供更加靈活的流程配置;另外,在某些 UI 開發中,需要通過某個屬性的表達式計算結果來動態控制 UI 組件的顯示。所有這些應用都可以歸結為一個通用模型,即表達式的解析以及計算。本文旨在提供一種可擴展的表達式解析及其計算方法。
表達式解析的一般條件及因素
本文所講的表達式是一種以一定的運算規則組合所表達的字符串;另外,通過解析表達式字符串並以其代表的運算規則可以得到一個結果。表達式解析一般需要滿足下列條件:
- 支持的操作符集合
- 操作符的優先級
- 操作符所代表的操作規則集合
- 支持的分隔符集合以及分隔符所代表的意義
- 支持的數據類型集合
- 語法約束,如命名規則、分割符所代表的語法規則等
表達式解析除了以上必須滿足的條件之外,在有些表達式環境中,可能還支持函數、變量。結合本文所要解決的問題,如下列出可選的條件:
- 支持的內部函數集合
- 支持的內部全局變量
- 支持函數定制
- 支持自定義變量
- 支持函數以及操作符重載
以上最后三個可選條件,是一個表達式解析引擎的可擴展支持需要滿足的條件。
再談編譯原理
一般的編譯過程主要包括詞法分析、語法分析、語義分析、中間代碼生成、代碼優化、目標代碼生成,如下圖所示。其中,詞法分析主要任務是輸入源程序,對構成源程序的程序進行掃描和分解,識別出一個一個的單詞。單詞是語言中具有獨立意義的最基本結構。一般這些單詞包括程序語言所支持的保留字、數據類型、操作符、分隔符以及程序定義的標識符、常量等。
語法分析的主要任務是在詞法分析的基礎上,根據語言的語法規則把單詞序列組合成各類語法單位(一般表示成語法樹)。
語義分析的主要任務是進一步分析語法結構正確的程序是否符合源程序的上下文約束、運算相容性等語義約束規則。
圖 1. 編譯一般過程

上述只是非常簡要的介紹了一般的編譯原理知識,詳細知識需要參考編譯原理相關的書籍和文檔。對於本文所講的表達式解析,筆者認為其與程序編譯具有本質上的相似,只是在問題的復雜性上會簡單的很多。表達式解析同樣需要首先把輸入的表達式字符串分解成一個一個的單詞,然后把單詞序列組合成語法單位,最后依據表達式的語言環境所定義的語義約束對這些語法單位進行分析計算。因此,我們可以程序編譯的基本方法有選擇的運用到表達式解析上。
中綴表達式、前綴表達式、后綴表達式
前綴表達式(又叫波蘭式)是一種不含括號的 表達式,並且將 運算符放在 操作數前面的表達式。對於一個前綴表達式的求值而言,所有的計算按運算符出現的順序,嚴格從左向右進行。
后綴表達式(又叫逆波蘭式)也是一種不含括號的 表達式,並且 運算符放在操作數的后。對於一個后綴表達式的求值而言,所有的計算按運算符出現的順序,嚴格從左向右進行。
中綴表達式是一個通用的算術或邏輯公式表示方法, 操作符是以中綴形式處於操作數的中間。中綴表達式不容易被計算機解析,但它符合人們的普遍用法。與前綴或后綴記法不同的是,中綴記法中括號是必需的。計算過程中必須用括號將操作符和對應的操作數括起來,用於指示運算的次序。
前綴表達式和后綴表達式是一種十分有用的表達式,它可以依靠簡單的操作就能得到運算結果的表達式。通常解析程序一般都會將中綴表達式轉換為前綴表達式或后綴表達式,方便表達式計算。
表達式樹
前面講過,表達式是一種以一定的運算規則組合所表達的字符串。每一個表達式的基本組成單位都是由一個操作符和操作數組成的操作單位,操作數可以是一個或者多個。所以一個表達式基本操作單位可以表示為一棵樹,下圖所示為一個N元操作符的樹表示,該操作符代表的運算需要N個操作數:
圖 2. N元操作符的樹表示

每個基本操作單位可以表示為一棵樹,每個操作單位的運算結果為其它操作符的一個操作數,從而一個含有多個操作符的表達式可以表示成一棵更大的樹,其每一棵子樹皆為一個子表達式。如表達式 a*b+c/d 可以表示為:
圖 3. 表達式 a*b+c/d 的樹表示

從編譯原理的知識可以得出這樣一個結論,一個表達式可能推導出多棵樹,呈現出二義性。因此,需要通過對表達式的語言環境增加語義約束規則,消除二義性,保證從表達式只能推導出唯一的表達式樹。通常的語義約束規則包括設定操作符的優先級、括號的使用等。
表達式解析引擎的基本結構設計
圖 4. 解析引擎的基本結構設計(查看大圖)

如 圖 4 所示,展示了一個表達式解析引擎的基本結構設計。整個表達式解析引擎內部結構主要包括:表達式解析引擎的語言環境定義、操作符處理器管理、表管理器、表達式解析器、表達式校驗器、表達式解析異常定義等。本文后續部分將詳細解釋。
表達式解析引擎實現說明
定義表達式解析引擎的語言環境
對於表達式解析,首先必須定義表達式所存在的語言環境。主要包括表達式所支持的數據操作、數據類型、合法的操作符集合、保留字、分隔符等。另外還包括內部自定義並可供表達式引用的全局變量、函數等。所有這些語言環境因素都通過表格管理器進行統一管理,如 圖 4 所示 TableManger 類。
表格管理器主要用於符號管理,主要管理如下四類符號數據:
操作符管理
操作符管理主要管理表達式所支持的所有操作符,包括內置操作符以及用戶自定義的操作符。其中,用戶自定義的操作符屬於表達式解析引擎中可擴展特性,需要注冊到表格管理器,以便表達式引用。
操作符具備一般的符號特性,包括操作符標示字符串,操作數以及操作數個數。另外,不同的操作符具有不同的操作優先級。
語法保留字管理
語法保留字管理主要存儲表達式所支持的各種關鍵字、分隔符等,屬於表達式構成的一部分。如"("、")"、","、"const"、"var"等。
函數管理
函數管理主要管理表達式所支持的所有函數,包括內置函數以及用戶自定義的函數。其中,用戶自定義的函數屬於表達式解析引擎中可擴展特性,需要注冊到表格管理器,以便表達式引用。
變量管理
變量管理主要管理表達式解析引擎內置的全局變量以及用戶自定義的變量。其中,用戶自定義的變量屬於表達式解析引擎中可擴展特性,需要注冊到表格管理器,以便表達式引用。
對於表格管理中的可擴展特性將在本文稍后部分講解。下面給出符號定義結構示例:
圖 5. 符號定義結構(查看大圖)

Symbol 類定義了表格管理中一般性符號,包括標識字符串 identifier 和轉義字符串 escape 兩個屬性。
OperationSymbol 類定義了一般性的操作符號,包括操作數個數 dimension 以及操作數集合 operands。前面講過,表達式中任何操作都可以抽象歸納為操作符以及零或多個操作數兩部分。本文涉及的操作主要包括基本操作符,函數以及取值符所代表的操作。當然,操作數包括數據類型、數據值等屬性,用 Variable 類型的對象表示。
需要注意的是,任何一個表達式一般都是一個字符串。所以,表達式解析過程需要正確解析字符串表達式中各個操作數的數據類型。
Operator 類定義了表達式解析引擎中基本的操作符號,如 +、—、*、/、&&、|| 等。Operator 類包括操作符優先級 priority 以及操作處理器 handler。
Function 類代表函數,包括函數實現類 clazz 屬性以及用於判斷函數是否靜態的 isStatic 屬性。
ValueOperator 類定義了表達式中的取值操作符。因為一個解析引擎通常僅僅只有一個取值操作符,所以該類屬於一個單例模型,包括一個 ValueOperator 對象實例屬性以及取值操作符處理器 handler。
本節闡述了表格管理器的內容,以下 清單 1 所示給出一個表格管理器示例,簡要列出表格管理器屬性及操作。
清單 1. 表格管理器
public class TableManager {
// 管理 / 存儲表達式語言環境涉及的操作符,包括解析引擎保留操作符以及用戶注冊的自定義操作符
private static Hashtable<String, Operator>
tblOperator = new Hashtable<String, Operator>();
// 管理 / 存儲表達式語言環境涉及的語法關鍵字
private static Hashtable<String, Symbol>
tblSyntaxKey = new Hashtable<String, Symbol>();
// 管理 / 存儲表達式語言環境定義的函數,包括解析引擎內部函數以及用戶注冊的自定義函數
private static Hashtable<String, Function>
tblFunction = new Hashtable<String, Function>();
// 管理 / 存儲表達式語言環境定義的變量,包括解析引擎內部全局變量以及用戶注冊的自定義變量
private static Hashtable<String, Variable>
tblGlobalVar = new Hashtable<String, Variable>();
// 標示符最大長度,方便表達式解析過程中的回朔處理
private static int MAX_IDENTIFIER_LEN = 1;
// 操作符是否可被重寫處理器
private static boolean isOverridable = false;
// 根據標示符號字符串取得操作符
public static Operator getOperator(String identifier) {
// 省略代碼 ....
}
// 根據標示符號字符串取得函數
public static Function getFunction(String identifier) {
// 省略代碼 ....
}
// 根據標示符號字符串取得標示符
public static Symbol getIdentifier(String identifier) {
// 省略代碼 ....
// 注冊操作符
public static boolean regOperator(Operator oper) {
// 省略代碼 ....
}
// 注冊語法關鍵字
public static boolean regSyntaxKeys(Symbol identifier) {
// 省略代碼 ....
}
// 注冊函數
public static boolean regFunction(Function func) {
// 省略代碼 ....
}
// 判斷是否存在某標示符
public static boolean existIdentifier(String identifier) {
// 省略代碼 ....
}
// 判斷是否存在某操作符
public static boolean existOperator(String identifier) {
// 省略代碼 ....
}
// 判斷是否存在某函數
public static boolean existFunc(String identifier) {
// 省略代碼 ....
}
// 取得操作符優先級
public static int getPriority(String identifier) {
// 省略代碼 ....
}
// 取得操作符定義的操作數個數
public static int getDimension(String identifier) {
// 省略代碼 ....
}
// 取得最大標示符長度
public static int getMaxIdentifierLen() {
return MAX_IDENTIFIER_LEN;
}
// 取得單詞類型
public static TokenType getTokenType(String token) {
// 省略代碼 ....
}
// 判斷是否為取值操作符
public static boolean isValueOperator(String token) {
// 省略代碼 ....
}
// 根據變量名取得已注冊的變量
public static Variable getVariable(String varName) {
// 省略代碼 ....
}
// 判斷是否存在某變量
public static boolean existVariable(String varName) {
// 省略代碼 ....
}
// 注冊變量
public static boolean regVariable(Variable var) {
// 省略代碼 ....
}
}
操作符處理器以及可擴展的問題
在表達式解析過程中,除了正確解析出各種操作符以及操作數,還需要能依據這些操作符所代表的操作語義正確地進行數據的操作處理。 在本文的"表達式解析的一般條件及因素"部分,提出了三個可選條件作為解析引擎的擴展支持,從而增強解析引擎的功能。本文所提出的設計方案主要采用 Bridge 設計模式和反射模式實現操作符處理以及擴展支持。
圖 6. 操作符處理器及可擴展處理結構(查看大圖)

如 圖 6 所示,提供了一個操作符處理器及可擴展處理的設計結構。組成表達式的操作單元主要包括基本操作符運算、變量取值、函數調用等。在 圖 7 所示的結構中,表格管理器 TableManager 主要用於管理各種操作符、函數、變量等並以表的形式進行存儲。至於表的實現,具體有很多種方法,如利用哈希表等。在基本操作符 Operator 和取值操作符 ValueOperator 的定義結構中,都定向聚集關聯了一個操作處理器對象 handler。Operator 類的操作處理器類型為 IExpressionHandler 接口類型 , ValueOperator 類的操作處理器類型為 IValueOperatorHandler 接口類型。通過該設計方法,除了表達式解析引擎內部實現的缺省操作處理器,用戶同樣可以通過實現 IExpressionHandler 接口和 IValueOperatorHandler 接口,向表管理器 TableManager 中注冊個性化業務相關的操作符處理以及取值處理。具體如下:
- 操作符處理:用戶可以自定義操作符或者進行操作符重載。首先,用戶通過實現 IExpressionHandler 接口實現用戶第三方操作符處理;然后,定義或重載操作符對象,並且引用指向用戶實現的第三方操作符處理器; 最后,通過 TableManager 提供的注冊方法注冊該操作符。
- 取值符處理:實現解析引擎內部的全局變量取值以及第三方業務相關的取值處理。對於全局變量的取值,首先需要注冊自定義的全局變量,從而保證在表達式的計算過程中能通過查找表管理器 TableManager 中的全局變量表來獲得變量的值。對於第三方業務相關的取值處理,首先需要實現 IValueOperatorHandler 接口來實現第三方取值處理,然后通過表管理器 TableManager 注冊第三方處理器到取值符中(前面講過,解析引擎通常僅僅只有一個取值操作符,所以單獨注冊取值處理器到取值操作符中)。
- 函數處理:實現解析引擎內部函數調用以及第三方自定義函數調用。函數處理主要通過在運行時狀態下利用反射方法動態調用。表達式可利用的每個函數都需要通過表管理器 TableManager 注冊到函數表中。在表達式解析計算過程中,當遇到函數調用時,函數處理器通過查詢函數表得到函數相關信息,然后利用這些函數信息反射調用函數,從而得到函數的執行結果。
如 清單 2 所示,為一個加法處理器代碼示例。該加法處理器能處理整型數字、浮點數以及字符串相加。
清單 2. 加法 + 操作符處理器
/**
* 加法操作符處理器,支持數值相加以及字符串相加
* @author Wang Jianguang
*/
public class PlusOperatorHandler implements IExpressionHandler {
/*
* 加法操作符處理器方法
* @param oper : 操作符號
* @return : Variable 類型的操作符處理結果
*/
public Variable operate(OperationSymbol oper) {
Variable res = null;
if(!validate(oper)) {
return res;
}
String identifier = oper.getIdentifier();
if(!identifier.equals("+")) {
return res;
}
Variable[] operands = oper.getOperands();
if(operands.length != 2) {
return res;
}
boolean isStrOp = containStrOperand(operands);
Object val0 = operands[0].getValue();
Object val1 = operands[1].getValue();
if(isStrOp) {
res = new Variable(DataType.STRING);
res.setValue(val0.toString() + val1.toString());
} else {
boolean isFloatResult = operands[0].getType() == DataType.FLOAT||
operands[1].getType() == DataType.FLOAT;
if(isFloatResult) {
float fvalue = Float.parseFloat(val0.toString())
+ Float.parseFloat(val1.toString());
res = new Variable(DataType.FLOAT);
res.setValue(fvalue);
} else {
int ivalue = Integer.parseInt(val0.toString())+
Integer.parseInt(val1.toString());
res = new Variable(DataType.INT);
res.setValue(ivalue);
}
}
return res;
}
表達式解析及計算方法
要正確解析表達式,首先必須通過詞法分析能識別出表達式中一個一個的單詞,這些單詞包括基本操作符、關鍵字、分隔符和操作數,還可能包括函數、變量等。一般給定一個待計算的表達式,通常是符合某種語言環境所提供的語法結構;往往這種語法並不一定適合計算機內部計算,或者說,對計算機計算而言比較復雜。所有,對於表達式的分析與計算,通常需要把表達式轉換為可以依靠簡單的操作就能得到運算結果的表達式。前面講過,通常解析程序一般都會將中綴表達式轉換為前綴表達式或后綴表達式,方便表達式計算。本文正是將表達式轉換為后綴表達式來進行分析與計算的。
圖 7. 表達式解析與計算過程

如 圖 7 所示,展示了表達式的解析與計算過程。下面就該過程的每部分進行簡要說明。
詞法分析:識別出表達式中合法的單詞,一般利用回朔算法進行分析,並利用表管理器來輔助識別。
生成后綴表達式棧:將表達式(一般為中綴表達式)轉換為后綴表達式,並以棧結構進行存儲,方便后續構造表達式樹。
清單 3. 生成后綴表達式棧
/**
* 構造后綴表達式棧。通過詞法分析,獲得各種類別的單詞,從而依據語法規則構造
* 后綴表達式棧
* @param expr 輸入表達式
* @return 由各個單詞構成的表達式棧
*/
public Stack<String> buildPostExpressionStack(String expr) {
Stack<String> retStack = new Stack<String>();
Stack<String> stack = new Stack<String>();
stack.push("ENDFLAG");
int iPointer = 0;
while(iPointer < expr.length()) {
// 詞法分析,得到單詞
String token = this.getToken(expr, iPointer);
int step = token.length();
if(token.equals("(")) {
stack.push(token);
} else if(TableManager.existOperator(token)) {
// 分析操作符
String lastToken = stack.lastElement();
while(TableManager.existFunc(lastToken)
|| TableManager.isValueOperator(token)
|| TableManager.getPriority(lastToken)
>= TableManager.getPriority(token)) {
retStack.push(stack.pop());
lastToken = stack.lastElement();
}
stack.push(token);
} else if(TableManager.existFunc(token) ||
TableManager.isValueOperator(token)) {
// 分析函數或取值符號
stack.push(token);
} else if(token.equals(",")) {
while(!stack.lastElement().equals("(")){
retStack.push(stack.pop());
}
} else if(token.equals(")")) {
while(!stack.lastElement().equals("(")) {
retStack.push(stack.pop());
}
stack.pop();
String lastToken = stack.lastElement();
if(TableManager.existFunc(lastToken)) {
retStack.push(stack.pop());
}
} else {
retStack.push(token);
}
iPointer += step;
}
while(!stack.lastElement().equals("ENDFLAG")) {
retStack.push(stack.pop());
}
adjust(retStack);
return retStack;
}
生成表達式樹:前面講過,一個含有多個操作符的表達式可以表示成一棵樹。此過程正是利用此結論來構造一棵表達式樹,方便后續的分析與計算結構。樹的每個節點都保存每個操作符或操作數的相關信息。以樹的結構存儲表達式還方便擴展分析處理。如 清單 4 所示為生成表達式樹代碼。
清單 4. 生成表達式樹
/**
* 構造表達式樹。首先通過詞法分析構造表達式棧,然后構造表達式樹。
* @param expr 輸入表達式
* @return 表達式樹
*/
public TreeNode buildTree(String expr) {
Stack<String> postExprStack = buildPostExpressionStack(expr);
Stack<String> tmpStack = reverseStack(postExprStack);
Stack<TreeNode> nodeStack = new Stack<TreeNode>();
while(!tmpStack.isEmpty()) {
String token = tmpStack.pop();
TreeNode node = new TreeNode(token);
TokenType nodeType = TableManager.getTokenType(token);
node.setNodeType(nodeType);
if(nodeType == TokenType.FUNCTION
|| nodeType == TokenType.OPERATOR
|| nodeType == TokenType.VARIABLE) {
int dimension = TableManager.getDimension(token);
TreeNode[] children = new TreeNode[dimension];
for(int j = dimension-1; j >= 0; j--){
children[j] = nodeStack.pop();
}
node.setChildren(children);
nodeStack.push(node);
} else {
nodeStack.push(node);
}
}
TreeNode root = nodeStack.pop();
return root;
}
分析與計算:該過程結合表管理器,通過遞歸分析與計算表達式樹,得到表達式計算結果。
清單 5. 表達式分析與計算
/**
* 計算表達式。 通過遞歸解析計算表達式樹得到計算結果。
* @param expr 輸入表達式
* @return 計算結果
*/
public Variable computeExpression(String expr) {
Variable value = null;
if(expr == null) {
return value;
}
// 構造表達式樹
TreeNode tree = buildTree(expr);
if(tree == null) {
return value;
}
// 解析計算表達式樹
value = computeSubTree(tree);
return value;
}
/**
* 解析計算表達式子樹
* @param node 子樹根節點
* @return 子樹計算結果
*/
public Variable computeSubTree(TreeNode node) {
Variable varRet = null;
if(node == null){
return null;
}
String value = node.getValue();
if(!TableManager.existOperator(value)
&& !TableManager.existFunc(value)
&& !TableManager.isValueOperator(value)){
return ParserUtil.getValue(value);
}
TreeNode[] children = node.getChildren();
if(children == null || children.length == 0) {
return ParserUtil.getValue(value);
}
Variable[] operands = new Variable[children.length];
for(int i = 0; i < children.length; i++) {
operands[i] = computeSubTree(children[i]);
}
OperationSymbol op = getOperationSymbol(node, value);
if(op != null) {
op.setOperands(operands);
varRet = op.operate();
}
return varRet;
}
表達式校驗器:在表達式分析與計算的每一個過程中,都需要利用表達式校驗器來保證其數據的合法性,如操作符是否合法、語法是否正確、分析得到的操作數數據類型是否正確、函數的參數類型是否正確等等。每當遇到校驗失敗的情況,都會拋出各種類型的異常,並中止解析過程,表示解析失敗。
下面給出一個表達式解析引擎應用示例,如 清單 6 所示。
清單 6. 表達式解析引擎應用
public class ExpressionParserTester {
/*
* 初始化:注冊用戶自定義的函數以及變量到表達式解析引擎
*/
public static void init() {
TableManager.regFunction(
new Function("test",
"com.gavin.parser.expression.test.Test",
new DataType[]{DataType.INT,DataType.INT,DataType.INT},
DataType.INT,
true));
TableManager.regFunction(
new Function("test2",
"com.gavin.parser.expression.test.Test",
new DataType[]{DataType.STRING},
DataType.INT,
true));
TableManager.regVariable(new Variable(DataType.INT, "GB_VAR", 31));
}
public static void main(String[] args) {
init();
String expr =
"1+2*test($GB_VAR,test((1+3)+4*(5+6 ),2,(2+3)%2),
test(-10,10,11*(1+test(3,4,5))))+test2('300')*3";
TExprParser parser = TExprParser.getInstance();
// 測試 1: 建立后綴表達並打印
String postExp = parser.buildPostExpression(expr);
System.out.println(postExp);
// 測試 2: 解析以及計算表達式並打印結果
String value = parser.parse(expr);
System.out.println(value);
}
public static int test(int a, int b, int c) {
return a+b+c;
}
public static int test2(String param) {
return Integer.valueOf(param).intValue();
}
}
總結
本文以一種循序漸進的方式闡述了一種表達式解析與計算方法,並提供了一個支持可擴展的表達式解析與計算的設計實例。首先本文提出定義了表達式的一般模型及相關概念;然后從引入編譯原理一般過程開始,逐步介紹了表達式的三種類型及表達式樹的概念。最后提供一個設計實例來介紹表達式解析與計算的方法與步驟。筆者希望籍以本文為讀者拋磚引玉,在表達式解析方面能提供些許指引。
