這個編程技巧別說我沒告訴你
- 2019 年 11 月 25 日
- 筆記
來源:公眾號【編程珠璣】
作者:守望先生
ID:shouwangxiansheng
前言
有讀者在後台留言說用c寫一篇有限狀態機的推文,正好之前也用過,就分享一下吧。
背景
先舉一個簡單的例子,假設是這樣的,一個小孩有兩種狀態,睡眠,清醒。睡的時候可能會撒尿,微笑,撒尿之後會轉為清醒狀態,而清醒的時候可能會笑,會吃,吃完之後會轉會睡眠狀態 用C語言實現,一般寫法可能是這樣的:
//來源:公眾號【編程珠璣】 #include <stdio.h> enum KID_STATUS { SLEEP = 0, WAKE = 1, INVALID = 2 }; enum KID_ACTION { SMILE = 0, EAT = 1, PEE = 2 }; /*孩子當前狀態*/ static int nowStatus = WAKE; /*孩子行為*/ int smile(int status) { printf("kid smilen"); return status; } int eat(int status) { printf("kid eatn"); return SLEEP; } int pee(int status) { printf("kid peen"); return WAKE; } /*孩子產生某個行為*/ void execute(int action) { int nextStatus; /*醒著時的行為*/ if(WAKE == nowStatus) { switch(action) { case SMILE: { nextStatus = smile(nowStatus); break; } case EAT: { nextStatus = eat(nowStatus); break; } case PEE: { nextStatus = pee(nowStatus); break; } default: printf("unknow action when wake %dn",action); nextStatus = nowStatus; break; } } /*睡著時的行為*/ else if(SLEEP == nowStatus) { switch(action) { case SMILE: { nextStatus = smile(nowStatus); break; } casePEE: { nextStatus = pee(nowStatus); break; } default: printf("unknow action when sleep %dn",action); nextStatus = nowStatus; break; } } else { printf("unknow actionn"); } nowStatus = nextStatus; printf("next status is %dn",nowStatus); } int main(void) { nowStatus = SLEEP; execute(EAT); return 0; }
這段程式碼的意圖就非常明顯了,首先判斷當前所在狀態, 然後找到其中的行為,最後執行。 但是這段程式碼有以下幾個特點:
- 新加一種行為需要修改execute函數
- 新加一種行為需要增加更多分支程式碼
- 新加一種狀態,需要新增一個大的分支
- 哪些狀態有哪些行為不是很明顯
換一種寫法
在《高級指針話題-函數指針》中介紹了函數指針,以及在《編程技巧-跳轉表》介紹了函數跳轉表。這裡我們把程式碼調整一下,看看結合跳轉表和狀態機,能寫出什麼樣的程式碼。
//來源:公眾號【編程珠璣】地址:https://www.yanbinghu.com #include <stdio.h> /*省略枚舉定義和函數定義,與前面相同*/ typedef int (*Handler)(int);//函數指針 /*用於處理某種狀態的行為*/ typedef struct Act_Handler { int action; Handler handler; }Act_Handler; /*某種狀態的處理集*/ typedef struct Stat_Handler { int status; int actNum; Act_Handler *actHandler; }Stat_Handler; /*sleep狀態行為處理*/ Act_Handler sleepHandler[] = { {SMILE,smile}, {PEE,pee} }; /*wake狀態行為處理*/ Act_Handler weakHandler[] = { {SMILE,smile}, {PEE,pee}, {EAT,eat} }; /*狀態處理*/ static Stat_Handler statHandler[] = { {SLEEP,sizeof(sleepHandler)/sizeof(Act_Handler),sleepHandler}, {WAKE,sizeof(weakHandler)/sizeof(Act_Handler),weakHandler}, }; static int statSize = sizeof(statHandler)/ sizeof(Stat_Handler); void execute(int action) { if(nowStatus >= statSize) { printf("unknow statusn"); return; } printf("now status is %d,action %dn",nowStatus,action); int nextStatus = nowStatus; Act_Handler *actHandler = statHandler[nowStatus].actHandler; int actNum = statHandler[nowStatus].actNum; int actIdx = 0; /*遍歷指定狀態下的行為集,找到對應的行為*/ for(actIdx = 0;actIdx < actNum;actIdx++) { if(actHandler[actIdx].action == action) { nextStatus = (actHandler[actIdx].handler)(action); break; } } if(actIdx == actNum) { printf("did find action %d in status %dn",action,nowStatus); } nowStatus = nextStatus; printf("next status is %dn",nowStatus); } int main(void) { nowStatus = WAKE; execute(EAT); return 0; }
這裡簡單說明一下execute函數中的執行流程。
- 判斷當前狀態合法性
- 在數組中找到對應狀態的行為處理集
- 在處理集中找到對應的行為
- 處理結束
這種方式有什麼特點呢?可以看到,在需要新加一個動作的時候,只需要在sleepHandler或者weakHandler中添加,完全不影響execute函數的改動。你甚至可以將不同狀態分在不同的文件中管理,使得結構更加清晰明朗。
另外一方面,某種狀態下,能執行哪些動作,非常清晰。
不過這樣的寫法對於初學者來說不太友好,但是不影響你添加新的內容。
有的讀者可能會堪慮,在尋找行為的時候,for循環會不會很慢?首先這和case差別不大,通常不會有性能問題,其次除了使用數組,還可以考慮其他數據結構,例如哈希,我在《工作中用不到演算法,為什麼還要學演算法?》中也提到了,數據結構和演算法能更好地幫我們解決問題。
總結
本文程式碼較多,建議在實際環境中運行調試。完整可運行程式碼也可以閱讀原文。