表驅動法

表驅動法

表驅動法,又稱之為表驅動、表驅動方法。 “表”是幾乎所有數據結構課本都要討論的非常有用的數據結構。表驅動方法出於特定的目的來使用表,程式設計師們經常談到“表驅動”方法,但是課本中卻從未提到過什麼是"表驅動"方法。表驅動方法是一種使你可以在表中查找信息,而不必用很多的邏輯語句(if或Case)來把它們找出來的方法。事實上,任何信息都可以通過表來挑選。在簡單的情況下,邏輯語句往往更簡單而且更直接。但隨著邏輯鏈的複雜,表就變得越來越富有吸引力了。

基本內容

表驅動法,又稱之為表驅動、表驅動方法。

“表”是幾乎所有數據結構課本都要討論的非常有用的數據結構。表驅動方法出於特定的目的來使用表,程式設計師們經常談到“表驅動”方法,但是課本中卻從未提到過什麼是"表驅動"方法。表驅動方法是一種使你可以在表中查找信息,而不必用很多的邏輯語句(if或Case)來把它們找出來的方法。事實上,任何信息都可以通過表來挑選。在簡單的情況下,邏輯語句往往更簡單而且更直接。但隨著邏輯鏈的複雜,表就變得越來越富有吸引力了,通過下面的這個例子大家就能知道什麼是所謂的表驅動方法了。

數據用法

假設你需要一個可以返回每個月中天數的函式(為簡單起見不考慮閏年),

一個比較笨的方法是一個大的if語句:

int iGetMonthDays(int iMonth)

{

int iDays;

if(1 == iMonth) {iDays = 31;}

else if(2 == iMonth) {iDays = 28;}

else if(3 == iMonth) {iDays = 31;}

else if(4 == iMonth) {iDays = 30;}

else if(5 == iMonth) {iDays = 31;}

else if(6 == iMonth) {iDays = 30;}

else if(7 == iMonth) {iDays = 31;}

else if(8 == iMonth) {iDays = 31;}

else if(9 == iMonth) {iDays = 30;}

else if(10 == iMonth) {iDays = 31;}

else if(11 == iMonth) {iDays = 30;}

else if(12 == iMonth) {iDays = 31;}

return iDays;

}

可以看出本來應該很簡單的一件事情,代碼卻是這么冗餘,解決這個的辦法就可以用表驅動方法。

static int aiMonthDays[] = {31,28,31,30,31,30,31,31,30,31,30,31};

// 我們可以先定義一個靜態數組,這個數組用來保存一年十二個月的天數

int iGetMonthDays(int iMonth)

{

return aiMonthDays[(iMonth - 1)];

}

接下來不用多說了,大家都能看出用這種表驅動的方法代替這種情邏輯性不強,但分支很多的代碼是多么令人"賞心悅目"的了。

函式用法

函式指針在表驅動方法中的套用

在使用表驅動方法時需要說明的一個問題是,你將在表中存儲些什麼。

在某些情況下,表查尋的結果是數據。如果是這種情況,你可以把數據存儲在表中。

在其它情況下,表查尋的結果是動作。在這種情況下,你可以把描述這一動作的代碼存儲在表中。

在某些語言中,也可以把實現這一動作的子程式的調用存儲在表中,也就是將函式的指針保存在表中,當查找到這項時,讓程式用這個函式指針來調用相應的程式代碼,這個就是函式指針在表驅動方法中的套用。

其實說到這已經說了很多表驅動方法的相關問題了,現在要把函式指針也套用進去,很多人應該已經想到會是個什麼樣子了,其實也很簡單,通過下面這兩段偽代碼的例子就可以充分體現函式指針在表驅動方法中套用會使代碼更加精緻。

我們在寫一段程式的過程中會經常遇到這樣的問題,我們在寫一個Task的主函式中有時會要等待不同的Event通知,並且處理不同的分支,首先有如下的Event Bit的宏定義和相應的處理函式的聲明。

#define TASK_EVENT_BIT00 (1 << 0)

#define TASK_EVENT_BIT01 (1 << 1)

#define TASK_EVENT_BIT02 (1 << 2)

#define TASK_EVENT_BIT03 (1 << 3)

#define TASK_EVENT_BIT04 (1 << 4)

#define TASK_EVENT_BIT05 (1 << 5)

#define TASK_EVENT_BIT06 (1 << 6)

#define TASK_EVENT_BIT07 (1 << 7)

#define TASK_EVENT_BIT08 (1 << 8)

#define TASK_EVENT_BIT09 (1 << 9)

