我們第一次接觸到狀態機,是在數位電路課程裡。計數器、序列奇偶檢校、檢驗三個1連續出現的報錯電路 等,都需要狀態機作為模型。實現這些功能的電路,與狀態機的狀態轉換圖、狀態轉換表都是等價的。
後來,我們再接觸狀態機,是在編譯原理課程裡。狀態機用於描述與正規表示式匹配的字串。
再後來,我們在gui介面設計中,需要設定一些控制項在某些條件下 禁用,某些條件下使能,某些條件下打個對號。這也可以用狀態機模型來控制。
1. 不要寫成 訊息響應/事件處理
狀態機和訊息響應都是 雙層 switch-case 結構。不同的是,狀態機的外層是狀態,內層是訊息;訊息響應外層是訊息,內層是狀態。
有的同學會說,那又有多大的區別呢?**只是外在形式而非本質,它所反應的是你對模型的理解,或者說,對於問題,你使用了哪種模型。
訊息響應適合於這樣的情形:有很多種訊息,對於同一種訊息,你的程式總是給出同一種反應。打個比方,你女朋友喜歡吃冰淇淋,任何時候你給她買,她都高興,或者轉怒為喜,或者轉悲為喜,總之,會置心情為"喜"。這種情形,適合用訊息響應解決。
而狀態機適合於另一種情形,你的程式是"有狀態的",它在不同的情況 (狀態)下,會對同一訊息做出不同的反應。狀態,是一種資料,但是它影響流程的行為。按物件導向的觀點,資料與流程間的這種高內聚關係,非常適合用 類 來實現。這是題外話,我們回到女朋友和冰淇淋間的關係。你女朋友可能並非在任何情況下吃了冰淇淋都高興,比如剛剛吃完十個八個的時候...這與她當前的狀態有關。
狀態機中,我們需要掌握的核心的資料是:當前狀態,當前訊息,將遷移到的狀態,在遷移中發生的動作。
在狀態機**之前,請先看一段訊息響應機制,vc生成的win32api**大抵如此。我們隨便找來一段片斷看看:
1 lresult callback wndproc(hwnd hwnd, uint message, wparam wparam, lparam lparam)
2 17
break;
18 case wm_paint: .... break;
19 case wm_destroy: ... break;
20 case wm_keydown: ... break;
21 default: return defwindowproc(hwnd, message, wparam, lparam);
22 }
23 return 0;
24 }
第6行開始到第22行結束,對每個訊息給出乙個響應。沒錯,win32api也把這個傳進來的東西稱為 message。這是很典型的適合訊息響應機制的情形,程式對於相同的訊息,處理的方法總是相同的。
我們常常錯誤地把狀態機寫成了訊息響應,訊息這部分處理得不錯,但是,由於沒有很好地記錄和遷移狀態,寫起來容易把自己寫糊塗了。無他,用錯了工具。拿螺絲刀打孔,不是工具差,而是工程師選錯了工具。
2. 狀態機例項,錄音機
例項得是相對簡單的,不然我們很容易淹沒在細節之中,沒有足夠精力去關注狀態機本身的機制了。假設我們**一台錄音機...
備註:我實在想不起來 暫停 和 停止 之間的關係了,似乎是這樣的,又似乎不是。反正大概是那麼個意思,不影響對狀態機的理解,就這麼地吧。
接下來是c**實現。
3. 介面 及 測試
看到以下**,有的同學會說,你這不就是主程式麼,為什麼要把小標題叫做介面。因為,它規定了我們的狀態機函式將是什麼樣子的。
1 enum message ;
2 3 int main(int argc, char *argv)
4 19 }
20 return exit_success;
21 }
上述**規定了,狀態機遷移函式的原型/簽名是 void state_change(enum
message m)。
測試的時候,我們這樣做:./state < test.in。test.in的內容是"psfsbspq",測試時期待看到輸出的狀態遷移過程。之所以這樣做,而不是每次從控制台手動輸入,是因為每次測試的內容都應該是相同的--相同的輸入,程式有相同的反應--可重現性。或者說,dry原則。
乙個非常值得我們注意的問題。在上述介面中,我們看不到"狀態"。事實上,我們將會定義:
enum state ;
但是,介面以外的**,是 *不應該* (是不應該,不是 不必要,是一定不要) 知道狀態的,既不應該知道當前狀態,也不應該知道將要遷移到哪個狀態,也不應該知道在遷移過程中應該做什麼動作。如果介面以外的**知道了這些,就侵入了狀態機的隱私,子系統的邊界就模糊了。而契約的首要任務就是規定邊界,規定國家與個人、個人與個人、個人與集體的邊界。
這一原則,早在195x年,軟體工程剛剛開始的時候就確立了,是最初確立的原則,即 資訊隱藏。後面的原則,都是它的兒子孫子。有個比喻講過這個道理。當你在超市出口付款的時候,你會自己把錢從錢夾裡拿出來遞給售貨員,而不會轉過身去對她說,"在我屁股兜裡,你自己掏吧,別忘了把零錢放回來。"這既增加了假設--你極端信任她,也增加了她的責任。
介面,最主要的任務就是為了明確責任,把責任分布在子系統邊界兩側。其次才是規定呼叫的方法,即邊界長什麼樣。
4. 狀態遷移
以下是狀態機的**片斷。
1 enum state ;
2 void state_change(enum message m)
3 13 else if (m==pause)
14
18 break;
我們還是要關注那四個關鍵點: (1) 當前狀態, (2) 當前訊息, (3) 將遷移到哪個狀態, (4) 遷移中會做哪些動作。
(1) 當前狀態必然是第1行的列舉型別中的乙個。我們初始化狀態為 停止,見第4行。
在第5行到第7行,我們的雙重 switch-case 的外層 按當前狀態分類,如下。
5 switch (s)
6 ;
8 enum message ;
9 10
11 void state_change(enum message m)
12 22 else if (m==pause)
23
27 break;
28 case s_pause:
29 if(m==pause)
30 34
else if(m==stop)
35 39
break;
40 case s_stop:
41 if(m==play)
42 46
if(m==backward)
47 51
if(m==forward)
52 56
if(m==record)
57 61
break;
62 case s_forward:
63 if(m==stop)
64 68
break;
69 case s_backward:
70 if(m==stop)
71 75
break;
76 case s_record:
77 if(m==stop)
78 82
break;
83 84
85 }
86
87 }
88 89
90 int main(int argc, char *argv)
91 106
107
108 }
109
110 return exit_success;
111 }
附錄b 狀態圖源** in graphviz
digraph state
--------------------[[
********************===
geek青年的狀態機
geek青年的狀態機,查表,純c語言實現 1.問題的提出,抽象 建一,不止是他,不少人跟我討論過這樣的問題 如何才能保證在需求變更 擴充的情況下,程式的主體部分不動呢?這是乙個非常深刻和艱難的問題。在進入實質討論之前,我們還得先明確什麼是 主體 就是我們不希望動的那一部分是什麼。事實上,沒有什麼 主...
C語言 狀態機程式設計
狀態機原理 有限狀態機的工作原理如圖1所示,發生事件 event 後,根據當前狀態 cur state 決定執行的動作 action 並設定下乙個狀態號 nxt state fsm的實現方式 1 switch case或者if else 這無意是最直觀的方式,使用一堆條件判斷,會程式設計的人都可以做...
C語言狀態機程式設計
輸入一段字串,字串由字母和分隔符 空格,逗號,句號等 組成,寫乙個函式統計該字串中的單詞數。狀態 typedef enum count state t count state t 判斷函式 static intis end char ch static intis division char ch ...