Như đã trù hoạch, phiên bản Lan Hoa sẽ hỗ trợ một số hàm toán học. Nhưng làm cách nào gọi các hàm này khi tính toán biểu thức? Câu trả lời sẽ rất thú vị.
Thứ nhất, hàm toán học sẽ được xem như toán tử đơn phân. Nó giống với dấu âm ở chỗ chỉ tác động lên toán hạng đứng bên phải, và khác ở chỗ toán hạng này phải được bao trong cặp dấu ngoặc.
Thứ nhì là cách thực hiện các hàm. Theo khuynh hướng thông thường thì ta sẽ dùng lệnh rẽ nhánh switchdựa theo thành phần IntValue của token. Đại loại như:
Syntax: [ Download ] [ Hide ] [ Select ] [ Contract ]
Using csharp Syntax Highlighting
switch (tmp.IntValue) {
res.Push(Math.Sqrt(res.Pop()));
case 1: //abs
res.Push(Math.Abs(res.Pop()));
// ...
}
Parsed in 0.007 seconds, using GeSHi 1.0.8.4
Nhưng không. Cách này tuy xài được, nhưng kém linh hoạt. Sau này nếu cần thêm các hàm mới, chẳng lẽ lại rẽ nhánh tiếp? Đó là chưa kể việc này làm giảm hiệu suất chương trình và tăng sự rối rắm của mã lệnh.
Chúng ta sẽ tận dụng sự hỗ trợ quý giá của môi trường .NET để áp dụng kỹ thuật "bảng rẽ nhánh" (branch table). Kỹ thuật này, nói một cách đơn giản và trong trường hợp hiện tại, là tạo một mảng các hàm và gọi các hàm này qua số chỉ mục tương ứng trong IntValue.
Nếu C/C++ có con trỏ hàm thì .NET có ủy thác hàm (delegate). Hy vọng bạn không xa lạ lắm đối với khái niệm này, vì sẽ cần nhiều thời gian để giải thích tường tận.
Ta cần định nghĩa một khuôn mẫu hàm như sau:
Syntax: [ Download ] [ Hide ]
Using csharp Syntax Highlighting
private delegate double FuncDelegate(double d);
Parsed in 0.006 seconds, using GeSHi 1.0.8.4
Và một mảng các hàm (bảng rẽ nhánh):
Syntax: [ Download ] [ Hide ]
Using csharp Syntax Highlighting
private FuncDelegate[] FuncTable;
Parsed in 0.006 seconds, using GeSHi 1.0.8.4
Ta sẽ lần lượt viết từng hàm với khuôn mẫu đã định nghĩa ở trên và đưa chúng vào mảng FuncTable trong hàm tạo lập của Expression.
Thí dụ:
Syntax: [ Download ] [ Hide ]
Using csharp Syntax Highlighting
private double f_sqrt(double d) {
return Math.Sqrt(d);
}
Parsed in 0.006 seconds, using GeSHi 1.0.8.4
Và khi gọi hàm thì ta có:
Syntax: [ Download ] [ Hide ]
Using csharp Syntax Highlighting
res.Push(FuncTable[tmp.IntValue](res.Pop()));
c. Viết mã lệnh
Người xưa nói: văn kỳ thinh bất kiến kỳ hình. Ở đây là chỉ nghe diễn tả mà chưa ngó mã lệnh thì cũng tù mù khó hình dung. Do đó, mời bạn xem mã lệnh của lớp Expression:
Syntax: [ Download ] [ Hide ] [ Select ] [ Contract ]
Using csharp Syntax Highlighting
using System;
using System.Collections.Generic;
namespace BieuThuc.LanHoa {
class Expression {
#region Các lớp hỗ trợ
private Parser mParser;
private Lexer mLexer;
private Error mError;
#endregion
#region Các hàm toán học
private delegate double FuncDelegate(double d);
private FuncDelegate[] FuncTable;
private double f_sqrt(double d) {
return Math.Sqrt(d);
}
private double f_abs(double d) {
return Math.Abs(d);
}
private double f_sin(double d) {
return Math.Sin(d);
}
private double f_cos(double d) {
return Math.Cos(d);
}
private double f_tan(double d) {
return Math.Tan(d);
}
private double f_asin(double d) {
return Math.Asin(d);
}
private double f_acos(double d) {
return Math.Acos(d);
}
private double f_atan(double d) {
return Math.Atan(d);
}
#endregion
public Expression() {
// Khởi lập các lớp
this.mError = new Error();
this.mLexer = new Lexer(this.mError);
this.mParser = new Parser(this.mLexer, this.mError);
// Tạo bảng ủy thác các hàm
// Lưu ý: thứ tự các hàm trong bảng này
// phải đúng y thứ tự trong mảng FuncIdent
// thuộc lớp Lexer.
this.FuncTable = new FuncDelegate[] {
f_sqrt, f_abs, f_sin, f_cos, f_tan, f_asin, f_acos, f_atan };
}
public bool Evaluate(string expr, ref double result) {
// Danh sách token
List<Lexer.Token> list = new List<Lexer.Token>();
// Danh sách token sau khi chuyển sang RPN
List<Lexer.Token> rpn = new List<Lexer.Token>();
Stack<double> res = new Stack<double>();
Lexer.Token tmp = new Lexer.Token();
double a = 0, b = 0;
// Thanh lọc biểu thức
if (mParser.Parse(expr, ref list) == false) return false;
// Chuyển sang RPN
ConvertToRPN(list, ref rpn);
for (int i = 0; i < rpn.Count; ++i) {
tmp = rpn[i ];
switch (tmp.Class) {
case Lexer.TokenClass.NumInt:
res.Push(tmp.IntValue);
break;
case Lexer.TokenClass.NumFloat:
res.Push(tmp.FloatValue);
break;
case Lexer.TokenClass.Plus:
b = res.Pop();
a = res.Pop();
res.Push(a + b);
break;
case Lexer.TokenClass.Minus:
b = res.Pop();
a = res.Pop();
res.Push(a - b);
break;
case Lexer.TokenClass.Mul:
b = res.Pop();
a = res.Pop();
res.Push(a * b);
break;
case Lexer.TokenClass.Div:
b = res.Pop();
a = res.Pop();
if (b == 0) {
mError.RaiseError(Error.ErrorConstant.DivisionByZero);
return false;
} else
res.Push(a / b);
} break;
case Lexer.TokenClass.Mod:
b = res.Pop();
a = res.Pop();
if (b == 0) {
mError.RaiseError(Error.ErrorConstant.DivisionByZero);
return false; } else { res.Push(a % b); } break;
case Lexer.TokenClass.Pow:
b = res.Pop();
a = res.Pop();
res.Push(Math.Pow(a, b));
break;
case Lexer.TokenClass.UMinus:
res.Push(-res.Pop());
break;
case Lexer.TokenClass.Func:
res.Push(FuncTable[tmp.IntValue](res.Pop()));
break;
} //switch
} //while
result = res.Pop();
return true;
}
private void ConvertToRPN(List<Lexer.Token> list, ref List<Lexer.Token> rpn) {
// Ngăn xếp tạm chứa toán tử
Stack<Lexer.Token> opr = new Stack<Lexer.Token>();
Lexer.Token tok = new Lexer.Token();
// Vòng lặp chuyển đổi
for (int i = 0; i < list.Count; ++i) {
tok = list<span style="font-style: italic">;
switch (tok.Class) {
case Lexer.TokenClass.NumInt:
case Lexer.TokenClass.NumFloat:
rpn.Add(tok);
break;
case Lexer.TokenClass.LPar:
opr.Push(tok);
break;
case Lexer.TokenClass.RPar:
while (opr.Count != 0 && opr.Peek().Class ! =Lexer.TokenClass.LPar)
rpn.Add(opr.Pop());
opr.Pop(); //LPar
break;
case Lexer.TokenClass.Plus:
case Lexer.TokenClass.Minus:
case Lexer.TokenClass.UMinus:
case Lexer.TokenClass.Mul:
case Lexer.TokenClass.Div:
case Lexer.TokenClass.Mod:
case Lexer.TokenClass.Pow:
case Lexer.TokenClass.Func:
while (opr.Count ! = 0 && opr.Peek().Precedence()>= tok.Precedence())
rpn.Add(opr.Pop());
opr.Push(tok);
break;
} }
// Xổ các toán tử còn lại vào danh sách RPN
while (opr.Count != 0) rpn.Add(opr.Pop());
} }
}
Parsed in 0.039 seconds, using GeSHi 1.0.8.4
6. Báo lỗi
Ta không chỉ bắt lỗi. Ta còn cần phải báo các lỗi đó cho người sử dụng biết. Chớ mà cứ bắt lỗi rồi im ru thì không giống con giáp nào hết.
Vậy nên mới có lớp [i]Error.
Nếu tinh mắt, bạn sẽ thấy trong các lớp ở trên, có một số lời gọi hàm RaiseError với đối số là một hằng thuộc kiểu ErrorConstant.
Trong hàm RaiseError có gì? Rất dễ nhận ra rằng trong đó có các lệnh tạo chuỗi báo lỗi và gọi hàmMessageBox.Show. Vậy, một hằng ErrorConstant chuyển thành một chuỗi thông báo lỗi bằng
cách nào?
Cách dễ là switch. Còn cách "đẹp" thì giống như trường hợp của các hàm toán học. Tức là sẽ có một mảng các thông báo mà chỉ mục của chúng tương ứng chính xác với giá trị của các hằng lỗi. Đây là mã lệnh của lớp Error:
Syntax: [ Download ] [ Hide ] [ Select ] [ Contract ]
Using csharp Syntax Highlighting
using System.Windows.Forms;
namespace BieuThuc.LanHoa {
class Error {
// Liệt kê các loại lỗi
public enum ErrorConstant { InvalidCharacter, UndefinedFunction, ExpectLPar, ExpectRPar, ExpectExpression, ExpectExprRest, DivisionByZero }
// Nội dung các thông báo lỗi
private string[] ErrorConstantStr;
public Error() {
this.ErrorConstantStr = new string[] {
"Ký tự lạ.",
"Hàm chưa định nghĩa.", "Thiếu dấu mở ngoặc.", "Thiếu dấu đóng ngoặc.", "Không nhận được biểu thức.",
"Không nhận được phần sau của biểu thức.", "Phép chia cho số 0."
};
}
// Hàm báo lỗi
public void RaiseError(ErrorConstant e) {
string err = "Lỗi " + ((int)e).ToString("D2") + ":\n";
MessageBox.Show(err, "Loi", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
}
Parsed in 0.013 seconds, using GeSHi 1.0.8.4
7. Kết luận
Mã lệnh cho phần giao diện không khác phiên bản trước. Chúng ta cứ chép lại và chạy thử. Lần này, thử nhập một biểu thức sai cú pháp. Chương trình sẽ nhanh nhảu lên tiếng báo lỗi.
Vậy là xong. Lan Hoa đã hoàn thành và thi hành ngon lành. Kể cũng không uổng mấy tiếng đồng hồ hì hục nghiên cứu!
Nhưng chúng ta sẽ còn gặp nhau. Bởi phiên bản 2.0 chưa thấm vào đâu so với phiên bản 3.0 sắp tới. Liên Hoa sẽ mang đến nhiều bất ngờ lý thú. Và, xin tiết lộ một chút, chúng ta sắp sửa viết được một chương trình thông dịch.
Tham khảo http://en.wikipedia.org/wiki/Parsing http://en.wikipedia.org/wiki/Recursive_descent_parser http://en.wikipedia.org/wiki/Top-down_parsing http://en.wikipedia.org/wiki/Branch_table TẬP TIN ĐÍNH KÈM LanHoa.zip
Biểu Thức 2.0 - Lan Hoa
(58.94 KiB) Đã tải về 85 lần
Sửa lần cuối bởi alexanderdna vào ngày Thứ 4 12/01/2011 8:55 am với 1 lần sửa trong tổng số. Bổn quốc tự kim cổ,
Việt tộc dựng cơ đồ.
Bắc phương sao cuồng vọng? Sẽ thấy tự chuốc khổ!
alexanderdna
Guru
Bài viết: 193
Ngày tham gia: Thứ 3 14/07/2009 11:13 am
Đến từ: Sài Gòn
Được cảm ơn:6 lần Đ ầ u t r a n g