void vDoWithEvent00();

void vDoWithEvent01();

void vDoWithEvent02();

void vDoWithEvent03();

void vDoWithEvent04();

void vDoWithEvent05();

void vDoWithEvent06();

void vDoWithEvent07();

void vDoWithEvent08();

void vDoWithEvent09();

我們一般首先想到的寫法是

unsigned long ulEventBit;

for(;;)

{

xos_waitFlag(&ulEventBit);

if(ulEventBit & TASK_EVENT_BIT00)

{

vDoWithEvent00();

}

if(ulEventBit & TASK_EVENT_BIT01)

{

vDoWithEvent01();

}

if(ulEventBit & TASK_EVENT_BIT02)

{

vDoWithEvent02();

}

if(ulEventBit & TASK_EVENT_BIT03)

{

vDoWithEvent03();

}

if(ulEventBit & TASK_EVENT_BIT04)

{

vDoWithEvent04();

}

if(ulEventBit & TASK_EVENT_BIT05)

{

vDoWithEvent05();

}

if(ulEventBit & TASK_EVENT_BIT06)

{

vDoWithEvent06();

}

if(ulEventBit & TASK_EVENT_BIT07)

{

vDoWithEvent07();

}

if(ulEventBit & TASK_EVENT_BIT08)

{

vDoWithEvent08();

}

if(ulEventBit & TASK_EVENT_BIT09)

{

vDoWithEvent09();

}

}

可以看出這樣寫是不是顯得程式太長了呢。

下面我們再看看同樣的一段代碼用函式指針和表驅動方法結合的方法寫出會是什麼樣子。

typedef struct {

unsigned long ulEventBit;

void (*Func)(void);

} EventDoWithTable_t;

/* 定義EventBit 與相應處理函式關係的結構體 */

static const EventDoWithTable_t astDoWithTable[] = {

{ TASK_EVENT_BIT00 , vDoWithEvent00},

{ TASK_EVENT_BIT01 , vDoWithEvent01},

{ TASK_EVENT_BIT02 , vDoWithEvent02},

{ TASK_EVENT_BIT03 , vDoWithEvent03},

{ TASK_EVENT_BIT04 , vDoWithEvent04},

{ TASK_EVENT_BIT05 , vDoWithEvent05},

{ TASK_EVENT_BIT06 , vDoWithEvent06},

{ TASK_EVENT_BIT07 , vDoWithEvent07},

{ TASK_EVENT_BIT08 , vDoWithEvent08},

{ TASK_EVENT_BIT09 , vDoWithEvent09}

};

/* 建立EventBit與相應處理函式的關係表 */

ulong ulEventBit;

int i;

for(;;)

{

xos_waitFlag(&ulEventBit);

for(i = 0 ; i < sizeof(astDoWithTable)/sizeof(astDoWithTable); i ++)

{

if ( ( ulEventBit & astDoWithTable[i].ulEventBit ) &&

( astDoWithTable[i].Func != NULL ) )

{

(*astDoWithTable[i].Func)();

/* 通過函式指針來調用相應的處理函式 */

}

}

}

可以看出這種代碼的風格使代碼變得精緻得多了,並且使程式的靈活性大大加強了,如果我們還要再加入EventBit,只修改表中的內容就可以了。

總結

通過上面介紹的,相信大家已經對函式指針的使用方法有所了解了,但是需要提醒大家,凡事都要具體情況具體分析,使用函式指針的時候一定要多加小心,因為函式指針有它的一個致命的缺點。

函式指針的致命缺點是:無法對參數 (parameter) 和返回值 (return value) 的類型進行檢查,因為函式已經退化成指針,指針是不帶有這些類型信息的。少了類型檢查,當參數或者反回值不一致時,會造成嚴重的錯誤。有些編譯器並不會幫我們找出函式指針這樣的致命錯誤。所以,許多新的程式語言都不支持函式指針了,而改用其他方式。

從上面的例3中我們可以看到

int max(int x,int y){ return x>y?x:y; }

int min(int x,int y){ return x<y?x:y; }

int add(int x,int y){ return x+y; }

這三個函式都有兩個參數,而在後面卻把處理函式定義成

int process(int x,int y, int (*f)())

{

return (*f)(x,y);

}

