前言
有讀者在後臺留言說用c寫一篇有限狀態機的推文,正好之前也用過,就分享一下吧。
背景
先舉一個簡單的例子,假設是這樣的,一個小孩有兩種狀態,睡眠,清醒。睡的時候可能會撒尿,微笑,撒尿之後會轉爲清醒狀態,而清醒的時候可能會笑,會喫,喫完之後會轉會睡眠狀態
用C語言實現,一般寫法可能是這樣的:
//來源:公衆號【編程珠璣】
#include <stdio.h>
enum KID_STATUS
{
SLEEP = 0,
WEAK = 1,
INVALID = 2
};
enum KID_ACTION
{
SMILE = 0,
EAT = 1,
PEE = 2
};
/*孩子當前狀態*/
static int nowStatus = WEAK;
/*孩子行爲*/
int smile(int status)
{
printf("kid smile\n");
return status;
}
int eat(int status)
{
printf("kid eat\n");
return SLEEP;
}
int pee(int status)
{
printf("kid pee\n");
return WEAK;
}
/*孩子產生某個行爲*/
void execute(int action)
{
int nextStatus;
/*醒着時的行爲*/
if(WEAK == 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 weak %d\n",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 %d\n",action);
nextStatus = nowStatus;
break;
}
}
else
{
printf("unknow action\n");
}
nowStatus = nextStatus;
printf("next status is %d\n",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}
};
/*weak狀態行爲處理*/
Act_Handler weakHandler[] =
{
{SMILE,smile},
{PEE,pee},
{EAT,eat}
};
/*狀態處理*/
static Stat_Handler statHandler[] =
{
{SLEEP,sizeof(sleepHandler)/sizeof(Act_Handler),sleepHandler},
{WEAK,sizeof(weakHandler)/sizeof(Act_Handler),weakHandler},
};
static int statSize = sizeof(statHandler)/ sizeof(Stat_Handler);
void execute(int action)
{
if(nowStatus >= statSize)
{
printf("unknow status\n");
return;
}
printf("now status is %d,action %d\n",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 %d\n",action,nowStatus);
}
nowStatus = nextStatus;
printf("next status is %d\n",nowStatus);
}
int main(void)
{
nowStatus = WEAK;
execute(EAT);
return 0;
}
這裏簡單說明一下execute函數中的執行流程。
-
判斷當前狀態合法性
-
在數組中找到對應狀態的行爲處理集
-
在處理集中找到對應的行爲
-
處理結束
這種方式有什麼特點呢?可以看到,在需要新加一個動作的時候,只需要在sleepHandler或者weakHandler中添加,完全不影響execute函數的改動。你甚至可以將不同狀態分在不同的文件中管理,使得結構更加清晰明朗。
另外一方面,某種狀態下,能執行哪些動作,非常清晰。
不過這樣的寫法對於初學者來說不太友好,但是不影響你添加新的內容。
有的讀者可能會堪慮,在尋找行爲的時候,for循環會不會很慢?首先這和case差別不大,通常不會有性能問題,其次除了使用數組,還可以考慮其他數據結構,例如哈希,我在《工作中用不到算法,爲什麼還要學算法?》中也提到了,數據結構和算法能更好地幫我們解決問題。
首發:公衆號【編程珠璣】
作者:守望先生
ID:shouwangxiansheng
總結
本文代碼較多,建議在實際環境中運行調試。完整可運行代碼也可以閱讀原文。
推薦閱讀:
關注公衆號【編程珠璣】,獲取更多Linux/C/C++/算法/計算機基礎/工具等原創技術文章。後臺免費獲取經典電子書和視頻資源