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} };
當然。為了遵循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語言實現