其中第三個參數是一個函式的指針,從表面上看它是個沒有參數,並且返回int型的函式的指針,但是在後面卻用process(a,b,max)的方式進行調用,max帶有兩個參數,這段程式在C語言中就可以順利的編譯通過(但是在C++中卻編譯不通過),可以看出如果編譯器沒有檢查出錯誤,而我們又不小心寫錯的話,後果是很嚴重的,比如return (*f)(x,y);不小心寫成return (*f)(x);在C語言中可以正常的被編譯通過,但是運行結果一定不是我們想要的。

因此在C語言中使用函式指針的時候,一定要小心“類型陷阱”,小心地使用函式指針,只有這樣我們才可以從函式指針中獲益。

其它參考

什麼是表驅動法

所謂表驅動法(Table-Driven Approach),簡單講是指用查表的方法獲取值。 我們平時查字典以及念國中時查《數學用表》找立方根就是典型的表驅動法。在數值不多的時候我們可以用邏輯語句(if 或case)的方法來獲取值,但隨著數值的增多邏輯語句就會越來越長,此時表驅動法的優勢就顯現出來了。

簡單示例

在我幾天前的一篇條碼序列的文章中提到用36進制(A表示10,B表示11,...)來表示更大的數字,如果用邏輯來表示的話可能會寫成:

if(i<10) { numChar=Convert.ToChar(i); } else if (i==10) { numChar="A" } else if (i==11) { numChar="B" } else if (i==11) { numChar="C" } . else if (i==36) { numChar="Z" }

代碼實在是太長了,按時髦的說法“代碼臭味”太濃了。 但要是存在一個表的話,代碼就非常簡單了 C# code 使用表驅動法 numChar=numChars[i]; 這行代碼假設已經建好了一個numChars的表,此時我們將數據存在一個表中而不是if判斷中. 三:查表的方式 在使用表驅動法的時候必須要解決的一個問題就是如何查表. 我們可以用非常直接的方式查一個表,就如前面示例講的我就用數組下標就好了,但你發現有誰查字典甚至《數學用表》是直接依靠頁碼來查的嗎?這是肯定行不通的。 常用的查表方式有

直接查詢

直接查詢,是指無需繞圈子,用下標的方式就能順利的獲取到數據;

if(day==1) { dayName="星期一"; } else if(day==2) { dayName="星期二"; } else if(day==0) { dayName="星期日"; }

實現同樣的功能,我們可以將這些數據存儲到一個表里 C#代碼 表驅動法獲取星期名稱 string[] dayNames=new string[]{"星期日","星期一","星期二","星期三","星期四","星期五","星期六"}; dayName=dayNames[day]; 只要一條語句就可以代替長長的if-else語句 如果某一天我們的網站要根據訪客選擇的語言來顯示星期幾的話 表驅動法仍然很簡單 C#代碼 表驅動法獲取星期名稱 dayName=dayNames[day,(int)GetUserLanguage()]; 但如果是用if-else的話,那長度可就的翻番啊。

索引查找

在用一個簡單方法無法將“英文單詞”這樣數據轉換成表下標時,可以考慮使用索引來查找.在.net中的Dictionary 就是一個典型的例子 C#代碼 獲取一個用戶對象 Dictionary users=GetAllUsers(); User tom=users["Tom"]; 其實我們常用的DataTable就可以用索引查找的方式來獲取數據 假如有個人員信息的table C#代碼 用索引查找方式從DataTable中獲取第一個用戶的姓名 DataTable userInfo=dal.GetAllUsersInfo(); name=userInfo.Rows.Columns["UserName"]; 使用索引查詢的主要優點就是代碼的可讀性大為增強,可維護性也更好 C#代碼 用直接查找方式從DataTable中獲取第一個用戶的姓名 DataTable userInfo=dal.GetAllUsersInfo(); name=userInfo.Rows[i].Columns; 對比上一段代碼,columns很讓人不知所謂;此外如果返回的Datable返回的列順序改變的話就必須更改魔術數字3,否則代碼就會出錯;

分段查找

分段查找通過確定數據所處的範圍確定分類(下標) 使用分段查找,需要先把每一個區間的上限寫在一個表中,然後通過循環確定所處的區段,最後獲得相應的等級 C#示例 根據分數查績效等級

private static double[] rangeLimit = { 60.0, 75.0, 85.0, 95.0,100.0 };

private static string[] grade = {"不合格", "合乎要求", "良好", "優秀" ,"卓越"};

private static readonly int maxLevel = grade.Length - 1;

public static string CalculateGrade(double score) {

int level = 0;

while (level <= maxLevel) {

if (score < rangeLimit[level]) {

return grade[level];

} else {

level++;

return grade[maxLevel];

}

相關詞條

熱門詞條

聯絡我們