1. 程式人生 > >geek青年的狀態機,查表,純C語言實現

geek青年的狀態機,查表,純C語言實現

fill south 總結 target 堅持 str 分享 接收 backward

geek青年的狀態機,查表,純C語言實現

1. 問題的提出。抽象

建一,不止是他,不少人跟我討論過這種問題:怎樣才幹保證在需求變更、擴充的情況下。程序的主體部分不動呢?

這是一個很深刻和艱難的問題。在進入實質討論之前,我們還得先明白什麽是"主體"。就是我們不希望動的那一部分是什麽。其實,沒有什麽"主體"。這是被我們主觀劃分的,代碼中有一部分是不動的,還有一部分是動的。而追求永恒(一勞永逸?) ,是我們的天性吧。

我們希望實現一段程序,換一些東西,遊戲就由 雙截龍 變成了 超級瑪麗,再換一點東西,就變成了 魂鬥羅。僅僅要招些美工,再招些腳本作者,全部的程序猿就能夠--解雇了。

這看起來不太現實,那麽我們來看一段類似的。可是更現實一點的。我們希望實現一段程序。在每輪叠代/循環中,這段代碼都能完畢我們須要做的任務。盡管這些任務可能在每輪叠代中有所不同。在數學歸納法,在 sigma 符號的的周圍,甚至在積分符號的周圍。都在發生這種事情。

這些夢想或者已經實現的技術,都基於"抽象"。

我們試圖找到在不同的情境 (動作、需求) 下那些同樣的部分。我們對詳細事件做抽象。而且期待抽象的結果適用於全部的詳細的事例。

這樣。原來的非常多工作就成為 應用抽象的理論 的過程,不再須要創造力。因此也不再能吸引我們。

那麽,我們再對抽象的結果繼續抽象,直到形而上。

2. 狀態機的引擎

引擎,就是上文中提到的開發出一個遊戲。然後能衍生出非常多遊戲的技術。代碼的核心部分、流程部分不會改變,僅僅有數據 (甚至能夠在外部文件裏) 才隨需求的變化而變化。

狀態機,也能夠用引擎實現。

實現這一目標的技術也存在已久。就是查表。查表的經典案例是 求三角函數 (在一定精度下),常量時間復雜度的解決方式 就是查表。事先把三角函數在不同度數下的值都求出來。放在hash表 (?

) 裏。你要查哪個度數。我就去查哪個度數相應的函數值。

在這個案例裏,查表的那段代碼,不隨三角函數由sin變成cos或tan而發生不論什麽變化。

這就是引擎。被查的表就是數據。

3. 接口

我們期待的接口跟前一篇普通青年中的全然一樣。在主函數中調用 void state_change(enum message m) 向狀態機傳遞消息,用 test.in 作為測試用例。主函數還知道,一共就這樣幾種消息:

enum message { play, stop, forward, backward, record, pause };

4. 狀態遷移表

在講怎樣查表前,我們先設計 表 本身。我們期待表格可以描寫敘述 狀態遷移 中的要素。

記得麽,一共4個。 (1) 當前狀態。 (2)當前消息。 (3)將遷移到的狀態,(4)在狀態遷移中的動作。我們期待能用表格,而不是如普通青年一文中用代碼(switch-case)的方式描寫敘述。由於我們相信,改表格比改代碼easy。

狀態遷移表與狀態遷移圖全然等價。

技術分享


表格看起來像以下這樣,假設想像劃上豎線效果更佳。


1 struct transition fsm[transition_num] = {
2         /* current_state, message/event, next_state*/
3     {s_play,	stop,		s_stop},
4     {s_play,	pause,		s_pause},
5     {s_pause,	pause,		s_play},
6     {s_pause,	stop,		s_stop},
7     {s_stop,	forward,	s_forward},
8     {s_stop,	play,		s_play},
9     {s_stop,	backward,	s_backward},
10     {s_stop,	record,		s_record},
11     {s_forward,	stop,		s_stop},
12     {s_backward,	stop,		s_stop},
13     {s_record,	stop,		s_stop} };


每一行,都是一組狀態遷移的匹配。第一列是當前狀態,第二列是接收到的消息,第三列是在此種情況下將遷移到的狀態。每添加一個遷移的匹配,我們就按這種規則添加一行。這規定了狀態機遷移中4要素裏的3條,剩下的那條是在遷移中的動作。後面再介紹。

當然。為了遵循C語言的語法,我們須要在此前就定義 (1) 狀態枚舉、 (2) 消息枚舉,還有 (3) 遷移的結構體。例如以下。

1 enum state { s_stop=‘s‘, s_play=‘p‘, s_forward=‘f‘, s_backward=‘b‘, s_pause=‘_‘, s_record=‘r‘  };
2 enum message { play, stop, forward, backward, record, pause };
3 
4 struct transition {
5     enum state current;
6     enum message m;
7     enum state next;
8 };

我們還須要定義一共多少條遷移規則,是為了我們還沒有寫出來的代碼準備的。只是此處已經用到,所以定義例如以下。



1 #define transition_num  11

5. 遷移時的動作

我們希望把遷移時的動作放在每一個狀態到達之處。

即,每一個狀態都能夠有一些"副作用"。這與遷移時的動作是等價的,證明略去。假設僅想在遷移時寫代碼,也能夠利用這樣的方法實現。



狀態機的動作 表格例如以下:

1 struct state_action state_action_map[state_num] = {
2     {s_stop, do_stop},
3     {s_play, do_play},
4     {s_forward, do_forward},
5     {s_backward, do_backward},
6     {s_pause, do_pause},
7     {s_record, do_record}};

每一行。是一個狀態相應的動作。

第一列是狀態,第二列是相應的動作。這樣。每添加一個狀態 (假設它有相應動作)。就在這裏添加一行;動作相應的函數須要實現。後面會介紹。

類似於狀態遷移圖,為了遵循C語言語法。我們須要在此前聲明例如以下。

1 #define state_num 6
2 typedef void (*action_foo)() ;
3 
4 enum state { s_stop=‘s‘, s_play=‘p‘, s_forward=‘f‘, s_backward=‘b‘, s_pause=‘_‘, s_record=‘r‘  };
5 
6 /* action starts */
7 void do_stop() {printf ("I am in state stop and should doing something here.\n");}
8 void do_play() {printf ("I am in state play and should doing something here.\n");}
9 void do_forward() {printf ("I am in state forward and should doing something here.\n");}
10 void do_backward() {printf ("I am in state backward and should doing something here.\n");}
11 void do_pause() {printf ("I am in state pause and should doing something here.\n");}
12 void do_record() {printf ("I am in state record and should doing something here.\n");}
13 
14 struct state_action {
15     enum state m_state;
16     action_foo foo;
17 };

第1行,是狀態的數量。

第2行和第7行到第12行,以及第16行,使用了函數指針(指向函數的指針。一個指針,它的基類型是一個函數),用於表示要運行的動作。第4行,是狀態枚舉。

第14行到第17行。是 狀態-動作 相應關系的結構體。

第7行至第12行。是動作的運行部分。當添加的狀態須要動作時,程序猿要在此處添加一個函數,它遵守第2行的簽名約定。



6. 引擎

假設表格的數據結構已定,代碼就好寫了。我們的引擎代碼的核心部分是查表,遍歷表格,找到與當前狀態、當前消息匹配的將遷移到的狀態。

我們還是自頂向下。如果 查表部分已經完畢,為主函數提供與 普通青年一文同樣的接口--而內部實現是不同的。



1 void state_change(enum message m)
2 {
3     static state = s_stop;
4     enum state next;
5     int index = 0;
6 
7     index = lookup_transition(state, m, fsm);
8     if(index!=ERR)
9     {
10         state = fsm[index].next;
11         lookup_action(state, state_action_map)();
12     }
13     return;
14 }

如第3行如示。初始狀態是 停止。在第7行,我們引用了一個尚未寫好的函數。lookup_transition。盡管函數還不存在。只是我們能猜出來它的作用,查表,找到 當前狀態是 state,當前消息是 m 時所相應的表項的下標 index。fsm參數是為了可能有多個狀態遷移表設計的。此處能夠略過。

當查找到 index 以後。且 index 不是 ERR (沒找到)。就能夠令 下一個狀態為state = fsm[index].next,見第10行。



以上,完畢了狀態遷移4要素中的3個:當前狀態、當前消息、將遷移到的狀態。



第11行。完畢的功能是運行與狀態相應的動作。這裏又用到函數指針。

在代碼 lookup_action(state, state_action_map)() 中,lookup_action(state, state_action_map) 用於找到狀態 state 相應的動作。後面的 "()",是由於這個動作是一個函數指針。能夠使用這種方式運行這個指針指向的函數。與上文中的 fsm 參數類似,state_action_map是為了應對有多個狀態-動作表的情況。這裏能夠略過。



不管數據 (狀態遷移、狀態-動作)怎樣變化。引擎代碼都不會變化。所以。甚至能夠把引擎放在靜態或動態鏈接庫裏,或者把數據放在外部文件中。執行時再加載。從而提高部署時的靈活性。

7. 查表

剛剛用到的兩個沒有定義的函數 lookup_transition(state, m, fsm) 和 lookup_action(state, state_action_map) 都使用了查表的方法。



代碼例如以下。能夠看出。二者的結構很類似,遍歷數組 (for循環) ,找到符合條件的元素 (if推斷)。然後把該元素的索引或者該元素結構體的某個成員返回。

ERR 和 ACTION_NOT_FOUND 是用來容錯的,萬一表格有誤。沒有查到匹配的項。

1 int const ERR = -1;
2 int lookup_transition (enum state s, enum message m, struct transition * t)
3 {
4     int ret=ERR;
5     int i;
6     for(i=0;i<transition_num;++i)
7     {
8         if(t[i].current == s && t[i].m == m)
9         {
10             ret = i;
11         }
12     }
13     return ret;
14 }
15 
16 action_foo ACTION_NOT_FOUND = NULL;
17 action_foo lookup_action(enum state s, struct state_action* a)
18 {
19     action_foo ret = ACTION_NOT_FOUND;
20     int i=0;
21     for (i=0;i<state_num;++i)
22     {
23         if(s == a[i].m_state)
24         {
25             ret = a[i].foo;
26         }
27     }
28     return ret;
29 }

8. 總結

geek青年。從接口上看。與普通青年並無不同。甚至在情況相對簡單 (狀態少、狀態遷移種類少) 的時候,代碼量比普通青年還有不如。那麽,geek青年的好處在哪裏呢?

古人雲:滄海橫流方顯英雄本色。古人又雲:大丈夫山崩於前不變色,海嘯於後不動容。

geek青年的好處在於,他始終如一,不管遇到的情形是多麽糟糕多麽惡劣,他始終沒有變化。這個世界上,總須要一些因素,一些承諾,不隨不論什麽易變的感情、不論什麽旁人不能承受的痛苦或誘惑而變化,穩定地堅持。

這才幹讓我們對這個世界保留一絲希望。未來才可以和值得期待。


這一篇和上一篇的代碼在這裏[http://download.csdn.net/detail/younggift/7569627]。


--------------------


博客會手工同步到下面地址:


[http://giftdotyoung.blogspot.com]


[http://blog.csdn.net/younggift]
=======================

geek青年的狀態機,查表,純C語言實現